github.com/wolfi-dev/wolfictl@v0.16.11/pkg/configs/advisory/v2/event.go (about) 1 package v2 2 3 import ( 4 "errors" 5 "fmt" 6 "slices" 7 "strings" 8 "time" 9 10 "gopkg.in/yaml.v3" 11 ) 12 13 const ( 14 EventTypeDetection = "detection" 15 EventTypeTruePositiveDetermination = "true-positive-determination" 16 EventTypeFixed = "fixed" 17 EventTypeFalsePositiveDetermination = "false-positive-determination" 18 EventTypeAnalysisNotPlanned = "analysis-not-planned" 19 EventTypeFixNotPlanned = "fix-not-planned" 20 EventTypePendingUpstreamFix = "pending-upstream-fix" 21 ) 22 23 type EventTypeData interface { 24 Detection | 25 TruePositiveDetermination | 26 Fixed | 27 FalsePositiveDetermination | 28 AnalysisNotPlanned | 29 FixNotPlanned | 30 PendingUpstreamFix 31 } 32 33 var ( 34 // EventTypes is a list of all valid event types. 35 EventTypes = []string{ 36 EventTypeDetection, 37 EventTypeTruePositiveDetermination, 38 EventTypeFixed, 39 EventTypeFalsePositiveDetermination, 40 EventTypeAnalysisNotPlanned, 41 EventTypeFixNotPlanned, 42 EventTypePendingUpstreamFix, 43 } 44 ) 45 46 // Event is a timestamped record of new information regarding the investigation 47 // and resolution of a potential vulnerability match. 48 type Event struct { 49 // Timestamp is the time at which the event occurred. 50 Timestamp Timestamp `yaml:"timestamp"` 51 52 // Type is a string that identifies the kind of event. This field is used to 53 // determine how to unmarshal the Data field. 54 Type string `yaml:"type"` 55 56 // Data is the event-specific data. The type of this field is determined by the 57 // Type field. 58 Data interface{} `yaml:"data,omitempty"` 59 } 60 61 type partialEvent struct { 62 Timestamp Timestamp `yaml:"timestamp"` 63 Type string `yaml:"type"` 64 Data yaml.Node 65 } 66 67 func (e *Event) UnmarshalYAML(v *yaml.Node) error { 68 // Unmarshal the event type and timestamp as a "partial event" before unmarshalling the event-type-specific data. 69 pe, err := strictUnmarshal[partialEvent](v) 70 if err != nil { 71 return fmt.Errorf("strict YAML unmarshaling failed: %w", err) 72 } 73 eventData := *pe 74 75 var event Event 76 77 switch pe.Type { 78 case EventTypeDetection: 79 event, err = decodeTypedEventData[Detection](eventData) 80 81 case EventTypeTruePositiveDetermination: 82 event, err = decodeTypedEventData[TruePositiveDetermination](eventData) 83 84 case EventTypeFixed: 85 event, err = decodeTypedEventData[Fixed](eventData) 86 87 case EventTypeFalsePositiveDetermination: 88 event, err = decodeTypedEventData[FalsePositiveDetermination](eventData) 89 90 case EventTypeAnalysisNotPlanned: 91 event, err = decodeTypedEventData[AnalysisNotPlanned](eventData) 92 93 case EventTypeFixNotPlanned: 94 event, err = decodeTypedEventData[FixNotPlanned](eventData) 95 96 case EventTypePendingUpstreamFix: 97 event, err = decodeTypedEventData[PendingUpstreamFix](eventData) 98 99 default: 100 // TODO: log at warn level: unrecognized event type 101 102 event = Event{ 103 Timestamp: pe.Timestamp, 104 Type: pe.Type, 105 } 106 } 107 108 if err != nil { 109 return err 110 } 111 112 *e = event 113 return nil 114 } 115 116 func decodeTypedEventData[T EventTypeData](pe partialEvent) (Event, error) { 117 event := Event{ 118 Timestamp: pe.Timestamp, 119 Type: pe.Type, 120 } 121 122 if pe.Data.IsZero() { 123 return event, nil 124 } 125 126 data, err := strictUnmarshal[T](&pe.Data) 127 if err != nil { 128 return Event{}, fmt.Errorf("strict YAML unmarshaling failed: %w", err) 129 } 130 131 event.Data = *data 132 133 return event, nil 134 } 135 136 func (e Event) Validate() error { 137 return errors.Join( 138 e.validateTimestamp(), 139 e.validateType(), 140 e.validateData(), 141 ) 142 } 143 144 func (e Event) validateTimestamp() error { 145 if e.Timestamp.IsZero() { 146 return fmt.Errorf("timestamp must not be zero") 147 } 148 149 futureCutoff := time.Now().Add(2 * time.Hour) 150 if e.Timestamp.After(Timestamp(futureCutoff)) { 151 return fmt.Errorf("timestamp must not be in the future") 152 } 153 154 return nil 155 } 156 157 func (e Event) validateType() error { 158 if e.Type == "" { 159 return fmt.Errorf("type must not be empty") 160 } 161 162 if !slices.Contains(EventTypes, e.Type) { 163 return fmt.Errorf("type is %q but must be one of [%v]", e.Type, strings.Join(EventTypes, ", ")) 164 } 165 166 return nil 167 } 168 169 func (e Event) validateData() error { 170 switch e.Type { 171 case EventTypeDetection: 172 return validateTypedEventData[Detection](e.Data) 173 174 case EventTypeTruePositiveDetermination: 175 // no validation needed currently 176 177 case EventTypeFixed: 178 return validateTypedEventData[Fixed](e.Data) 179 180 case EventTypeFalsePositiveDetermination: 181 return validateTypedEventData[FalsePositiveDetermination](e.Data) 182 183 case EventTypeAnalysisNotPlanned: 184 return validateTypedEventData[AnalysisNotPlanned](e.Data) 185 186 case EventTypeFixNotPlanned: 187 return validateTypedEventData[FixNotPlanned](e.Data) 188 189 case EventTypePendingUpstreamFix: 190 return validateTypedEventData[PendingUpstreamFix](e.Data) 191 } 192 193 return nil 194 } 195 196 func (e Event) IsZero() bool { 197 return e.Timestamp.IsZero() && e.Type == "" && e.Data == nil 198 } 199 200 func validateTypedEventData[T interface{ Validate() error }](data interface{}) error { 201 d, ok := data.(T) 202 if !ok { 203 return fmt.Errorf("data must be of type %T", new(T)) 204 } 205 206 return d.Validate() 207 }