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  }