diff --git a/syft/pkg/cataloger/gitlab/cataloger.go b/syft/pkg/cataloger/gitlab/cataloger.go new file mode 100644 index 000000000..bab5f52ad --- /dev/null +++ b/syft/pkg/cataloger/gitlab/cataloger.go @@ -0,0 +1,15 @@ +package gitlab + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +const ( + catalogerName = "gitlab-cataloger" + versionManifestGlob = "**/opt/gitlab/version-manifest.json" +) + +func NewGitLabCataloger() *generic.Cataloger { + return generic.NewCataloger(catalogerName). + WithParserByGlobs(parseVersionManifest, versionManifestGlob) +} diff --git a/syft/pkg/cataloger/gitlab/parse_manifest.go b/syft/pkg/cataloger/gitlab/parse_manifest.go new file mode 100644 index 000000000..2fb9f1a10 --- /dev/null +++ b/syft/pkg/cataloger/gitlab/parse_manifest.go @@ -0,0 +1,96 @@ +package gitlab + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +type versionManifest struct { + ManifestFormat int `json:"manifest_format"` + Software map[string]manifestPackage `json:"software"` + BuildVersion string `json:"build_version"` + BuildGitRevision string `json:"build_git_revision"` + License string `json:"license"` +} + +type manifestPackage struct { + LockedVersion *string `json:"locked_version"` + LockedSource *lockedSource `json:"locked_source"` + SourceType *string `json:"url"` + DescribedVersion *string `json:"described_version"` + DisplayVersion *string `json:"display_version"` + Vendor *string `json:"vendor"` + License *string `json:"license"` +} + +type lockedSource struct { + Git string `json:"git"` + URL string `json:"url"` + Sha256 string `json:"sha256"` +} + +func parseVersionManifest(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + bytes, err := io.ReadAll(reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to load version-manifest.json file: %w", err) + } + + var pkgs []pkg.Package + var manifest versionManifest + err = json.Unmarshal(bytes, &manifest) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse version-manifest.json file: %w", err) + } + + for pkgName, pkgData := range manifest.Software { + p := pkg.Package{ + Name: pkgName, + Locations: file.NewLocationSet(reader.Location), + } + + if pkgData.DisplayVersion != nil { + p.Version = *pkgData.DisplayVersion + } + if pkgData.License != nil { + p.Licenses = pkg.NewLicenseSet(pkg.NewLicense(*pkgData.License)) + } + if pkgData.LockedSource != nil && (*pkgData.SourceType == "git" || *pkgData.SourceType == "url") { + var purl packageurl.PackageURL + if *pkgData.SourceType == "git" { + purl.Type = "gitlab" + gitUrlComponents := strings.Split(pkgData.LockedSource.Git, "/") + // Namespace is the user or organization + purl.Namespace = strings.Split(gitUrlComponents[0], ":")[1] + // Name is the repository name (with .git sliced out) + purl.Name = gitUrlComponents[1][:len(gitUrlComponents[1])-4] + } else { + purl.Type = "generic" + purl.Qualifiers = append( + purl.Qualifiers, + packageurl.Qualifier{ + Key: "download_url", + Value: pkgData.LockedSource.URL, + }, + packageurl.Qualifier{ + Key: "checksum", + Value: pkgData.LockedSource.Sha256, + }) + } + } + + pkgs = append(pkgs, p) + } + + pkg.Sort(pkgs) + + return pkgs, nil, nil +} diff --git a/syft/pkg/cataloger/gitlab/parse_manifest_test.go b/syft/pkg/cataloger/gitlab/parse_manifest_test.go new file mode 100644 index 000000000..d8ef100ec --- /dev/null +++ b/syft/pkg/cataloger/gitlab/parse_manifest_test.go @@ -0,0 +1,26 @@ +package gitlab + +import ( + "testing" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" +) + +func TestParseVersionManifestFile(t *testing.T) { + fixture := "test-fixtures/glob-paths/opt/gitlab/version-manifest.json" + locations := file.NewLocationSet(file.NewLocation(fixture)) + + var expectedPkg = pkg.Package{ + Name: "openssl", + Version: "1.1.1q", + Locations: locations, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocations("OpenSSL"), + ), + PURL: "pkg:gitlab/omnibus-mirror/openssl", + } + + pkgtest.TestFileParser(t, fixture, parseVersionManifest, []pkg.Package{expectedPkg}, nil) +} diff --git a/syft/pkg/cataloger/gitlab/test-fixtures/glob-paths/opt/gitlab/version-manifest.json b/syft/pkg/cataloger/gitlab/test-fixtures/glob-paths/opt/gitlab/version-manifest.json new file mode 100644 index 000000000..8c641dc35 --- /dev/null +++ b/syft/pkg/cataloger/gitlab/test-fixtures/glob-paths/opt/gitlab/version-manifest.json @@ -0,0 +1,19 @@ +{ + "manifest_format": 2, + "software": { + "openssl": { + "locked_version": "29708a562a1887a91de0fa6ca668c71871accde9", + "locked_source": { + "git": "git@dev.gitlab.org:omnibus-mirror/openssl.git" + }, + "source_type": "git", + "described_version": "OpenSSL_1_1_1q", + "display_version": "1.1.1q", + "vendor": "openssl", + "license": "OpenSSL" + } + }, + "build_version": "15.6.1", + "build_git_revision": "e3d1cd74ef1abe2b9514d8aa64c065b434becd3a", + "license": "MIT" +} \ No newline at end of file