Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 51 additions & 84 deletions ecosystems.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import (
"github.com/ecosyste-ms/ecosystems-go"
"github.com/ecosyste-ms/ecosystems-go/packages"
"github.com/git-pkgs/registries"
"github.com/oapi-codegen/nullable"
)

// nullableValue returns the underlying value of n, or the zero value of T if n
// is null or unspecified.
func nullableValue[T any](n nullable.Nullable[T]) T {
v, _ := n.Get()
return v
}

// EcosystemsClient wraps the ecosyste.ms API client.
type EcosystemsClient struct {
client *ecosystems.Client
Expand Down Expand Up @@ -58,30 +66,20 @@ func (c *EcosystemsClient) BulkLookup(ctx context.Context, purls []string) (map[
RegistryURL: registries.DefaultURL(pkg.Ecosystem),
Source: "ecosystems",
}
if pkg.LatestReleaseNumber != nil {
info.LatestVersion = *pkg.LatestReleaseNumber
}
info.LatestVersion = nullableValue(pkg.LatestReleaseNumber)
if len(pkg.NormalizedLicenses) > 0 {
info.License = pkg.NormalizedLicenses[0]
} else if pkg.Licenses != nil && *pkg.Licenses != "" {
info.License = *pkg.Licenses
}
if pkg.Description != nil {
info.Description = *pkg.Description
} else if licenses := nullableValue(pkg.Licenses); licenses != "" {
info.License = licenses
}
if pkg.Homepage != nil {
info.Homepage = *pkg.Homepage
}
if pkg.RepositoryUrl != nil {
info.Repository = *pkg.RepositoryUrl
}
info.ChangelogFilename = extractChangelogFilename(pkg.RepoMetadata)
info.Description = nullableValue(pkg.Description)
info.Homepage = nullableValue(pkg.Homepage)
info.Repository = nullableValue(pkg.RepositoryURL)
info.ChangelogFilename = extractChangelogFilename(nullableValue(pkg.RepoMetadata))

// Popularity and usage
info.Downloads = pkg.Downloads
if pkg.DownloadsPeriod != nil {
info.DownloadsPeriod = *pkg.DownloadsPeriod
}
info.DownloadsPeriod = nullableValue(pkg.DownloadsPeriod)
info.DependentPackagesCount = pkg.DependentPackagesCount
info.DependentReposCount = pkg.DependentReposCount

Expand All @@ -96,12 +94,8 @@ func (c *EcosystemsClient) BulkLookup(ctx context.Context, purls []string) (map[

// extractChangelogFilename digs into the ecosyste.ms RepoMetadata to find the
// changelog filename at metadata.files.changelog.
func extractChangelogFilename(repoMetadata *map[string]interface{}) string {
if repoMetadata == nil {
return ""
}
meta := *repoMetadata
metadataRaw, ok := meta["metadata"]
func extractChangelogFilename(repoMetadata map[string]interface{}) string {
metadataRaw, ok := repoMetadata["metadata"]
if !ok {
return ""
}
Expand Down Expand Up @@ -176,26 +170,20 @@ func (c *EcosystemsClient) GetVersion(ctx context.Context, purlStr string) (*Ver

func versionInfoFromEcosystems(
number string,
publishedAt, integrity, license, status *string,
metadata *map[string]any,
publishedAt, integrity, license, status nullable.Nullable[string],
metadata nullable.Nullable[map[string]any],
) VersionInfo {
info := VersionInfo{Number: number}
if publishedAt != nil {
info.PublishedAt, _ = time.Parse(time.RFC3339, *publishedAt)
}
if integrity != nil {
info.Integrity = *integrity
if pub := nullableValue(publishedAt); pub != "" {
info.PublishedAt, _ = time.Parse(time.RFC3339, pub)
}
if license != nil {
info.License = *license
}
if status != nil {
info.Status = *status
info.Integrity = nullableValue(integrity)
info.License = nullableValue(license)
if s, err := status.Get(); err == nil {
info.Status = s
info.Yanked = info.Status == string(registries.StatusYanked)
}
if metadata != nil {
info.Metadata = *metadata
}
info.Metadata = nullableValue(metadata)
return info
}

Expand Down Expand Up @@ -235,7 +223,7 @@ func (c *EcosystemsClient) GetDependentsByRepositoryURL(ctx context.Context, rep
result = append(result, RepositoryDependents{
PackageName: pkg.Name,
Ecosystem: pkg.Ecosystem,
PURL: pkg.Purl,
PURL: pkg.PURL,
Dependents: out,
})
}
Expand All @@ -246,32 +234,25 @@ func convertDependentPackage(pkg packages.Package) DependentPackage {
out := DependentPackage{
Ecosystem: pkg.Ecosystem,
Name: pkg.Name,
PURL: pkg.Purl,
PURL: pkg.PURL,
Downloads: pkg.Downloads,
DependentReposCount: pkg.DependentReposCount,
}
if pkg.RepositoryUrl != nil {
out.Repository = *pkg.RepositoryUrl
}
out.Repository = nullableValue(pkg.RepositoryURL)
if out.Repository == "" {
out.Repository = extractRepoHTMLURL(pkg.RepoMetadata)
out.Repository = extractRepoHTMLURL(nullableValue(pkg.RepoMetadata))
}
if pkg.RegistryUrl != nil {
out.RegistryURL = *pkg.RegistryUrl
if registryURL, err := pkg.RegistryURL.Get(); err == nil {
out.RegistryURL = registryURL
} else {
out.RegistryURL = registries.DefaultURL(pkg.Ecosystem)
}
if pkg.LatestReleaseNumber != nil {
out.LatestVersion = *pkg.LatestReleaseNumber
}
out.LatestVersion = nullableValue(pkg.LatestReleaseNumber)
return out
}

func extractRepoHTMLURL(repoMetadata *map[string]interface{}) string {
if repoMetadata == nil {
return ""
}
if htmlURL, ok := (*repoMetadata)["html_url"].(string); ok {
func extractRepoHTMLURL(repoMetadata map[string]interface{}) string {
if htmlURL, ok := repoMetadata["html_url"].(string); ok {
return htmlURL
}
return ""
Expand All @@ -283,23 +264,13 @@ func convertMaintainers(maintainers []packages.Maintainer) []Maintainer {
}
result := make([]Maintainer, 0, len(maintainers))
for _, m := range maintainers {
out := Maintainer{}
if m.Login != nil {
out.Login = *m.Login
}
if m.Name != nil {
out.Name = *m.Name
}
if m.Email != nil {
out.Email = *m.Email
}
if m.HtmlUrl != nil {
out.URL = *m.HtmlUrl
}
if m.Role != nil {
out.Role = *m.Role
}
result = append(result, out)
result = append(result, Maintainer{
Login: nullableValue(m.Login),
Name: nullableValue(m.Name),
Email: nullableValue(m.Email),
URL: nullableValue(m.HTMLURL),
Role: nullableValue(m.Role),
})
}
return result
}
Expand All @@ -311,19 +282,15 @@ func convertAdvisories(advisories []packages.Advisory) []Advisory {
result := make([]Advisory, 0, len(advisories))
for _, adv := range advisories {
a := Advisory{
Identifiers: adv.Identifiers,
}
if adv.Title != nil {
a.Title = *adv.Title
Title: nullableValue(adv.Title),
Severity: nullableValue(adv.Severity),
CVSSScore: nullableValue(adv.CVSSScore),
URL: nullableValue(adv.URL),
}
if adv.Severity != nil {
a.Severity = *adv.Severity
}
if adv.CvssScore != nil {
a.CVSSScore = *adv.CvssScore
}
if adv.Url != nil {
a.URL = *adv.Url
for _, id := range adv.Identifiers {
if v := nullableValue(id); v != "" {
a.Identifiers = append(a.Identifiers, v)
}
}
result = append(result, a)
}
Expand Down
37 changes: 19 additions & 18 deletions enrichment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/ecosyste-ms/ecosystems-go"
"github.com/ecosyste-ms/ecosystems-go/packages"
"github.com/oapi-codegen/nullable"
)

const testVersionLodash = "4.17.21"
Expand Down Expand Up @@ -46,7 +47,7 @@ func TestExtractChangelogFilename(t *testing.T) {
},
},
}
got := extractChangelogFilename(&meta)
got := extractChangelogFilename(meta)
if got != "CHANGELOG.md" {
t.Errorf("got %q, want %q", got, "CHANGELOG.md")
}
Expand All @@ -61,7 +62,7 @@ func TestExtractChangelogFilename(t *testing.T) {

t.Run("missing metadata key", func(t *testing.T) {
meta := map[string]interface{}{"other": "value"}
got := extractChangelogFilename(&meta)
got := extractChangelogFilename(meta)
if got != "" {
t.Errorf("got %q, want empty", got)
}
Expand All @@ -71,15 +72,15 @@ func TestExtractChangelogFilename(t *testing.T) {
meta := map[string]interface{}{
"metadata": map[string]interface{}{"other": "value"},
}
got := extractChangelogFilename(&meta)
got := extractChangelogFilename(meta)
if got != "" {
t.Errorf("got %q, want empty", got)
}
})

t.Run("wrong type for metadata", func(t *testing.T) {
meta := map[string]interface{}{"metadata": "not a map"}
got := extractChangelogFilename(&meta)
got := extractChangelogFilename(meta)
if got != "" {
t.Errorf("got %q, want empty", got)
}
Expand All @@ -93,7 +94,7 @@ func TestExtractChangelogFilename(t *testing.T) {
},
},
}
got := extractChangelogFilename(&meta)
got := extractChangelogFilename(meta)
if got != "" {
t.Errorf("got %q, want empty", got)
}
Expand Down Expand Up @@ -132,16 +133,16 @@ func TestAdvisoryMapping(t *testing.T) {
}

func TestConvertMaintainers(t *testing.T) {
login := "alice"
name := "Alice Example"
email := "alice@example.com"
htmlURL := "https://www.npmjs.com/~alice"
role := "owner"
login := nullable.NewNullableWithValue("alice")
name := nullable.NewNullableWithValue("Alice Example")
email := nullable.NewNullableWithValue("alice@example.com")
htmlURL := nullable.NewNullableWithValue("https://www.npmjs.com/~alice")
role := nullable.NewNullableWithValue("owner")

t.Run("populated", func(t *testing.T) {
got := convertMaintainers([]packages.Maintainer{
{Login: &login, Name: &name, Email: &email, HtmlUrl: &htmlURL, Role: &role},
{Login: &login},
{Login: login, Name: name, Email: email, HTMLURL: htmlURL, Role: role},
{Login: login},
})
want := []Maintainer{
{Login: "alice", Name: "Alice Example", Email: "alice@example.com", URL: "https://www.npmjs.com/~alice", Role: "owner"},
Expand Down Expand Up @@ -471,14 +472,14 @@ func TestEcosystemsClientGetVersionIncludesMetadata(t *testing.T) {
}

func TestVersionInfoFromEcosystemsIncludesYankedStatus(t *testing.T) {
status := "yanked"
integrity := "sha256-example"
license := "MIT"
metadata := map[string]any{"reason": "bad release"}
status := nullable.NewNullableWithValue("yanked")
integrity := nullable.NewNullableWithValue("sha256-example")
license := nullable.NewNullableWithValue("MIT")
metadata := nullable.NewNullableWithValue(map[string]any{"reason": "bad release"})

got := versionInfoFromEcosystems("1.2.3", nil, &integrity, &license, &status, &metadata)
got := versionInfoFromEcosystems("1.2.3", nil, integrity, license, status, metadata)

if got.Number != "1.2.3" || got.Integrity != integrity || got.License != license {
if got.Number != "1.2.3" || got.Integrity != "sha256-example" || got.License != "MIT" {
t.Fatalf("version metadata = %+v", got)
}
if got.Status != "yanked" || !got.Yanked {
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ module github.com/git-pkgs/enrichment
go 1.25.6

require (
github.com/ecosyste-ms/ecosystems-go v0.3.0
github.com/ecosyste-ms/ecosystems-go v0.4.0
github.com/git-pkgs/purl v0.1.15
github.com/git-pkgs/registries v0.6.3
github.com/git-pkgs/spdx v0.1.4
github.com/git-pkgs/vers v0.3.0
github.com/git-pkgs/vulns v0.2.1
github.com/oapi-codegen/nullable v1.1.0
)

require (
Expand All @@ -17,7 +18,7 @@ require (
github.com/git-pkgs/pom v0.1.5 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/oapi-codegen/runtime v1.4.2 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/package-url/packageurl-go v0.1.6 // indirect
github.com/pandatix/go-cvss v0.6.2 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ecosyste-ms/ecosystems-go v0.3.0 h1:eVTNKQ3PyVNkSAnk1934958Vg4rR7ho5B5MWEQaMLi4=
github.com/ecosyste-ms/ecosystems-go v0.3.0/go.mod h1:bkjiI8WoTFB1Jw0M3h8yn3KXCD/+LdETsQ3m2BNLMHQ=
github.com/ecosyste-ms/ecosystems-go v0.4.0 h1:5A+zF+XWT8sYYYjlc2/tI1SmiDGzbHLyT9CapVc5dGA=
github.com/ecosyste-ms/ecosystems-go v0.4.0/go.mod h1:FVswCrp3DQkur1HjVqfDF/gYrDSEmiFflntcB1G0DbA=
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
Expand All @@ -28,8 +28,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs=
github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE=
github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU=
github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/package-url/packageurl-go v0.1.6 h1:YO3p6u1XmCUliivUg/qWphaY8vI6hxSnnPv7Bfg3m5M=
github.com/package-url/packageurl-go v0.1.6/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=
github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=
Expand Down