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  }