From 707b03d9361ec583f6ad0301be3f2936d83f26f2 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Wed, 5 Jun 2024 15:46:22 +0100 Subject: [PATCH] Add support for sbt-dependency-lock lockfiles --- pkg/sbt/lockfile/parse.go | 74 ++++++++++++++++++ pkg/sbt/lockfile/parse_test.go | 87 +++++++++++++++++++++ pkg/sbt/lockfile/testdata/empty.sbt.lock | 10 +++ pkg/sbt/lockfile/testdata/v1_happy.sbt.lock | 59 ++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 pkg/sbt/lockfile/parse.go create mode 100644 pkg/sbt/lockfile/parse_test.go create mode 100644 pkg/sbt/lockfile/testdata/empty.sbt.lock create mode 100644 pkg/sbt/lockfile/testdata/v1_happy.sbt.lock diff --git a/pkg/sbt/lockfile/parse.go b/pkg/sbt/lockfile/parse.go new file mode 100644 index 00000000..76581753 --- /dev/null +++ b/pkg/sbt/lockfile/parse.go @@ -0,0 +1,74 @@ +package lockfile + +import ( + dio "github.com/aquasecurity/go-dep-parser/pkg/io" + "github.com/aquasecurity/go-dep-parser/pkg/types" + "github.com/liamg/jfather" + "golang.org/x/xerrors" + "io" +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (Parser) Parse(r dio.ReadSeekerAt) ([]types.Library, []types.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 []types.Library + + for _, dependency := range lockfile.Dependencies { + libraries = append(libraries, types.Library{ + ID: dependency.Organization + ":" + dependency.Name + ":" + dependency.Version, + Name: dependency.Organization + ":" + dependency.Name, + Version: dependency.Version, + Locations: []types.Location{{StartLine: dependency.StartLine, EndLine: dependency.EndLine}}, + }) + } + + 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 +} + +// lockfile format defined at: https://stringbean.github.io/sbt-dependency-lock/file-formats/version-1.html +type sbtLockfile struct { + Version int `json:"lockVersion"` + Timestamp string `json:"timestamp"` + Configurations []string `json:"configurations"` + Dependencies []sbtLockfileDependency `json:"dependencies"` +} + +type sbtLockfileDependency struct { + Organization string `json:"org"` + Name string `json:"name"` + Version string `json:"version"` + Artifacts []sbtLockfileArtifact `json:"artifacts"` + Configurations []string `json:"configurations"` + StartLine int + EndLine int +} + +type sbtLockfileArtifact struct { + Name string `json:"name"` + Hash string `json:"hash"` +} diff --git a/pkg/sbt/lockfile/parse_test.go b/pkg/sbt/lockfile/parse_test.go new file mode 100644 index 00000000..04946941 --- /dev/null +++ b/pkg/sbt/lockfile/parse_test.go @@ -0,0 +1,87 @@ +package lockfile + +import ( + "github.com/aquasecurity/go-dep-parser/pkg/types" + "github.com/stretchr/testify/assert" + "os" + "sort" + "strings" + "testing" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []types.Library + }{ + { + name: "v1 happy path", + inputFile: "testdata/v1_happy.sbt.lock", + want: []types.Library{ + { + 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", + 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) + assert.NoError(t, err) + + libs, _, err := parser.Parse(f) + assert.Equal(t, nil, err) + + sortLibs(libs) + assert.Equal(t, tt.want, libs) + }) + } +} + +func sortLibs(libs []types.Library) { + sort.Slice(libs, func(i, j int) bool { + ret := strings.Compare(libs[i].Name, libs[j].Name) + if ret == 0 { + return libs[i].Version < libs[j].Version + } + return ret < 0 + }) +} diff --git a/pkg/sbt/lockfile/testdata/empty.sbt.lock b/pkg/sbt/lockfile/testdata/empty.sbt.lock new file mode 100644 index 00000000..61255478 --- /dev/null +++ b/pkg/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/sbt/lockfile/testdata/v1_happy.sbt.lock b/pkg/sbt/lockfile/testdata/v1_happy.sbt.lock new file mode 100644 index 00000000..26b5ef40 --- /dev/null +++ b/pkg/sbt/lockfile/testdata/v1_happy.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