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  }