github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/rust/cargoauditable/cargoauditable.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 cargoauditable extracts dependencies from cargo auditable inside rust binaries.
    16  package cargoauditable
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  
    24  	"github.com/google/osv-scalibr/extractor"
    25  	"github.com/google/osv-scalibr/extractor/filesystem"
    26  	"github.com/google/osv-scalibr/inventory"
    27  	"github.com/google/osv-scalibr/log"
    28  	"github.com/google/osv-scalibr/plugin"
    29  	"github.com/google/osv-scalibr/purl"
    30  	"github.com/google/osv-scalibr/stats"
    31  	"github.com/rust-secure-code/go-rustaudit"
    32  )
    33  
    34  const (
    35  	// Name is the unique name of this extractor.
    36  	Name = "rust/cargoauditable"
    37  )
    38  
    39  // defaultMaxFileSizeBytes is the maximum file size an extractor will unmarshal.
    40  // If Extract gets a bigger file, it will return an error.
    41  const defaultMaxFileSizeBytes = 0
    42  
    43  // defaultExtractBuildDependencies is whether to extract build dependencies or only runtime ones.
    44  const defaultExtractBuildDependencies = false
    45  
    46  // Config is the configuration for the Extractor.
    47  type Config struct {
    48  	// Stats is a stats collector for reporting metrics.
    49  	Stats stats.Collector
    50  	// MaxFileSizeBytes is the maximum size of a file that can be extracted.
    51  	// If this limit is greater than zero and a file is encountered that is larger
    52  	// than this limit, the file is ignored by returning false for `FileRequired`.
    53  	MaxFileSizeBytes int64
    54  	// ExtractBuildDependencies is whether to extract build dependencies or only runtime ones.
    55  	ExtractBuildDependencies bool
    56  }
    57  
    58  // Extractor for extracting dependencies from cargo auditable inside rust binaries.
    59  type Extractor struct {
    60  	stats                    stats.Collector
    61  	maxFileSizeBytes         int64
    62  	extractBuildDependencies bool
    63  }
    64  
    65  // DefaultConfig returns a default configuration for the extractor.
    66  func DefaultConfig() Config {
    67  	return Config{
    68  		Stats:                    nil,
    69  		MaxFileSizeBytes:         defaultMaxFileSizeBytes,
    70  		ExtractBuildDependencies: defaultExtractBuildDependencies,
    71  	}
    72  }
    73  
    74  // New returns a Cargo Auditable extractor.
    75  //
    76  // For most use cases, initialize with:
    77  // ```
    78  // e := New(DefaultConfig())
    79  // ```
    80  func New(cfg Config) *Extractor {
    81  	return &Extractor{
    82  		stats:                    cfg.Stats,
    83  		maxFileSizeBytes:         cfg.MaxFileSizeBytes,
    84  		extractBuildDependencies: cfg.ExtractBuildDependencies,
    85  	}
    86  }
    87  
    88  // NewDefault returns an extractor with the default config settings.
    89  func NewDefault() filesystem.Extractor { return New(DefaultConfig()) }
    90  
    91  // Name of the extractor.
    92  func (e Extractor) Name() string { return Name }
    93  
    94  // Version of the extractor.
    95  func (e Extractor) Version() int { return 0 }
    96  
    97  // Requirements for enabling the extractor.
    98  func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
    99  
   100  // FileRequired returns true if the specified file is marked executable.
   101  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
   102  	path := api.Path()
   103  
   104  	fileinfo, err := api.Stat()
   105  	if err != nil {
   106  		return false
   107  	}
   108  
   109  	if !filesystem.IsInterestingExecutable(api) {
   110  		return false
   111  	}
   112  
   113  	sizeLimitExceeded := e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes
   114  	result := stats.FileRequiredResultOK
   115  	if sizeLimitExceeded {
   116  		result = stats.FileRequiredResultSizeLimitExceeded
   117  	}
   118  
   119  	if e.stats != nil {
   120  		e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{
   121  			Path:          path,
   122  			Result:        result,
   123  			FileSizeBytes: fileinfo.Size(),
   124  		})
   125  	}
   126  	return !sizeLimitExceeded
   127  }
   128  
   129  // Extract extracts packages from cargo auditable inside rust binaries.
   130  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   131  	reader, ok := input.Reader.(io.ReaderAt)
   132  	if !ok {
   133  		return inventory.Inventory{}, errors.New("input.Reader is not a ReaderAt")
   134  	}
   135  
   136  	dependencyInfo, err := rustaudit.GetDependencyInfo(reader)
   137  	e.reportFileExtracted(input, filesystem.ExtractorErrorToFileExtractedResult(err))
   138  	// Most errors are just that the file is not a cargo auditable rust binary.
   139  	if err != nil {
   140  		if errors.Is(err, rustaudit.ErrUnknownFileFormat) || errors.Is(err, rustaudit.ErrNoRustDepInfo) {
   141  			return inventory.Inventory{}, nil
   142  		}
   143  		log.Debugf("error getting dependency information from binary (%s) for extraction: %v", input.Path, err)
   144  		return inventory.Inventory{}, fmt.Errorf("rustaudit.GetDependencyInfo: %w", err)
   145  	}
   146  
   147  	pkgs := []*extractor.Package{}
   148  	for _, dep := range dependencyInfo.Packages {
   149  		// Cargo auditable also tracks build-only dependencies which we may not want to report.
   150  		// Note: the main package is reported as a runtime dependency.
   151  		if dep.Kind == rustaudit.Runtime || e.extractBuildDependencies {
   152  			pkgs = append(pkgs, &extractor.Package{
   153  				Name:      dep.Name,
   154  				Version:   dep.Version,
   155  				PURLType:  purl.TypeCargo,
   156  				Locations: []string{input.Path},
   157  			})
   158  		}
   159  	}
   160  	return inventory.Inventory{Packages: pkgs}, nil
   161  }
   162  
   163  func (e Extractor) reportFileExtracted(input *filesystem.ScanInput, result stats.FileExtractedResult) {
   164  	if e.stats == nil {
   165  		return
   166  	}
   167  	e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{
   168  		Path:          input.Path,
   169  		Result:        result,
   170  		FileSizeBytes: input.Info.Size(),
   171  	})
   172  }
   173  
   174  // Ensure Extractor implements the filesystem.Extractor interface.
   175  var _ filesystem.Extractor = Extractor{}