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

     1  package v2
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"slices"
     7  	"sort"
     8  
     9  	"github.com/samber/lo"
    10  	"github.com/wolfi-dev/wolfictl/pkg/internal/errorhelpers"
    11  	"github.com/wolfi-dev/wolfictl/pkg/versions"
    12  	"github.com/wolfi-dev/wolfictl/pkg/vuln"
    13  )
    14  
    15  type Advisory struct {
    16  	ID string `yaml:"id"`
    17  
    18  	// Aliases lists any known IDs of this vulnerability in databases.
    19  	Aliases []string `yaml:"aliases,omitempty"`
    20  
    21  	// Events is a list of timestamped events that occurred during the investigation
    22  	// and resolution of the vulnerability.
    23  	Events []Event `yaml:"events"`
    24  }
    25  
    26  // IsZero returns true if the advisory has no data.
    27  func (adv Advisory) IsZero() bool {
    28  	return adv.ID == "" && len(adv.Aliases) == 0 && len(adv.Events) == 0
    29  }
    30  
    31  // DescribesVulnerability returns true if the advisory cites the given
    32  // vulnerability ID in either its ID or its aliases.
    33  func (adv Advisory) DescribesVulnerability(vulnID string) bool {
    34  	return adv.ID == vulnID || slices.Contains(adv.Aliases, vulnID)
    35  }
    36  
    37  // Latest returns the latest event in the advisory.
    38  func (adv Advisory) Latest() Event {
    39  	if len(adv.Events) == 0 {
    40  		return Event{}
    41  	}
    42  
    43  	sorted := adv.SortedEvents()
    44  	return sorted[len(adv.Events)-1]
    45  }
    46  
    47  // SortedEvents returns the events in the advisory, sorted by timestamp, from
    48  // oldest to newest.
    49  func (adv Advisory) SortedEvents() []Event {
    50  	// avoid mutating the original slice
    51  	sorted := make([]Event, len(adv.Events))
    52  	copy(sorted, adv.Events)
    53  
    54  	sort.Slice(sorted, func(i, j int) bool {
    55  		return sorted[i].Timestamp.Before(sorted[j].Timestamp)
    56  	})
    57  
    58  	return sorted
    59  }
    60  
    61  // Resolved returns true if the advisory indicates that the vulnerability does
    62  // not presently affect the distro package and/or that no further investigation
    63  // is planned.
    64  func (adv Advisory) Resolved() bool {
    65  	if len(adv.Events) == 0 {
    66  		return false
    67  	}
    68  
    69  	switch adv.Latest().Type {
    70  	case EventTypeDetection, EventTypeTruePositiveDetermination:
    71  		return false
    72  
    73  	default:
    74  		return true
    75  	}
    76  }
    77  
    78  // ResolvedAtVersion returns true if the advisory indicates that the
    79  // vulnerability does not affect the distro package at the given package
    80  // version, or that no further investigation is planned.
    81  func (adv Advisory) ResolvedAtVersion(version, packageType string) bool {
    82  	if len(adv.Events) == 0 {
    83  		return false
    84  	}
    85  
    86  	switch latest := adv.Latest(); latest.Type {
    87  	case EventTypeFalsePositiveDetermination,
    88  		EventTypeFixNotPlanned,
    89  		EventTypeAnalysisNotPlanned,
    90  		EventTypePendingUpstreamFix:
    91  		return true
    92  
    93  	case EventTypeFixed:
    94  		return adv.isFixedVersion(version, packageType, latest)
    95  
    96  	default:
    97  		return false
    98  	}
    99  }
   100  
   101  // ConcludedAtVersion returns true if the advisory indicates that the
   102  // vulnerability has been solved, or those where no change is
   103  // expected to fix the CVE in the upstream code.
   104  func (adv Advisory) ConcludedAtVersion(version, packageType string) bool {
   105  	if len(adv.Events) == 0 {
   106  		return false
   107  	}
   108  
   109  	latest := adv.Latest()
   110  	if latest.Type == EventTypePendingUpstreamFix {
   111  		return false
   112  	}
   113  	// NOTE: The resolved set is part of the concluded one
   114  	// with the exception of the pending-upstream-fix event type.
   115  	return adv.ResolvedAtVersion(version, packageType)
   116  }
   117  
   118  // isFixedVersion determines whether the vulnerability discovered for the provided
   119  // version has been fixed.
   120  func (adv Advisory) isFixedVersion(version, packageType string, latest Event) bool {
   121  	if packageType != "apk" {
   122  		return false
   123  	}
   124  
   125  	givenVersion, err := versions.NewVersion(version)
   126  	if err != nil {
   127  		return false
   128  	}
   129  	fixedData, ok := latest.Data.(Fixed)
   130  	if !ok {
   131  		return false
   132  	}
   133  	fixedVersion, err := versions.NewVersion(fixedData.FixedVersion)
   134  	if err != nil {
   135  		return false
   136  	}
   137  
   138  	fixedInLatest := givenVersion.GreaterThanOrEqual(fixedVersion)
   139  	return fixedInLatest
   140  }
   141  
   142  // Validate returns an error if the advisory is invalid.
   143  func (adv Advisory) Validate() error {
   144  	return errorhelpers.LabelError(adv.ID,
   145  		errors.Join(
   146  			vuln.ValidateID(adv.ID),
   147  			adv.validateAliases(),
   148  			adv.validateEvents(),
   149  		),
   150  	)
   151  }
   152  
   153  func (adv Advisory) validateAliases() error {
   154  	var errs []error
   155  
   156  	// Validate aliases as a collection
   157  	errs = append(errs, validateNoDuplicates(adv.Aliases))
   158  
   159  	// Loop through aliases to validate each one
   160  	for _, alias := range adv.Aliases {
   161  		errs = append(errs,
   162  			validateAliasFormat(alias),
   163  			validateAliasIsNotAdvisoryID(alias, adv.ID),
   164  		)
   165  	}
   166  
   167  	return errorhelpers.LabelError("aliases", errors.Join(errs...))
   168  }
   169  
   170  func (adv Advisory) validateEvents() error {
   171  	if len(adv.Events) == 0 {
   172  		return fmt.Errorf("there must be at least one event")
   173  	}
   174  
   175  	return errorhelpers.LabelError("events",
   176  		errors.Join(lo.Map(adv.Events, func(event Event, i int) error {
   177  			err := event.Validate()
   178  			if err != nil {
   179  				// show the event index as 1-based, not 0-based, just for ease of understanding
   180  				return errorhelpers.LabelError(fmt.Sprintf("event %d", i+1), err)
   181  			}
   182  			return nil
   183  		})...),
   184  	)
   185  }
   186  
   187  func validateAliasFormat(alias string) error {
   188  	switch {
   189  	case vuln.RegexGHSA.MatchString(alias),
   190  		vuln.RegexGO.MatchString(alias):
   191  		return nil
   192  	default:
   193  		return fmt.Errorf("%q is not a valid GHSA ID or Go vuln ID", alias)
   194  	}
   195  }
   196  
   197  func validateAliasIsNotAdvisoryID(alias, advisoryID string) error {
   198  	if advisoryID == alias {
   199  		return fmt.Errorf("alias %q cannot duplicate the advisory's ID", alias)
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  func validateNoDuplicates(items []string) error {
   206  	seen := make(map[string]struct{})
   207  	for _, item := range items {
   208  		if _, ok := seen[item]; ok {
   209  			return fmt.Errorf("%q is duplicated in the list", item)
   210  		}
   211  		seen[item] = struct{}{}
   212  	}
   213  	return nil
   214  }