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 }