github.com/google/osv-scalibr@v0.4.1/binary/cli/cli.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 cli defines the structures to store the CLI flags used by the scanner binary.
    16  package cli
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"os"
    22  	"regexp"
    23  	"slices"
    24  	"strings"
    25  
    26  	"github.com/gobwas/glob"
    27  	"github.com/google/go-containerregistry/pkg/authn"
    28  	v1 "github.com/google/go-containerregistry/pkg/v1"
    29  	"github.com/google/go-containerregistry/pkg/v1/remote"
    30  	scalibr "github.com/google/osv-scalibr"
    31  	scalibrimage "github.com/google/osv-scalibr/artifact/image"
    32  	"github.com/google/osv-scalibr/binary/cdx"
    33  	"github.com/google/osv-scalibr/binary/platform"
    34  	"github.com/google/osv-scalibr/binary/proto"
    35  	binspdx "github.com/google/osv-scalibr/binary/spdx"
    36  	"github.com/google/osv-scalibr/converter"
    37  	convspdx "github.com/google/osv-scalibr/converter/spdx"
    38  	"github.com/google/osv-scalibr/detector"
    39  	"github.com/google/osv-scalibr/extractor/filesystem"
    40  	scalibrfs "github.com/google/osv-scalibr/fs"
    41  	"github.com/google/osv-scalibr/log"
    42  	"github.com/google/osv-scalibr/plugin"
    43  	pl "github.com/google/osv-scalibr/plugin/list"
    44  	"github.com/spdx/tools-golang/spdx/v2/common"
    45  	"google.golang.org/protobuf/encoding/prototext"
    46  
    47  	cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
    48  )
    49  
    50  // Array is a type to be passed to flag.Var that supports arrays passed as repeated flags,
    51  // e.g. ./scalibr -o binproto=out.bp -o spdx23-json=out.spdx.json
    52  type Array []string
    53  
    54  func (i *Array) String() string {
    55  	return strings.Join(*i, ",")
    56  }
    57  
    58  // Set gets called whenever a new instance of a flag is read during CLI arg parsing.
    59  // For example, in the case of -o foo -o bar the library will call arr.Set("foo") then arr.Set("bar").
    60  func (i *Array) Set(value string) error {
    61  	*i = append(*i, strings.TrimSpace(value))
    62  	return nil
    63  }
    64  
    65  // Get returns the underlying []string value stored by this flag struct.
    66  func (i *Array) Get() any {
    67  	return i
    68  }
    69  
    70  // StringListFlag is a type to be passed to flag.Var that supports list flags passed as repeated
    71  // flags, e.g. ./scalibr -o a -o b,c the library will call arr.Set("a") then arr.Set("a,b").
    72  type StringListFlag struct {
    73  	set          bool
    74  	value        []string
    75  	defaultValue []string
    76  }
    77  
    78  // NewStringListFlag creates a new StringListFlag with the given default value.
    79  func NewStringListFlag(defaultValue []string) StringListFlag {
    80  	return StringListFlag{defaultValue: defaultValue}
    81  }
    82  
    83  // Set gets called whenever a new instance of a flag is read during CLI arg parsing.
    84  // For example, in the case of -o foo -o bar the library will call arr.Set("foo") then arr.Set("bar").
    85  func (s *StringListFlag) Set(x string) error {
    86  	s.value = append(s.value, strings.Split(x, ",")...)
    87  	s.set = true
    88  	return nil
    89  }
    90  
    91  // Get returns the underlying []string value stored by this flag struct.
    92  func (s *StringListFlag) Get() any {
    93  	return s.GetSlice()
    94  }
    95  
    96  // GetSlice returns the underlying []string value stored by this flag struct.
    97  func (s *StringListFlag) GetSlice() []string {
    98  	if s.set {
    99  		return s.value
   100  	}
   101  	return s.defaultValue
   102  }
   103  
   104  func (s *StringListFlag) String() string {
   105  	if len(s.value) == 0 {
   106  		return ""
   107  	}
   108  	return fmt.Sprint(s.value)
   109  }
   110  
   111  // Reset resets the flag to its default value.
   112  func (s *StringListFlag) Reset() {
   113  	s.set = false
   114  	s.value = nil
   115  }
   116  
   117  // Flags contains a field for all the cli flags that can be set.
   118  type Flags struct {
   119  	PrintVersion          bool
   120  	Root                  string
   121  	ResultFile            string
   122  	Output                Array
   123  	ExtractorsToRun       []string
   124  	DetectorsToRun        []string
   125  	AnnotatorsToRun       []string
   126  	PluginsToRun          []string
   127  	PluginCFG             []string
   128  	ExtractorOverride     Array
   129  	PathsToExtract        []string
   130  	IgnoreSubDirs         bool
   131  	DirsToSkip            []string
   132  	SkipDirRegex          string
   133  	SkipDirGlob           string
   134  	MaxFileSize           int
   135  	UseGitignore          bool
   136  	RemoteImage           string
   137  	ImageLocal            string
   138  	ImageTarball          string
   139  	ImagePlatform         string
   140  	GovulncheckDBPath     string
   141  	SPDXDocumentName      string
   142  	SPDXDocumentNamespace string
   143  	SPDXCreators          string
   144  	CDXComponentName      string
   145  	CDXComponentType      string
   146  	CDXComponentVersion   string
   147  	CDXAuthors            string
   148  	Verbose               bool
   149  	ExplicitPlugins       bool
   150  	FilterByCapabilities  bool
   151  	StoreAbsolutePath     bool
   152  	WindowsAllDrives      bool
   153  	Offline               bool
   154  }
   155  
   156  var supportedOutputFormats = []string{
   157  	"textproto", "binproto", "spdx23-tag-value", "spdx23-json", "spdx23-yaml", "cdx-json", "cdx-xml",
   158  }
   159  
   160  var supportedComponentTypes = []string{
   161  	"application", "framework", "library", "container", "platform",
   162  	"operating-system", "device", "device-driver", "firmware", "file",
   163  	"machine-learning-model", "data", "cryptographic-asset",
   164  }
   165  
   166  // ValidateFlags validates the passed command line flags.
   167  func ValidateFlags(flags *Flags) error {
   168  	if flags.PrintVersion {
   169  		// SCALIBR prints the version and exits so other flags don't need to be present.
   170  		return nil
   171  	}
   172  	if len(flags.ResultFile) == 0 && len(flags.Output) == 0 {
   173  		return errors.New("either --result or --o needs to be set")
   174  	}
   175  	if flags.Root != "" && flags.WindowsAllDrives {
   176  		return errors.New("--root and --windows-all-drives cannot be used together")
   177  	}
   178  	if flags.ImagePlatform != "" && len(flags.RemoteImage) == 0 {
   179  		return errors.New("--image-platform cannot be used without --remote-image")
   180  	}
   181  	if flags.ImageTarball != "" && flags.RemoteImage != "" {
   182  		return errors.New("--image-tarball cannot be used with --remote-image")
   183  	}
   184  	if flags.ImageTarball != "" && flags.ImagePlatform != "" {
   185  		return errors.New("--image-tarball cannot be used with --image-platform")
   186  	}
   187  	if flags.ImageLocal != "" && flags.RemoteImage != "" {
   188  		return errors.New("image-local-docker cannot be used with --remote-image")
   189  	}
   190  	if flags.ImageLocal != "" && flags.ImagePlatform != "" {
   191  		return errors.New("image-local-docker cannot be used with --image-platform")
   192  	}
   193  	if flags.ImageLocal != "" && flags.ImageTarball != "" {
   194  		return errors.New("image-local-docker cannot be used with --image-tarball")
   195  	}
   196  	if err := validateResultPath(flags.ResultFile); err != nil {
   197  		return fmt.Errorf("--result %w", err)
   198  	}
   199  	if err := validateOutput(flags.Output); err != nil {
   200  		return fmt.Errorf("--o %w", err)
   201  	}
   202  	if err := validateExtractorOverride(flags.ExtractorOverride); err != nil {
   203  		return fmt.Errorf("--extractor-override: %w", err)
   204  	}
   205  	if err := validateImagePlatform(flags.ImagePlatform); err != nil {
   206  		return fmt.Errorf("--image-platform %w", err)
   207  	}
   208  	if err := validateMultiStringArg(flags.PluginsToRun); err != nil {
   209  		return fmt.Errorf("--plugins: %w", err)
   210  	}
   211  	// Legacy args for setting plugins.
   212  	if err := validateMultiStringArg(flags.ExtractorsToRun); err != nil {
   213  		return fmt.Errorf("--extractors: %w", err)
   214  	}
   215  	if err := validateMultiStringArg(flags.DetectorsToRun); err != nil {
   216  		return fmt.Errorf("--detectors: %w", err)
   217  	}
   218  	if err := validateMultiStringArg(flags.AnnotatorsToRun); err != nil {
   219  		return fmt.Errorf("--annotators: %w", err)
   220  	}
   221  
   222  	if err := validateMultiStringArg(flags.DirsToSkip); err != nil {
   223  		return fmt.Errorf("--skip-dirs: %w", err)
   224  	}
   225  	if err := validateRegex(flags.SkipDirRegex); err != nil {
   226  		return fmt.Errorf("--skip-dir-regex: %w", err)
   227  	}
   228  	if err := validateGlob(flags.SkipDirGlob); err != nil {
   229  		return fmt.Errorf("--skip-dir-glob: %w", err)
   230  	}
   231  	pluginsToRun := slices.Concat(flags.PluginsToRun, flags.ExtractorsToRun, flags.DetectorsToRun, flags.AnnotatorsToRun)
   232  	if err := validateDependency(pluginsToRun, flags.ExplicitPlugins); err != nil {
   233  		return err
   234  	}
   235  	if err := validateComponentType(flags.CDXComponentType); err != nil {
   236  		return err
   237  	}
   238  	return nil
   239  }
   240  
   241  func validateExtractorOverride(extractorOverride []string) error {
   242  	for _, item := range extractorOverride {
   243  		parts := strings.SplitN(item, ":", 2)
   244  		if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
   245  			return fmt.Errorf("invalid format for extractor override %q, should be <plugin-name>:<glob-pattern>", item)
   246  		}
   247  		if _, err := glob.Compile(parts[1]); err != nil {
   248  			return fmt.Errorf("invalid glob pattern %q in extractor override %q: %w", parts[1], item, err)
   249  		}
   250  	}
   251  	return nil
   252  }
   253  
   254  func validateResultPath(filePath string) error {
   255  	if len(filePath) == 0 {
   256  		return nil
   257  	}
   258  	if err := proto.ValidExtension(filePath); err != nil {
   259  		return err
   260  	}
   261  	return nil
   262  }
   263  
   264  func validateOutput(output []string) error {
   265  	for _, item := range output {
   266  		o := strings.Split(item, "=")
   267  		if len(o) != 2 {
   268  			return errors.New("invalid output format, should follow a format like -o textproto=result.textproto -o spdx23-json=result.spdx.json")
   269  		}
   270  		oFormat := o[0]
   271  		if !slices.Contains(supportedOutputFormats, oFormat) {
   272  			return fmt.Errorf("output format %q not recognized, supported formats are %v", oFormat, supportedOutputFormats)
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  func validateImagePlatform(imagePlatform string) error {
   279  	if len(imagePlatform) == 0 {
   280  		return nil
   281  	}
   282  	platformDetails := strings.Split(imagePlatform, "/")
   283  	if len(platformDetails) < 2 {
   284  		return fmt.Errorf("image platform '%s' is invalid. Must be in the form OS/Architecture (e.g. linux/amd64)", imagePlatform)
   285  	}
   286  	return nil
   287  }
   288  
   289  func validateMultiStringArg(arg []string) error {
   290  	if len(arg) == 0 {
   291  		return nil
   292  	}
   293  	for _, item := range arg {
   294  		if len(item) == 0 {
   295  			continue
   296  		}
   297  		for item := range strings.SplitSeq(item, ",") {
   298  			if len(item) == 0 {
   299  				return errors.New("list item cannot be left empty")
   300  			}
   301  		}
   302  	}
   303  	return nil
   304  }
   305  
   306  func validateRegex(arg string) error {
   307  	if len(arg) == 0 {
   308  		return nil
   309  	}
   310  	_, err := regexp.Compile(arg)
   311  	return err
   312  }
   313  
   314  func validateGlob(arg string) error {
   315  	_, err := glob.Compile(arg)
   316  	return err
   317  }
   318  
   319  func validateDependency(pluginNames []string, requireExtractors bool) error {
   320  	f := &Flags{PluginsToRun: pluginNames}
   321  	plugins, _, err := f.pluginsToRun()
   322  	if err != nil {
   323  		return err
   324  	}
   325  	pMap := make(map[string]bool)
   326  	for _, p := range plugins {
   327  		pMap[p.Name()] = true
   328  	}
   329  	if requireExtractors {
   330  		for _, p := range plugins {
   331  			if d, ok := p.(detector.Detector); ok {
   332  				for _, req := range d.RequiredExtractors() {
   333  					if !pMap[req] {
   334  						return fmt.Errorf("extractor %s must be turned on for Detector %s to run", req, d.Name())
   335  					}
   336  				}
   337  			}
   338  		}
   339  	}
   340  	return nil
   341  }
   342  
   343  func validateComponentType(componentType string) error {
   344  	if len(componentType) > 0 && !slices.Contains(supportedComponentTypes, componentType) {
   345  		return fmt.Errorf("unsupported cdx-component-type '%s'", componentType)
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  type extractorOverride struct {
   352  	glob      glob.Glob
   353  	extractor filesystem.Extractor
   354  }
   355  
   356  // GetScanConfig constructs a SCALIBR scan config from the provided CLI flags.
   357  func (f *Flags) GetScanConfig() (*scalibr.ScanConfig, error) {
   358  	plugins, pluginCFG, err := f.pluginsToRun()
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	capab := f.capabilities()
   363  	if f.FilterByCapabilities {
   364  		plugins = filterByCapabilities(plugins, capab)
   365  	}
   366  	if len(f.PluginsToRun)+len(f.ExtractorsToRun)+len(f.DetectorsToRun)+len(f.AnnotatorsToRun) == 0 {
   367  		names := make([]string, 0, len(plugins))
   368  		for _, p := range plugins {
   369  			names = append(names, p.Name())
   370  		}
   371  		log.Warnf("No plugins specified, using default list: %v", names)
   372  	}
   373  
   374  	var skipDirRegex *regexp.Regexp
   375  	if f.SkipDirRegex != "" {
   376  		skipDirRegex, err = regexp.Compile(f.SkipDirRegex)
   377  		if err != nil {
   378  			return nil, err
   379  		}
   380  	}
   381  	var skipDirGlob glob.Glob
   382  	if f.SkipDirGlob != "" {
   383  		skipDirGlob, err = glob.Compile(f.SkipDirGlob)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  	}
   388  
   389  	scanRoots, err := f.scanRoots()
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	var overrides []extractorOverride
   395  	if len(f.ExtractorOverride) > 0 {
   396  		pluginMap := make(map[string]filesystem.Extractor)
   397  		for _, p := range plugins {
   398  			if e, ok := p.(filesystem.Extractor); ok {
   399  				pluginMap[e.Name()] = e
   400  			}
   401  		}
   402  
   403  		for _, o := range f.ExtractorOverride {
   404  			parts := strings.SplitN(o, ":", 2)
   405  			pluginName := parts[0]
   406  			globPattern := parts[1]
   407  			extractor, ok := pluginMap[pluginName]
   408  			if !ok {
   409  				return nil, fmt.Errorf("plugin %q specified in --extractor-override not found or not a filesystem extractor", pluginName)
   410  			}
   411  			g, err := glob.Compile(globPattern)
   412  			if err != nil {
   413  				// This should not happen due to ValidateFlags.
   414  				return nil, fmt.Errorf("invalid glob pattern %q in extractor override: %w", globPattern, err)
   415  			}
   416  			overrides = append(overrides, extractorOverride{
   417  				glob:      g,
   418  				extractor: extractor,
   419  			})
   420  		}
   421  	}
   422  
   423  	var extractorOverrideFn func(filesystem.FileAPI) []filesystem.Extractor
   424  	if len(overrides) > 0 {
   425  		extractorOverrideFn = func(api filesystem.FileAPI) []filesystem.Extractor {
   426  			var result []filesystem.Extractor
   427  			for _, o := range overrides {
   428  				if o.glob.Match(api.Path()) {
   429  					result = append(result, o.extractor)
   430  				}
   431  			}
   432  			return result
   433  		}
   434  	}
   435  
   436  	return &scalibr.ScanConfig{
   437  		ScanRoots:            scanRoots,
   438  		Plugins:              plugins,
   439  		Capabilities:         capab,
   440  		PathsToExtract:       f.PathsToExtract,
   441  		IgnoreSubDirs:        f.IgnoreSubDirs,
   442  		DirsToSkip:           f.dirsToSkip(scanRoots),
   443  		SkipDirRegex:         skipDirRegex,
   444  		SkipDirGlob:          skipDirGlob,
   445  		MaxFileSize:          f.MaxFileSize,
   446  		UseGitignore:         f.UseGitignore,
   447  		StoreAbsolutePath:    f.StoreAbsolutePath,
   448  		ExtractorOverride:    extractorOverrideFn,
   449  		ExplicitPlugins:      f.ExplicitPlugins,
   450  		RequiredPluginConfig: pluginCFG,
   451  	}, nil
   452  }
   453  
   454  // GetSPDXConfig creates an SPDXConfig struct based on the CLI flags.
   455  func (f *Flags) GetSPDXConfig() convspdx.Config {
   456  	var creators []common.Creator
   457  	if len(f.SPDXCreators) > 0 {
   458  		for item := range strings.SplitSeq(f.SPDXCreators, ",") {
   459  			c := strings.Split(item, ":")
   460  			cType := c[0]
   461  			cName := c[1]
   462  			creators = append(creators, common.Creator{
   463  				CreatorType: cType,
   464  				Creator:     cName,
   465  			})
   466  		}
   467  	}
   468  	return convspdx.Config{
   469  		DocumentName:      f.SPDXDocumentName,
   470  		DocumentNamespace: f.SPDXDocumentNamespace,
   471  		Creators:          creators,
   472  	}
   473  }
   474  
   475  // GetCDXConfig creates a CDXConfig struct based on the CLI flags.
   476  func (f *Flags) GetCDXConfig() converter.CDXConfig {
   477  	return converter.CDXConfig{
   478  		ComponentName:    f.CDXComponentName,
   479  		ComponentType:    f.CDXComponentType,
   480  		ComponentVersion: f.CDXComponentVersion,
   481  		Authors:          strings.Split(f.CDXAuthors, ","),
   482  	}
   483  }
   484  
   485  // WriteScanResults writes SCALIBR scan results to files specified by the CLI flags.
   486  func (f *Flags) WriteScanResults(result *scalibr.ScanResult) error {
   487  	if len(f.ResultFile) > 0 {
   488  		log.Infof("Writing scan results to %s", f.ResultFile)
   489  		resultProto, err := proto.ScanResultToProto(result)
   490  		if err != nil {
   491  			return err
   492  		}
   493  		if err := proto.Write(f.ResultFile, resultProto); err != nil {
   494  			return err
   495  		}
   496  	}
   497  	if len(f.Output) > 0 {
   498  		for _, item := range f.Output {
   499  			o := strings.Split(item, "=")
   500  			oFormat := o[0]
   501  			oPath := o[1]
   502  			log.Infof("Writing scan results to %s", oPath)
   503  			if strings.Contains(oFormat, "proto") {
   504  				resultProto, err := proto.ScanResultToProto(result)
   505  				if err != nil {
   506  					return err
   507  				}
   508  				if err := proto.WriteWithFormat(oPath, resultProto, oFormat); err != nil {
   509  					return err
   510  				}
   511  			} else if strings.Contains(oFormat, "spdx23") {
   512  				doc := converter.ToSPDX23(result, f.GetSPDXConfig())
   513  				if err := binspdx.Write23(doc, oPath, oFormat); err != nil {
   514  					return err
   515  				}
   516  			} else if strings.Contains(oFormat, "cdx") {
   517  				doc := converter.ToCDX(result, f.GetCDXConfig())
   518  				if err := cdx.Write(doc, oPath, oFormat); err != nil {
   519  					return err
   520  				}
   521  			}
   522  		}
   523  	}
   524  	return nil
   525  }
   526  
   527  // TODO(b/279413691): Allow commas in argument names.
   528  func (f *Flags) pluginsToRun() ([]plugin.Plugin, *cpb.PluginConfig, error) {
   529  	result := make([]plugin.Plugin, 0, len(f.PluginsToRun))
   530  	pluginNames := multiStringToList(f.PluginsToRun)
   531  	pluginCFG, err := pluginCFGFromFlags(f.PluginCFG)
   532  	if err != nil {
   533  		return nil, nil, err
   534  	}
   535  
   536  	extractorNames := addPluginPrefixToGroups("extractors/", multiStringToList(f.ExtractorsToRun))
   537  	detectorNames := addPluginPrefixToGroups("detectors/", multiStringToList(f.DetectorsToRun))
   538  	annotatorNames := addPluginPrefixToGroups("annotators/", multiStringToList(f.AnnotatorsToRun))
   539  
   540  	// Use the default plugins if nothing is specified.
   541  	allPluginNames := slices.Concat(pluginNames, extractorNames, detectorNames, annotatorNames)
   542  	if len(allPluginNames) == 0 {
   543  		allPluginNames = []string{"default"}
   544  	}
   545  
   546  	for _, name := range allPluginNames {
   547  		plugins, err := pl.FromNames([]string{name}, pluginCFG)
   548  		if err != nil {
   549  			return nil, nil, err
   550  		}
   551  		result = append(result, plugins...)
   552  	}
   553  
   554  	return result, pluginCFG, nil
   555  }
   556  
   557  // addPluginPrefixToGroups adds the specified prefix to the "default" and "all"
   558  // plugin group names so that they're only applied for a specific plugin type
   559  // so that e.g. --extractors=all only enables all extractors and not other plugins.
   560  func addPluginPrefixToGroups(prefix string, pluginNames []string) []string {
   561  	result := make([]string, 0, len(pluginNames))
   562  	for _, p := range pluginNames {
   563  		if p == "all" || p == "default" {
   564  			p = prefix + p
   565  		}
   566  		result = append(result, p)
   567  	}
   568  	return result
   569  }
   570  
   571  // pluginCFGFromFlags parses individually provided
   572  // plugin config strings into one proto.
   573  func pluginCFGFromFlags(flags []string) (*cpb.PluginConfig, error) {
   574  	var cfgString strings.Builder
   575  	for _, flag := range flags {
   576  		pluginCFG := &cpb.PluginConfig{}
   577  		err := prototext.Unmarshal([]byte(flag), pluginCFG)
   578  		if err != nil {
   579  			// Flags can use a shorthand for specifying a single plugin-specific setting.
   580  			// In this case we have to add the wrapping proto parts manually.
   581  			flag = "plugin_specific:{" + flag + "}"
   582  			err := prototext.Unmarshal([]byte(flag), pluginCFG)
   583  			if err != nil {
   584  				return nil, err
   585  			}
   586  		}
   587  		cfgString.WriteString(flag + "\n")
   588  	}
   589  
   590  	allCFGs := &cpb.PluginConfig{}
   591  	err := prototext.Unmarshal([]byte(cfgString.String()), allCFGs)
   592  	return allCFGs, err
   593  }
   594  
   595  func multiStringToList(arg []string) []string {
   596  	var result []string
   597  	for _, item := range arg {
   598  		result = append(result, strings.Split(item, ",")...)
   599  	}
   600  	return result
   601  }
   602  
   603  func (f *Flags) scanRoots() ([]*scalibrfs.ScanRoot, error) {
   604  	if f.RemoteImage != "" {
   605  		imageOptions := f.scanRemoteImageOptions()
   606  		fs, err := scalibrimage.NewFromRemoteName(f.RemoteImage, *imageOptions...)
   607  		if err != nil {
   608  			return nil, err
   609  		}
   610  		// We're scanning a virtual filesystem that describes the remote container.
   611  		return []*scalibrfs.ScanRoot{{FS: fs, Path: ""}}, nil
   612  	}
   613  
   614  	if len(f.Root) != 0 {
   615  		return scalibrfs.RealFSScanRoots(f.Root), nil
   616  	}
   617  
   618  	// If ImageTarball is set, do not set the root.
   619  	// It is computed later on by ScanContainer(...) when the tarball is read.
   620  	if f.ImageTarball != "" {
   621  		return nil, nil
   622  	}
   623  	// If ImageLocal is set, do not set the root.
   624  	// It is computed later on by ScanContainer(...) when the tarball is read.
   625  	if f.ImageLocal != "" {
   626  		return nil, nil
   627  	}
   628  
   629  	// Compute the default scan roots.
   630  	var scanRoots []*scalibrfs.ScanRoot
   631  	var scanRootPaths []string
   632  	var err error
   633  	if scanRootPaths, err = platform.DefaultScanRoots(f.WindowsAllDrives); err != nil {
   634  		return nil, err
   635  	}
   636  	for _, r := range scanRootPaths {
   637  		scanRoots = append(scanRoots, &scalibrfs.ScanRoot{FS: scalibrfs.DirFS(r), Path: r})
   638  	}
   639  	return scanRoots, nil
   640  }
   641  
   642  func (f *Flags) scanRemoteImageOptions() *[]remote.Option {
   643  	imageOptions := []remote.Option{
   644  		remote.WithAuthFromKeychain(authn.DefaultKeychain),
   645  	}
   646  	if f.ImagePlatform != "" {
   647  		platformDetails := strings.Split(f.ImagePlatform, "/")
   648  		imageOptions = append(imageOptions, remote.WithPlatform(
   649  			v1.Platform{
   650  				OS:           platformDetails[0],
   651  				Architecture: platformDetails[1],
   652  			},
   653  		))
   654  	}
   655  	return &imageOptions
   656  }
   657  
   658  // All capabilities are enabled when running SCALIBR as a binary.
   659  func (f *Flags) capabilities() *plugin.Capabilities {
   660  	network := plugin.NetworkOnline
   661  	if f.Offline {
   662  		network = plugin.NetworkOffline
   663  	}
   664  	if f.RemoteImage != "" {
   665  		// We're scanning a Linux container image whose filesystem is mounted to the host's disk.
   666  		return &plugin.Capabilities{
   667  			OS:            plugin.OSLinux,
   668  			Network:       network,
   669  			DirectFS:      true,
   670  			RunningSystem: false,
   671  		}
   672  	}
   673  	return &plugin.Capabilities{
   674  		OS:            platform.OS(),
   675  		Network:       network,
   676  		DirectFS:      true,
   677  		RunningSystem: true,
   678  	}
   679  }
   680  
   681  // Filters the specified list of plugins (filesystem extractors, standalone extractors, detectors, enrichers)
   682  // by removing all plugins that don't satisfy the specified capabilities.
   683  func filterByCapabilities(plugins []plugin.Plugin, capab *plugin.Capabilities) []plugin.Plugin {
   684  	fp := make([]plugin.Plugin, 0, len(plugins))
   685  	for _, p := range plugins {
   686  		if err := plugin.ValidateRequirements(p, capab); err == nil {
   687  			fp = append(fp, p)
   688  		}
   689  	}
   690  	return fp
   691  }
   692  
   693  func (f *Flags) dirsToSkip(scanRoots []*scalibrfs.ScanRoot) []string {
   694  	paths, err := platform.DefaultIgnoredDirectories()
   695  	if err != nil {
   696  		log.Warnf("Failed to get default ignored directories: %v", err)
   697  	}
   698  	if len(f.DirsToSkip) > 0 {
   699  		paths = append(paths, multiStringToList(f.DirsToSkip)...)
   700  	}
   701  
   702  	// Ignore paths that are not under Root.
   703  	result := make([]string, 0, len(paths))
   704  	for _, root := range scanRoots {
   705  		path := root.Path
   706  		if !strings.HasSuffix(path, string(os.PathSeparator)) {
   707  			path += string(os.PathSeparator)
   708  		}
   709  		for _, p := range paths {
   710  			if strings.HasPrefix(p, path) {
   711  				result = append(result, p)
   712  			}
   713  		}
   714  	}
   715  	return result
   716  }