From f18d035ae13b281c96aa4ed69ca32e507d336e66 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Wed, 19 Jun 2024 09:46:22 +0100 Subject: [PATCH] feat(java): add support for sbt projects using sbt-dependency-lock (#6882) Signed-off-by: knqyf263 Co-authored-by: knqyf263 --- docs/docs/configuration/reporting.md | 2 + docs/docs/coverage/language/index.md | 1 + docs/docs/coverage/language/java.md | 16 +- integration/repo_test.go | 8 + .../testdata/fixtures/repo/sbt/build.sbt.lock | 29 ++++ integration/testdata/sbt.json.golden | 149 ++++++++++++++++++ pkg/dependency/id.go | 2 +- pkg/dependency/id_test.go | 9 ++ pkg/dependency/parser/sbt/lockfile/parse.go | 84 ++++++++++ .../parser/sbt/lockfile/parse_test.go | 77 +++++++++ .../sbt/lockfile/testdata/empty.sbt.lock | 10 ++ .../sbt/lockfile/testdata/v1_happy.sbt.lock | 73 +++++++++ pkg/detector/library/driver.go | 2 +- pkg/fanal/analyzer/all/import.go | 1 + pkg/fanal/analyzer/const.go | 3 + .../analyzer/language/java/sbt/lockfile.go | 47 ++++++ .../language/java/sbt/lockfile_test.go | 92 +++++++++++ .../java/sbt/testdata/empty/build.sbt.lock | 10 ++ .../java/sbt/testdata/v1/build.sbt.lock | 59 +++++++ pkg/fanal/types/const.go | 2 + pkg/purl/purl.go | 2 +- pkg/purl/purl_test.go | 14 ++ 22 files changed, 687 insertions(+), 5 deletions(-) create mode 100644 integration/testdata/fixtures/repo/sbt/build.sbt.lock create mode 100644 integration/testdata/sbt.json.golden create mode 100644 pkg/dependency/parser/sbt/lockfile/parse.go create mode 100644 pkg/dependency/parser/sbt/lockfile/parse_test.go create mode 100644 pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock create mode 100644 pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock create mode 100644 pkg/fanal/analyzer/language/java/sbt/lockfile.go create mode 100644 pkg/fanal/analyzer/language/java/sbt/lockfile_test.go create mode 100644 pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock create mode 100644 pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock diff --git a/docs/docs/configuration/reporting.md b/docs/docs/configuration/reporting.md index b8b61d34a346..17bf0b864283 100644 --- a/docs/docs/configuration/reporting.md +++ b/docs/docs/configuration/reporting.md @@ -64,6 +64,7 @@ The following languages are currently supported: | PHP | [composer.lock][composer-lock] | | Java | [pom.xml][pom-xml] | | | [*gradle.lockfile][gradle-lockfile] | +| | [*.sbt.lock][sbt-lockfile] | | Dart | [pubspec.lock][pubspec-lock] | This tree is the reverse of the dependency graph. @@ -447,5 +448,6 @@ $ trivy convert --format table --severity CRITICAL result.json [composer-lock]: ../coverage/language/php.md#composer [pom-xml]: ../coverage/language/java.md#pomxml [gradle-lockfile]: ../coverage/language/java.md#gradlelock +[sbt-lockfile]: ../coverage/language/java.md#sbt [pubspec-lock]: ../coverage/language/dart.md#dart [cargo-binaries]: ../coverage/language/rust.md#binaries \ No newline at end of file diff --git a/docs/docs/coverage/language/index.md b/docs/docs/coverage/language/index.md index eb694bbcc228..b9461b4e013b 100644 --- a/docs/docs/coverage/language/index.md +++ b/docs/docs/coverage/language/index.md @@ -38,6 +38,7 @@ On the other hand, when the target is a post-build artifact, like a container im | [Java](java.md) | JAR/WAR/PAR/EAR[^4] | ✅ | ✅ | - | - | | | pom.xml | - | - | ✅ | ✅ | | | *gradle.lockfile | - | - | ✅ | ✅ | +| | *.sbt.lock | - | - | ✅ | ✅ | | [Go](golang.md) | Binaries built by Go | ✅ | ✅ | - | - | | | go.mod | - | - | ✅ | ✅ | | [Rust](rust.md) | Cargo.lock | ✅ | ✅ | ✅ | ✅ | diff --git a/docs/docs/coverage/language/java.md b/docs/docs/coverage/language/java.md index 87db939ea288..bb90366c1772 100644 --- a/docs/docs/coverage/language/java.md +++ b/docs/docs/coverage/language/java.md @@ -1,5 +1,5 @@ # Java -Trivy supports three types of Java scanning: `JAR/WAR/PAR/EAR`, `pom.xml` and `*gradle.lockfile` files. +Trivy supports four types of Java scanning: `JAR/WAR/PAR/EAR`, `pom.xml`, `*gradle.lockfile` and `*.sbt.lock` files. Each artifact supports the following scanners: @@ -8,6 +8,7 @@ Each artifact supports the following scanners: | JAR/WAR/PAR/EAR | ✓ | ✓ | - | | pom.xml | ✓ | ✓ | ✓ | | *gradle.lockfile | ✓ | ✓ | ✓ | +| *.sbt.lock | ✓ | ✓ | - | The following table provides an outline of the features Trivy offers. @@ -16,6 +17,7 @@ The following table provides an outline of the features Trivy offers. | JAR/WAR/PAR/EAR | Trivy Java DB | Include | - | - | | pom.xml | Maven repository [^1] | Exclude | ✓ | ✓[^7] | | *gradle.lockfile | - | Exclude | ✓ | ✓ | +| *.sbt.lock | - | Exclude | - | ✓ | These may be enabled or disabled depending on the target. See [here](./index.md) for the detail. @@ -94,6 +96,15 @@ Trity also can detect licenses for dependencies. Make sure that you have cache[^8] directory to find licenses from `*.pom` dependency files. + +## SBT + +`build.sbt.lock` files only contain information about used dependencies. This requires a lockfile generated using the +[sbt-dependency-lock][sbt-dependency-lock] plugin. + +!!!note + All necessary files are checked locally. SBT file scanning doesn't require internet access. + [^1]: Uses maven repository to get information about dependencies. Internet access required. [^2]: It means `*.jar`, `*.war`, `*.par` and `*.ear` file [^3]: `ArtifactID`, `GroupID` and `Version` @@ -106,4 +117,5 @@ Make sure that you have cache[^8] directory to find licenses from `*.pom` depend [dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies [maven-invoker-plugin]: https://maven.apache.org/plugins/maven-invoker-plugin/usage.html [maven-central]: https://repo.maven.apache.org/maven2/ -[maven-pom-repos]: https://maven.apache.org/settings.html#repositories \ No newline at end of file +[maven-pom-repos]: https://maven.apache.org/settings.html#repositories +[sbt-dependency-lock]: https://stringbean.github.io/sbt-dependency-lock diff --git a/integration/repo_test.go b/integration/repo_test.go index e11e5a21a8b4..49e342d1a91a 100644 --- a/integration/repo_test.go +++ b/integration/repo_test.go @@ -153,6 +153,14 @@ func TestRepository(t *testing.T) { }, golden: "testdata/gradle.json.golden", }, + { + name: "sbt", + args: args{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/sbt", + }, + golden: "testdata/sbt.json.golden", + }, { name: "conan", args: args{ diff --git a/integration/testdata/fixtures/repo/sbt/build.sbt.lock b/integration/testdata/fixtures/repo/sbt/build.sbt.lock new file mode 100644 index 000000000000..33bcdbee245e --- /dev/null +++ b/integration/testdata/fixtures/repo/sbt/build.sbt.lock @@ -0,0 +1,29 @@ +{ + "lockVersion" : 1, + "timestamp" : "2024-06-06T11:03:09.964557Z", + "configurations" : [ + "compile", + "optional", + "provided", + "runtime", + "test" + ], + "dependencies" : [ + { + "org" : "com.fasterxml.jackson.core", + "name" : "jackson-databind", + "version" : "2.9.1", + "artifacts" : [ + { + "name" : "jackson-databind.jar", + "hash" : "sha1:716da1830a2043f18882fc036ec26eb32cbe5aff" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/sbt.json.golden b/integration/testdata/sbt.json.golden new file mode 100644 index 000000000000..94bf111fd221 --- /dev/null +++ b/integration/testdata/sbt.json.golden @@ -0,0 +1,149 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/sbt", + "ArtifactType": "repository", + "Metadata": { + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "build.sbt.lock", + "Class": "lang-pkgs", + "Type": "sbt", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-9548", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "9ccd2eb3e03373ff" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.4", + "Status": "fixed", + "Layer": {}, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-9548", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Title": "jackson-databind: Serialization gadgets in anteros-core", + "Description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 6.8, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-9548", + "https://github.com/FasterXML/jackson-databind/issues/2634", + "https://github.com/advisories/GHSA-p43x-xfjf-5jhr", + "https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html", + "https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062", + "https://nvd.nist.gov/vuln/detail/CVE-2020-9548", + "https://security.netapp.com/advisory/ntap-20200904-0006/", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ], + "PublishedDate": "2020-03-02T04:15:00Z", + "LastModifiedDate": "2021-12-02T21:23:00Z" + }, + { + "VulnerabilityID": "CVE-2021-20190", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "9ccd2eb3e03373ff" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.7", + "Status": "fixed", + "Layer": {}, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-20190", + "DataSource": { + "ID": "glad", + "Name": "GitLab Advisory Database Community", + "URL": "https://gitlab.com/gitlab-org/advisories-community" + }, + "Title": "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing", + "Description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:C", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 8.3, + "V3Score": 8.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2021-20190", + "https://bugzilla.redhat.com/show_bug.cgi?id=1916633", + "https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a", + "https://github.com/FasterXML/jackson-databind/issues/2854", + "https://github.com/advisories/GHSA-5949-rw7g-wx7w", + "https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-20190", + "https://security.netapp.com/advisory/ntap-20210219-0008/" + ], + "PublishedDate": "2021-01-19T17:15:00Z", + "LastModifiedDate": "2021-07-20T23:15:00Z" + } + ] + } + ] +} diff --git a/pkg/dependency/id.go b/pkg/dependency/id.go index d40289cedc6a..8f01bf23123a 100644 --- a/pkg/dependency/id.go +++ b/pkg/dependency/id.go @@ -25,7 +25,7 @@ func ID(ltype types.LangType, name, version string) string { if !strings.HasPrefix(version, "v") { version = "v" + version } - case types.Jar, types.Pom, types.Gradle: + case types.Jar, types.Pom, types.Gradle, types.Sbt: sep = ":" } return name + sep + version diff --git a/pkg/dependency/id_test.go b/pkg/dependency/id_test.go index 68e380e6c651..18359f771e7b 100644 --- a/pkg/dependency/id_test.go +++ b/pkg/dependency/id_test.go @@ -47,6 +47,15 @@ func TestID(t *testing.T) { }, want: "test:1.0.0", }, + { + name: "sbt", + args: args{ + ltype: types.Sbt, + name: "test", + version: "1.0.0", + }, + want: "test:1.0.0", + }, { name: "pip", args: args{ diff --git a/pkg/dependency/parser/sbt/lockfile/parse.go b/pkg/dependency/parser/sbt/lockfile/parse.go new file mode 100644 index 000000000000..3b5b1865903d --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/parse.go @@ -0,0 +1,84 @@ +package lockfile + +import ( + "io" + "slices" + "sort" + + "github.com/liamg/jfather" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +// lockfile format defined at: https://stringbean.github.io/sbt-dependency-lock/file-formats/version-1.html +type sbtLockfile struct { + Version int `json:"lockVersion"` + Dependencies []sbtLockfileDependency `json:"dependencies"` +} + +type sbtLockfileDependency struct { + Organization string `json:"org"` + Name string `json:"name"` + Version string `json:"version"` + Configurations []string `json:"configurations"` + StartLine int + EndLine int +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockfile sbtLockfile + input, err := io.ReadAll(r) + + if err != nil { + return nil, nil, xerrors.Errorf("failed to read sbt lockfile: %w", err) + } + if err := jfather.Unmarshal(input, &lockfile); err != nil { + return nil, nil, xerrors.Errorf("JSON decoding failed: %w", err) + } + + var libraries ftypes.Packages + + for _, dep := range lockfile.Dependencies { + if slices.ContainsFunc(dep.Configurations, isIncludedConfig) { + name := dep.Organization + ":" + dep.Name + libraries = append(libraries, ftypes.Package{ + ID: dependency.ID(ftypes.Sbt, name, dep.Version), + Name: name, + Version: dep.Version, + Locations: []ftypes.Location{ + { + StartLine: dep.StartLine, + EndLine: dep.EndLine, + }, + }, + }) + } + } + + sort.Sort(libraries) + return libraries, nil, nil +} + +// UnmarshalJSONWithMetadata needed to detect start and end lines of deps +func (t *sbtLockfileDependency) UnmarshalJSONWithMetadata(node jfather.Node) error { + if err := node.Decode(&t); err != nil { + return err + } + // Decode func will overwrite line numbers if we save them first + t.StartLine = node.Range().Start.Line + t.EndLine = node.Range().End.Line + return nil +} + +func isIncludedConfig(config string) bool { + return config == "compile" || config == "runtime" +} diff --git a/pkg/dependency/parser/sbt/lockfile/parse_test.go b/pkg/dependency/parser/sbt/lockfile/parse_test.go new file mode 100644 index 000000000000..a11e7b8aedb2 --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/parse_test.go @@ -0,0 +1,77 @@ +package lockfile + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + }{ + { + name: "v1 happy path", + inputFile: "testdata/v1_happy.sbt.lock", + want: []ftypes.Package{ + { + ID: "org.apache.commons:commons-lang3:3.9", + Name: "org.apache.commons:commons-lang3", + Version: "3.9", + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 25, + }, + }, + }, + { + ID: "org.scala-lang:scala-library:2.12.10", + Name: "org.scala-lang:scala-library", + Version: "2.12.10", + Locations: []ftypes.Location{ + { + StartLine: 26, + EndLine: 41, + }, + }, + }, + { + ID: "org.typelevel:cats-core_2.12:2.9.0", + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + Locations: []ftypes.Location{ + { + StartLine: 42, + EndLine: 57, + }, + }, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty.sbt.lock", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := NewParser() + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + libs, _, err := parser.Parse(f) + require.NoError(t, err) + + assert.Equal(t, tt.want, libs) + }) + } +} diff --git a/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock b/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock new file mode 100644 index 000000000000..6125547882da --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock @@ -0,0 +1,10 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [] +} \ No newline at end of file diff --git a/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock b/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock new file mode 100644 index 000000000000..0dcba8ba09c6 --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock @@ -0,0 +1,73 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [ + { + "org": "org.apache.commons", + "name": "commons-lang3", + "version": "3.9", + "artifacts": [ + { + "name": "commons-lang3.jar", + "hash": "sha1:0122c7cee69b53ed4a7681c03d4ee4c0e2765da5" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org": "org.scala-lang", + "name": "scala-library", + "version": "2.12.10", + "artifacts": [ + { + "name": "scala-library.jar", + "hash": "sha1:3509860bc2e5b3da001ed45aca94ffbe5694dbda" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org" : "org.typelevel", + "name" : "cats-core_2.12", + "version" : "2.9.0", + "artifacts" : [ + { + "name" : "cats-core_2.12.jar", + "hash" : "sha1:844f21541d1809008586fbc1172dc02c96476639" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + }, + { + "org" : "org.scalatest", + "name" : "scalatest-core_2.13", + "version" : "3.2.15", + "artifacts" : [ + { + "name" : "scalatest-core_2.13.jar", + "hash" : "sha1:231d1f4049a9fa4bd65c17b806a58180b9f4abe1" + } + ], + "configurations" : [ + "test" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go index f78932b13442..af91c30c786f 100644 --- a/pkg/detector/library/driver.go +++ b/pkg/detector/library/driver.go @@ -39,7 +39,7 @@ func NewDriver(libType ftypes.LangType) (Driver, bool) { case ftypes.GoBinary, ftypes.GoModule: ecosystem = vulnerability.Go comparer = compare.GenericComparer{} - case ftypes.Jar, ftypes.Pom, ftypes.Gradle: + case ftypes.Jar, ftypes.Pom, ftypes.Gradle, ftypes.Sbt: ecosystem = vulnerability.Maven comparer = maven.Comparer{} case ftypes.Npm, ftypes.Yarn, ftypes.Pnpm, ftypes.NodePkg, ftypes.JavaScript: diff --git a/pkg/fanal/analyzer/all/import.go b/pkg/fanal/analyzer/all/import.go index a5b0d05298a1..1849bcebf682 100644 --- a/pkg/fanal/analyzer/all/import.go +++ b/pkg/fanal/analyzer/all/import.go @@ -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/java/sbt" _ "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" diff --git a/pkg/fanal/analyzer/const.go b/pkg/fanal/analyzer/const.go index 99ac3cf4bee5..82bf3bb12296 100644 --- a/pkg/fanal/analyzer/const.go +++ b/pkg/fanal/analyzer/const.go @@ -55,6 +55,7 @@ const ( TypeJar Type = "jar" TypePom Type = "pom" TypeGradleLock Type = "gradle-lockfile" + TypeSbtLock Type = "sbt-lockfile" // Node.js TypeNpmPkgLock Type = "npm" @@ -173,6 +174,7 @@ var ( TypeJar, TypePom, TypeGradleLock, + TypeSbtLock, TypeNpmPkgLock, TypeNodePkg, TypeYarn, @@ -210,6 +212,7 @@ var ( TypePom, TypeConanLock, TypeGradleLock, + TypeSbtLock, TypeCocoaPods, TypeSwift, TypePubSpecLock, diff --git a/pkg/fanal/analyzer/language/java/sbt/lockfile.go b/pkg/fanal/analyzer/language/java/sbt/lockfile.go new file mode 100644 index 000000000000..4d1d17cb5d34 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/lockfile.go @@ -0,0 +1,47 @@ +package sbt + +import ( + "context" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/sbt/lockfile" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&sbtDependencyLockAnalyzer{}) +} + +const version = 1 + +// sbtDependencyLockAnalyzer analyzes '*.sbt.lock' +type sbtDependencyLockAnalyzer struct{} + +func (a sbtDependencyLockAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + parser := lockfile.NewParser() + + res, err := language.Analyze(types.Sbt, input.FilePath, input.Content, parser) + + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + + return res, nil +} + +func (a sbtDependencyLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return types.SbtLock == filepath.Base(filePath) +} + +func (a sbtDependencyLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeSbtLock +} + +func (a sbtDependencyLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go b/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go new file mode 100644 index 000000000000..0469d9156a19 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go @@ -0,0 +1,92 @@ +package sbt + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_sbtDependencyLockAnalyzer(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "v1 lockfile", + inputFile: "testdata/v1/build.sbt.lock", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Sbt, + FilePath: "testdata/v1/build.sbt.lock", + Packages: types.Packages{ + { + ID: "org.apache.commons:commons-lang3:3.9", + Name: "org.apache.commons:commons-lang3", + Version: "3.9", + Locations: []types.Location{ + { + StartLine: 10, + EndLine: 25, + }, + }, + }, + { + ID: "org.scala-lang:scala-library:2.12.10", + Name: "org.scala-lang:scala-library", + Version: "2.12.10", + Locations: []types.Location{ + { + StartLine: 26, + EndLine: 41, + }, + }, + }, + { + ID: "org.typelevel:cats-core_2.12:2.9.0", + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + Locations: []types.Location{ + { + StartLine: 42, + EndLine: 57, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty lockfile", + inputFile: "testdata/empty/build.sbt.lock", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + a := sbtDependencyLockAnalyzer{} + ctx := context.Background() + + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock b/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock new file mode 100644 index 000000000000..6125547882da --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock @@ -0,0 +1,10 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock b/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock new file mode 100644 index 000000000000..26b5ef401aeb --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock @@ -0,0 +1,59 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [ + { + "org": "org.apache.commons", + "name": "commons-lang3", + "version": "3.9", + "artifacts": [ + { + "name": "commons-lang3.jar", + "hash": "sha1:0122c7cee69b53ed4a7681c03d4ee4c0e2765da5" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org": "org.scala-lang", + "name": "scala-library", + "version": "2.12.10", + "artifacts": [ + { + "name": "scala-library.jar", + "hash": "sha1:3509860bc2e5b3da001ed45aca94ffbe5694dbda" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org" : "org.typelevel", + "name" : "cats-core_2.12", + "version" : "2.9.0", + "artifacts" : [ + { + "name" : "cats-core_2.12.jar", + "hash" : "sha1:844f21541d1809008586fbc1172dc02c96476639" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index 6874b8a40b06..9ad8c1a2c57b 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -63,6 +63,7 @@ const ( Jar LangType = "jar" Pom LangType = "pom" Gradle LangType = "gradle" + Sbt LangType = "sbt" GoBinary LangType = "gobinary" GoModule LangType = "gomod" JavaScript LangType = "javascript" @@ -114,6 +115,7 @@ const ( GoSum = "go.sum" MavenPom = "pom.xml" + SbtLock = "build.sbt.lock" NpmPkg = "package.json" NpmPkgLock = "package-lock.json" diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go index e312e40043f7..1ccf39c8530a 100644 --- a/pkg/purl/purl.go +++ b/pkg/purl/purl.go @@ -442,7 +442,7 @@ func parseJulia(pkgName, pkgUUID string) (string, string, packageurl.Qualifiers) func purlType(t ftypes.TargetType) string { switch t { - case ftypes.Jar, ftypes.Pom, ftypes.Gradle: + case ftypes.Jar, ftypes.Pom, ftypes.Gradle, ftypes.Sbt: return packageurl.TypeMaven case ftypes.Bundler, ftypes.GemSpec: return packageurl.TypeGem diff --git a/pkg/purl/purl_test.go b/pkg/purl/purl_test.go index ddcfc98222e6..25e9e7829d7b 100644 --- a/pkg/purl/purl_test.go +++ b/pkg/purl/purl_test.go @@ -51,6 +51,20 @@ func TestNewPackageURL(t *testing.T) { Version: "5.3.14", }, }, + { + name: "sbt package", + typ: ftypes.Sbt, + pkg: ftypes.Package{ + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.typelevel", + Name: "cats-core_2.12", + Version: "2.9.0", + }, + }, { name: "yarn package", typ: ftypes.Yarn,