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 }