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

     1  package advisory
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"sort"
     8  
     9  	"github.com/chainguard-dev/clog"
    10  	"github.com/wolfi-dev/wolfictl/pkg/advisory/secdb"
    11  	"github.com/wolfi-dev/wolfictl/pkg/configs"
    12  	v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2"
    13  )
    14  
    15  const apkURL = "{{urlprefix}}/{{reponame}}/{{arch}}/{{pkg.name}}-{{pkg.ver}}.apk"
    16  
    17  // BuildSecurityDatabaseOptions contains the options for building a database.
    18  type BuildSecurityDatabaseOptions struct {
    19  	AdvisoryDocIndices []*configs.Index[v2.Document]
    20  
    21  	URLPrefix string
    22  	Archs     []string
    23  	Repo      string
    24  }
    25  
    26  var (
    27  	ErrNoPackageSecurityData = errors.New("no package security data found")
    28  	ErrorPackageCollision    = errors.New("found multiple advisory documents for the same package")
    29  )
    30  
    31  // BuildSecurityDatabase builds an Alpine-style security database from the given options.
    32  func BuildSecurityDatabase(ctx context.Context, opts BuildSecurityDatabaseOptions) ([]byte, error) {
    33  	log := clog.FromContext(ctx)
    34  	var packageEntries []secdb.PackageEntry
    35  
    36  	seenPackages := make(map[string]struct{})
    37  
    38  	for _, index := range opts.AdvisoryDocIndices {
    39  		var indexPackageEntries []secdb.PackageEntry
    40  
    41  		for _, doc := range index.Select().Configurations() {
    42  			if _, exists := seenPackages[doc.Package.Name]; exists {
    43  				// TODO Merge the events between multiple advisories
    44  				log.InfoContextf(ctx, "cannot process additional advisory data for package %q: %v", doc.Package.Name, ErrorPackageCollision)
    45  			}
    46  			seenPackages[doc.Package.Name] = struct{}{}
    47  
    48  			if len(doc.Advisories) == 0 {
    49  				continue
    50  			}
    51  
    52  			secfixes := make(secdb.Secfixes)
    53  
    54  			sort.Slice(doc.Advisories, func(i, j int) bool {
    55  				return doc.Advisories[i].ID < doc.Advisories[j].ID
    56  			})
    57  
    58  			for _, advisory := range doc.Advisories {
    59  				if len(advisory.Events) == 0 {
    60  					continue
    61  				}
    62  
    63  				sortedEvents := advisory.SortedEvents()
    64  				latest := sortedEvents[len(advisory.Events)-1]
    65  
    66  				addVulnToPkgVersion := func(vulnID string) {
    67  					switch latest.Type {
    68  					case v2.EventTypeFixed:
    69  						version := latest.Data.(v2.Fixed).FixedVersion
    70  						secfixes[version] = append(secfixes[version], vulnID)
    71  					case v2.EventTypeFalsePositiveDetermination:
    72  						secfixes[secdb.NAK] = append(secfixes[secdb.NAK], vulnID)
    73  					}
    74  				}
    75  
    76  				// Get vulnerability from advisory ID
    77  
    78  				vulnID := advisory.ID
    79  				addVulnToPkgVersion(vulnID)
    80  
    81  				// Get vulnerabilities from advisory aliases
    82  
    83  				for _, alias := range advisory.Aliases {
    84  					vulnID := alias
    85  					addVulnToPkgVersion(vulnID)
    86  				}
    87  			}
    88  
    89  			if len(secfixes) == 0 {
    90  				continue
    91  			}
    92  
    93  			pe := secdb.PackageEntry{
    94  				Pkg: secdb.Package{
    95  					Name:     doc.Package.Name,
    96  					Secfixes: secfixes,
    97  				},
    98  			}
    99  
   100  			indexPackageEntries = append(indexPackageEntries, pe)
   101  		}
   102  
   103  		if len(indexPackageEntries) == 0 {
   104  			// Catch the unexpected case where an advisories directory contains no security data.
   105  			return nil, ErrNoPackageSecurityData
   106  		}
   107  
   108  		packageEntries = append(packageEntries, indexPackageEntries...)
   109  	}
   110  
   111  	db := secdb.Database{
   112  		APKURL:    apkURL,
   113  		Archs:     opts.Archs,
   114  		Repo:      opts.Repo,
   115  		URLPrefix: opts.URLPrefix,
   116  		Packages:  packageEntries,
   117  	}
   118  
   119  	return json.MarshalIndent(db, "", "  ")
   120  }