github.com/google/osv-scalibr@v0.4.1/enricher/license/license.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package license contains an Enricher that adds license data 16 // to software packages by querying deps.dev 17 package license 18 19 import ( 20 "context" 21 "fmt" 22 23 "github.com/google/osv-scalibr/clients/datasource" 24 "github.com/google/osv-scalibr/depsdev" 25 "github.com/google/osv-scalibr/enricher" 26 "github.com/google/osv-scalibr/inventory" 27 "github.com/google/osv-scalibr/plugin" 28 scalibrversion "github.com/google/osv-scalibr/version" 29 "golang.org/x/sync/errgroup" 30 "google.golang.org/grpc/codes" 31 "google.golang.org/grpc/status" 32 33 depsdevpb "deps.dev/api/v3" 34 ) 35 36 const ( 37 // Name is the unique name of this Enricher. 38 Name = "license/depsdev" 39 version = 1 40 ) 41 42 const maxConcurrentRequests = 1000 43 44 var _ enricher.Enricher = &Enricher{} 45 46 // Enricher adds license data to software packages by querying deps.dev 47 type Enricher struct { 48 client Client 49 } 50 51 // NewWithClient returns an Enricher which uses a specified deps.dev client. 52 func NewWithClient(c Client) enricher.Enricher { 53 return &Enricher{client: c} 54 } 55 56 // New creates a new Enricher 57 func New() enricher.Enricher { 58 return &Enricher{} 59 } 60 61 // Name of the Enricher. 62 func (Enricher) Name() string { 63 return Name 64 } 65 66 // Version of the Enricher. 67 func (Enricher) Version() int { 68 return version 69 } 70 71 // Requirements of the Enricher. 72 // Needs network access so it can validate Secrets. 73 func (Enricher) Requirements() *plugin.Capabilities { 74 return &plugin.Capabilities{ 75 Network: plugin.NetworkOnline, 76 } 77 } 78 79 // RequiredPlugins returns the plugins that are required to be enabled for this 80 // Enricher to run. While it works on the results of other extractors, 81 // the Enricher itself can run independently. 82 func (Enricher) RequiredPlugins() []string { 83 return []string{} 84 } 85 86 // Enrich adds license data to all the packages using deps.dev 87 func (e *Enricher) Enrich(ctx context.Context, _ *enricher.ScanInput, inv *inventory.Inventory) error { 88 if e.client == nil { 89 depsDevAPIClient, err := datasource.NewCachedInsightsClient(depsdev.DepsdevAPI, "osv-scalibr/"+scalibrversion.ScannerVersion) 90 if err != nil { 91 return fmt.Errorf("cannot connect with deps.dev %w", err) 92 } 93 e.client = depsDevAPIClient 94 } 95 96 queries := make([]*depsdevpb.GetVersionRequest, len(inv.Packages)) 97 for i, pkg := range inv.Packages { 98 if err := ctx.Err(); err != nil { 99 return err 100 } 101 102 ecoSystem, ok := depsdev.System[pkg.PURLType] 103 if !ok { 104 continue 105 } 106 queries[i] = versionQuery(ecoSystem, pkg.Name, pkg.Version) 107 } 108 109 licenses, err := e.makeVersionRequest(ctx, queries) 110 if err != nil { 111 return fmt.Errorf("failed to get version information %w", err) 112 } 113 114 for i, license := range licenses { 115 // use license information from deps.dev if available (preferred source of truth) 116 if len(license) > 0 { 117 inv.Packages[i].Licenses = license 118 continue 119 } 120 121 // if deps.dev has no info, but the package already has license data, retain it 122 if len(inv.Packages[i].Licenses) > 0 { 123 continue 124 } 125 126 // if no license info is available from any source, mark as "UNKNOWN" 127 inv.Packages[i].Licenses = []string{"UNKNOWN"} 128 } 129 130 return nil 131 } 132 133 // makeVersionRequest calls the deps.dev GetVersion gRPC API endpoint for each 134 // query. It makes these requests concurrently, sharing the single HTTP/2 135 // connection. The order in which the requests are specified should correspond 136 // to the order of licenses returned by this function. 137 func (e *Enricher) makeVersionRequest(ctx context.Context, queries []*depsdevpb.GetVersionRequest) ([][]string, error) { 138 licenses := make([][]string, len(queries)) 139 g, ctx := errgroup.WithContext(ctx) 140 g.SetLimit(maxConcurrentRequests) 141 142 for i := range queries { 143 // if the query is not set, skip the pkg 144 if queries[i] == nil { 145 continue 146 } 147 g.Go(func() error { 148 resp, err := e.client.GetVersion(ctx, queries[i]) 149 if err != nil { 150 if status.Code(err) == codes.NotFound { 151 return nil 152 } 153 return err 154 } 155 licenses[i] = resp.GetLicenses() 156 return nil 157 }) 158 } 159 if err := g.Wait(); err != nil { 160 return nil, err 161 } 162 163 return licenses, nil 164 } 165 166 func versionQuery(system depsdevpb.System, name string, version string) *depsdevpb.GetVersionRequest { 167 // Matching deps.dev naming convention. 168 if system == depsdevpb.System_GO { 169 if name == "stdlib" { 170 version = "go" + version 171 } else { 172 version = "v" + version 173 } 174 } 175 176 return &depsdevpb.GetVersionRequest{ 177 VersionKey: &depsdevpb.VersionKey{ 178 System: system, 179 Name: name, 180 Version: version, 181 }, 182 } 183 }