github.com/wolfi-dev/wolfictl@v0.16.11/pkg/configs/advisory/v2/detection.go (about)

     1  package v2
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"slices"
     7  	"strings"
     8  
     9  	"github.com/wolfi-dev/wolfictl/pkg/internal/errorhelpers"
    10  	"github.com/wolfi-dev/wolfictl/pkg/vuln"
    11  	"gopkg.in/yaml.v3"
    12  )
    13  
    14  const (
    15  	DetectionTypeManual = "manual"
    16  	DetectionTypeNVDAPI = "nvdapi"
    17  	DetectionTypeScanV1 = "scan/v1"
    18  )
    19  
    20  var (
    21  	// DetectionTypes is a list of all valid detection types.
    22  	DetectionTypes = []string{
    23  		DetectionTypeManual,
    24  		DetectionTypeNVDAPI,
    25  		DetectionTypeScanV1,
    26  	}
    27  )
    28  
    29  // Detection is an event that indicates that a potential vulnerability was
    30  // detected for a distro package.
    31  type Detection struct {
    32  	// Type is the type of detection used to identify the vulnerability match.
    33  	Type string `yaml:"type"`
    34  
    35  	// Data is the data associated with the detection type.
    36  	Data interface{} `yaml:"data,omitempty"`
    37  }
    38  
    39  // Validate returns an error if the Detection data is invalid.
    40  func (d Detection) Validate() error {
    41  	return errors.Join(
    42  		d.validateType(),
    43  		d.validateData(),
    44  	)
    45  }
    46  
    47  func (d Detection) validateType() error {
    48  	if !slices.Contains(DetectionTypes, d.Type) {
    49  		return fmt.Errorf("type is %q but must be one of [%v]", d.Type, strings.Join(DetectionTypes, ", "))
    50  	}
    51  
    52  	return nil
    53  }
    54  
    55  func (d Detection) validateData() error {
    56  	switch d.Type {
    57  	case DetectionTypeManual:
    58  		if d.Data != nil {
    59  			return fmt.Errorf("data must be nil for detection type %q", d.Type)
    60  		}
    61  
    62  	case DetectionTypeNVDAPI:
    63  		return validateTypedDetectionData[DetectionNVDAPI](d.Data)
    64  
    65  	case DetectionTypeScanV1:
    66  		return validateTypedDetectionData[DetectionScanV1](d.Data)
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  func validateTypedDetectionData[T interface{ Validate() error }](data interface{}) error {
    73  	d, ok := data.(T)
    74  	if !ok {
    75  		return fmt.Errorf("data must be of type %T", new(T))
    76  	}
    77  
    78  	return d.Validate()
    79  }
    80  
    81  // UnmarshalYAML implements the yaml.Unmarshaler interface.
    82  func (d *Detection) UnmarshalYAML(v *yaml.Node) error {
    83  	type partialDetection struct {
    84  		Type string    `yaml:"type"`
    85  		Data yaml.Node `yaml:"data"`
    86  	}
    87  
    88  	// Unmarshal the detection type and timestamp as a "partial detection" before
    89  	// unmarshalling the detection-type-specific data.
    90  	partial, err := strictUnmarshal[partialDetection](v)
    91  	if err != nil {
    92  		return fmt.Errorf("strict YAML unmarshaling failed: %w", err)
    93  	}
    94  
    95  	// Unmarshal the detection-type-specific data.
    96  	switch partial.Type {
    97  	case DetectionTypeManual:
    98  		// No data associated with this type.
    99  
   100  	case DetectionTypeNVDAPI:
   101  		var data DetectionNVDAPI
   102  		if err := partial.Data.Decode(&data); err != nil {
   103  			return err
   104  		}
   105  		d.Data = data
   106  
   107  	case DetectionTypeScanV1:
   108  		var data DetectionScanV1
   109  		if err := partial.Data.Decode(&data); err != nil {
   110  			return err
   111  		}
   112  		d.Data = data
   113  
   114  	default:
   115  		// TODO: log at warn level: unrecognized type
   116  	}
   117  
   118  	// Copy the data from the partial detection.
   119  	d.Type = partial.Type
   120  
   121  	return nil
   122  }
   123  
   124  // DetectionNVDAPI is the data associated with DetectionTypeNVDAPI.
   125  type DetectionNVDAPI struct {
   126  	CPESearched string `yaml:"cpeSearched"`
   127  	CPEFound    string `yaml:"cpeFound"`
   128  }
   129  
   130  // Validate returns an error if the DetectionNVDAPI data is invalid.
   131  func (d DetectionNVDAPI) Validate() error {
   132  	return errorhelpers.LabelError("nvdapi detection data",
   133  		errors.Join(
   134  			errorhelpers.LabelError("cpeSearched", vuln.ValidateCPE(d.CPESearched)),
   135  			errorhelpers.LabelError("cpeFound", vuln.ValidateCPE(d.CPEFound)),
   136  		),
   137  	)
   138  }
   139  
   140  var (
   141  	DetectionScannerGrype = "grype"
   142  )
   143  
   144  var DetectionScanners = []string{
   145  	DetectionScannerGrype,
   146  }
   147  
   148  // DetectionScanV1 is the data associated with DetectionTypeScanV1.
   149  type DetectionScanV1 struct {
   150  	SubpackageName    string `yaml:"subpackageName"`
   151  	ComponentID       string `yaml:"componentID"` // TODO: consider namespacing this ID using the SBOM tool+format
   152  	ComponentName     string `yaml:"componentName"`
   153  	ComponentVersion  string `yaml:"componentVersion"`
   154  	ComponentType     string `yaml:"componentType"`
   155  	ComponentLocation string `yaml:"componentLocation"`
   156  	Scanner           string `yaml:"scanner"` // TODO: it'd be nice for the scanner value to be automatically versioned
   157  }
   158  
   159  // Validate returns an error if the DetectionScanV1 data is invalid.
   160  func (d DetectionScanV1) Validate() error {
   161  	return errorhelpers.LabelError("scan/v1 detection data",
   162  		errors.Join(
   163  			errorhelpers.LabelError("subpackageName", validateNotEmpty(d.SubpackageName)),
   164  			errorhelpers.LabelError("componentID", validateNotEmpty(d.ComponentID)),
   165  			errorhelpers.LabelError("componentName", validateNotEmpty(d.ComponentName)),
   166  			errorhelpers.LabelError("componentVersion", validateNotEmpty(d.ComponentVersion)),
   167  			errorhelpers.LabelError("componentType", validateNotEmpty(d.ComponentType)),
   168  			errorhelpers.LabelError("componentLocation", validateNotEmpty(d.ComponentLocation)),
   169  			errorhelpers.LabelError("scanner", validateDetectionScanner(d.Scanner)),
   170  		),
   171  	)
   172  }
   173  
   174  func validateDetectionScanner(scanner string) error {
   175  	if !slices.Contains(DetectionScanners, scanner) {
   176  		return fmt.Errorf("value is %q but must be one of [%v]", scanner, strings.Join(
   177  			DetectionScanners,
   178  			", ",
   179  		))
   180  	}
   181  
   182  	return nil
   183  }