github.com/wolfi-dev/wolfictl@v0.16.11/pkg/configs/advisory/v2/document.go (about) 1 package v2 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 8 "github.com/hashicorp/go-version" 9 "github.com/samber/lo" 10 "github.com/wolfi-dev/wolfictl/pkg/internal/errorhelpers" 11 "gopkg.in/yaml.v3" 12 ) 13 14 // SchemaVersion is the latest known schema version for advisory documents. 15 // Wolfictl can only operate on documents that use a schema version that is 16 // equal to or earlier than this version and that is not earlier than this 17 // version's MAJOR number. 18 const SchemaVersion = "2.0.2" 19 20 type Document struct { 21 SchemaVersion string `yaml:"schema-version"` 22 Package Package `yaml:"package"` 23 Advisories Advisories `yaml:"advisories,omitempty"` 24 } 25 26 func (doc Document) Name() string { 27 return doc.Package.Name 28 } 29 30 func (doc Document) Validate() error { 31 return errorhelpers.LabelError(doc.Name(), 32 errors.Join( 33 doc.ValidateSchemaVersion(), 34 doc.Package.Validate(), 35 doc.Advisories.Validate(), 36 ), 37 ) 38 } 39 40 func (doc Document) ValidateSchemaVersion() error { 41 docSchemaVersion, err := version.NewVersion(doc.SchemaVersion) 42 if err != nil { 43 return err 44 } 45 46 currentSchemaVersion, err := version.NewVersion(SchemaVersion) 47 if err != nil { 48 return err 49 } 50 51 if docSchemaVersion.GreaterThan(currentSchemaVersion) { 52 return fmt.Errorf("document schema version %q is newer than the latest known schema version %q; if %q is supported by a later version of wolfictl, please update wolfictl and try this again", doc.SchemaVersion, SchemaVersion, doc.SchemaVersion) 53 } 54 55 // Document schema version also can't be earlier than the current schema version's MAJOR number. 56 currentMajorNumber := currentSchemaVersion.Segments()[0] 57 docMajorNumber := docSchemaVersion.Segments()[0] 58 if docMajorNumber < currentMajorNumber { 59 return fmt.Errorf("document schema version %q is too old to operate on with this version of wolfictl, document must use at least schema version \"%d\"", doc.SchemaVersion, currentMajorNumber) 60 } 61 62 return nil 63 } 64 65 func decodeDocument(r io.Reader) (*Document, error) { 66 doc := &Document{} 67 decoder := yaml.NewDecoder(r) 68 decoder.KnownFields(true) 69 err := decoder.Decode(doc) 70 if err != nil { 71 return nil, err 72 } 73 74 if doc.SchemaVersion == "" { 75 doc.SchemaVersion = "1" 76 } 77 78 return doc, nil 79 } 80 81 type Package struct { 82 Name string `yaml:"name"` 83 } 84 85 func (p Package) Validate() error { 86 if p.Name == "" { 87 return fmt.Errorf("package name must not be empty") 88 } 89 90 return nil 91 } 92 93 type Advisories []Advisory 94 95 func (advs Advisories) Validate() error { 96 if len(advs) == 0 { 97 return fmt.Errorf("this file should not exist if there are no advisories recorded") 98 } 99 100 seenIDs := make(map[string]bool) 101 var duplicateErrs []error 102 for _, adv := range advs { 103 if _, ok := seenIDs[adv.ID]; ok { 104 duplicateErrs = append(duplicateErrs, fmt.Errorf("%s: %w", adv.ID, ErrAdvisoryIDDuplicated)) 105 } 106 seenIDs[adv.ID] = true 107 108 for _, alias := range adv.Aliases { 109 if _, ok := seenIDs[alias]; ok { 110 duplicateErrs = append(duplicateErrs, fmt.Errorf("%s: %w", alias, ErrAdvisoryAliasDuplicated)) 111 } 112 seenIDs[alias] = true 113 } 114 } 115 116 if len(duplicateErrs) > 0 { 117 return errorhelpers.LabelError("advisories", errors.Join(duplicateErrs...)) 118 } 119 120 return errorhelpers.LabelError("advisories", 121 errors.Join(lo.Map(advs, func(adv Advisory, _ int) error { 122 return adv.Validate() 123 })...), 124 ) 125 } 126 127 var ( 128 ErrAdvisoryIDDuplicated = errors.New("advisory ID is not unique") 129 ErrAdvisoryAliasDuplicated = errors.New("advisory alias is not unique") 130 ) 131 132 // Get returns the advisory with the given ID. If such an advisory does not 133 // exist, the second return value will be false; otherwise it will be true. 134 func (advs Advisories) Get(id string) (Advisory, bool) { 135 for _, adv := range advs { 136 if adv.ID == id { 137 return adv, true 138 } 139 } 140 141 return Advisory{}, false 142 } 143 144 // GetByVulnerability returns the advisory that references the given 145 // vulnerability ID as its advisory ID or as one of the advisory's aliases. If 146 // such an advisory does not exist, the second return value will be false; 147 // otherwise it will be true. 148 func (advs Advisories) GetByVulnerability(id string) (Advisory, bool) { 149 for _, adv := range advs { 150 if adv.ID == id { 151 return adv, true 152 } 153 154 for _, alias := range adv.Aliases { 155 if alias == id { 156 return adv, true 157 } 158 } 159 } 160 161 return Advisory{}, false 162 } 163 164 func (advs Advisories) Update(id string, advisory Advisory) Advisories { 165 for i, adv := range advs { 166 if adv.ID == id { 167 advs[i] = advisory 168 return advs 169 } 170 } 171 172 return advs 173 } 174 175 // Implement sort.Interface for Advisories. 176 177 func (advs Advisories) Len() int { 178 return len(advs) 179 } 180 181 func (advs Advisories) Less(i, j int) bool { 182 return advs[i].ID < advs[j].ID 183 } 184 185 func (advs Advisories) Swap(i, j int) { 186 advs[i], advs[j] = advs[j], advs[i] 187 } 188 189 func validateNotEmpty(s string) error { 190 if s == "" { 191 return fmt.Errorf("must not be empty") 192 } 193 194 return nil 195 }