github.com/khulnasoft-lab/tunnel-db@v0.0.0-20231117205118-74e1113bd007/pkg/vulnsrc/rocky/rocky.go (about)

     1  package rocky
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/samber/lo"
    13  	bolt "go.etcd.io/bbolt"
    14  	"golang.org/x/exp/slices"
    15  	"golang.org/x/xerrors"
    16  
    17  	"github.com/khulnasoft-lab/tunnel-db/pkg/db"
    18  	"github.com/khulnasoft-lab/tunnel-db/pkg/types"
    19  	"github.com/khulnasoft-lab/tunnel-db/pkg/utils"
    20  	ustrings "github.com/khulnasoft-lab/tunnel-db/pkg/utils/strings"
    21  	"github.com/khulnasoft-lab/tunnel-db/pkg/vulnsrc/vulnerability"
    22  )
    23  
    24  const (
    25  	rockyDir       = "rocky"
    26  	platformFormat = "rocky %s"
    27  )
    28  
    29  var (
    30  	targetRepos = []string{
    31  		"BaseOS",
    32  		"AppStream",
    33  		"extras",
    34  	}
    35  	targetArches = []string{
    36  		"x86_64",
    37  		"aarch64",
    38  	}
    39  	source = types.DataSource{
    40  		ID:   vulnerability.Rocky,
    41  		Name: "Rocky Linux updateinfo",
    42  		URL:  "https://download.rockylinux.org/pub/rocky/",
    43  	}
    44  )
    45  
    46  type PutInput struct {
    47  	PlatformName string
    48  	CveID        string
    49  	Vuln         types.VulnerabilityDetail
    50  	Advisories   map[string]types.Advisories // pkg name => advisory
    51  	Erratum      RLSA                        // for extensibility, not used in tunnel-db
    52  }
    53  
    54  type DB interface {
    55  	db.Operation
    56  	Put(*bolt.Tx, PutInput) error
    57  	Get(release, pkgName, arch string) ([]types.Advisory, error)
    58  }
    59  
    60  type VulnSrc struct {
    61  	DB
    62  }
    63  
    64  type Rocky struct {
    65  	db.Operation
    66  }
    67  
    68  func NewVulnSrc() *VulnSrc {
    69  	return &VulnSrc{
    70  		DB: &Rocky{Operation: db.Config{}},
    71  	}
    72  }
    73  
    74  func (vs *VulnSrc) Name() types.SourceID {
    75  	return source.ID
    76  }
    77  
    78  func (vs *VulnSrc) Update(dir string) error {
    79  	rootDir := filepath.Join(dir, "vuln-list", rockyDir)
    80  	errata, err := vs.parse(rootDir)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	if err = vs.put(errata); err != nil {
    85  		return xerrors.Errorf("error in Rocky save: %w", err)
    86  	}
    87  
    88  	return nil
    89  }
    90  
    91  // parse parses all the advisories from Rocky Linux.
    92  // It is exported for those who want to customize tunnel-db.
    93  func (vs *VulnSrc) parse(rootDir string) (map[string][]RLSA, error) {
    94  	errata := map[string][]RLSA{}
    95  	err := utils.FileWalk(rootDir, func(r io.Reader, path string) error {
    96  		var erratum RLSA
    97  		if err := json.NewDecoder(r).Decode(&erratum); err != nil {
    98  			return xerrors.Errorf("failed to decode Rocky erratum: %w", err)
    99  		}
   100  
   101  		dirs := strings.Split(strings.TrimPrefix(path, rootDir), string(filepath.Separator))[1:]
   102  		if len(dirs) != 5 {
   103  			log.Printf("Invalid path: %s", path)
   104  			return nil
   105  		}
   106  
   107  		// vulnerabilities are contained in directories with a minor version(like 8.5)
   108  		majorVer := dirs[0]
   109  		if strings.Count(dirs[0], ".") > 0 {
   110  			majorVer = dirs[0][:strings.Index(dirs[0], ".")]
   111  		}
   112  		repo, arch := dirs[1], dirs[2]
   113  		if !ustrings.InSlice(repo, targetRepos) {
   114  			log.Printf("Unsupported Rocky repo: %s", repo)
   115  			return nil
   116  		}
   117  
   118  		if !ustrings.InSlice(arch, targetArches) {
   119  			log.Printf("Unsupported Rocky arch: %s", arch)
   120  			return nil
   121  		}
   122  
   123  		errata[majorVer] = append(errata[majorVer], erratum)
   124  		return nil
   125  	})
   126  	if err != nil {
   127  		return nil, xerrors.Errorf("error in Rocky walk: %w", err)
   128  	}
   129  	return errata, nil
   130  }
   131  
   132  func (vs *VulnSrc) put(errataVer map[string][]RLSA) error {
   133  	err := vs.BatchUpdate(func(tx *bolt.Tx) error {
   134  		for majorVer, errata := range errataVer {
   135  			platformName := fmt.Sprintf(platformFormat, majorVer)
   136  			if err := vs.PutDataSource(tx, platformName, source); err != nil {
   137  				return xerrors.Errorf("failed to put data source: %w", err)
   138  			}
   139  			if err := vs.commit(tx, platformName, errata); err != nil {
   140  				return xerrors.Errorf("error in save Rocky %s: %w", majorVer, err)
   141  			}
   142  		}
   143  		return nil
   144  	})
   145  	if err != nil {
   146  		return xerrors.Errorf("error in db batch update: %w", err)
   147  	}
   148  	return nil
   149  }
   150  
   151  func (vs *VulnSrc) commit(tx *bolt.Tx, platformName string, errata []RLSA) error {
   152  	savedInputs := map[string]PutInput{}
   153  	for _, erratum := range errata {
   154  		for _, cveID := range erratum.CveIDs {
   155  			input := PutInput{
   156  				Advisories: map[string]types.Advisories{},
   157  			}
   158  			if in, ok := savedInputs[cveID]; ok {
   159  				input = in
   160  			}
   161  			for _, pkg := range erratum.Packages {
   162  				// Skip the modular packages until the following bug is fixed.
   163  				// https://forums.rockylinux.org/t/some-errata-missing-in-comparison-with-rhel-and-almalinux/3843/8
   164  				if strings.Contains(pkg.Release, ".module+el") {
   165  					continue
   166  				}
   167  
   168  				entry := types.Advisory{
   169  					FixedVersion: utils.ConstructVersion(pkg.Epoch, pkg.Version, pkg.Release),
   170  					Arches:       []string{pkg.Arch},
   171  					VendorIDs:    []string{erratum.ID},
   172  				}
   173  
   174  				// if the advisory for this package and CVE have been kept - just add the new architecture
   175  				if adv, ok := input.Advisories[pkg.Name]; ok {
   176  					// update `fixedVersion` if `fixedVersion` for `x86_64` was not previously saved
   177  					adv.FixedVersion = fixedVersion(adv.FixedVersion, entry.FixedVersion, pkg.Arch)
   178  
   179  					old, i, found := lo.FindIndexOf(adv.Entries, func(adv types.Advisory) bool {
   180  						return adv.FixedVersion == entry.FixedVersion
   181  					})
   182  
   183  					// If the advisory with the same fixed version and RLSA-ID is present - just add the new architecture
   184  					if found {
   185  						if !slices.Contains(old.Arches, pkg.Arch) {
   186  							adv.Entries[i].Arches = append(old.Arches, pkg.Arch)
   187  						}
   188  						if !slices.Contains(old.VendorIDs, erratum.ID) {
   189  							adv.Entries[i].VendorIDs = append(old.VendorIDs, erratum.ID)
   190  						}
   191  						input.Advisories[pkg.Name] = adv
   192  					} else if !found {
   193  						adv.Entries = append(adv.Entries, entry)
   194  						input.Advisories[pkg.Name] = adv
   195  					}
   196  				} else {
   197  					input.Advisories[pkg.Name] = types.Advisories{
   198  						// will save `0.0.0` version for non-`x86_64` arch
   199  						// to avoid false positives when using old Tunnel with new database
   200  						FixedVersion: fixedVersion("0.0.0", entry.FixedVersion, pkg.Arch), // For backward compatibility
   201  						Entries:      []types.Advisory{entry},
   202  					}
   203  				}
   204  			}
   205  
   206  			if len(input.Advisories) == 0 {
   207  				continue
   208  			}
   209  
   210  			var references []string
   211  			for _, ref := range erratum.References {
   212  				references = append(references, ref.Href)
   213  			}
   214  
   215  			vuln := types.VulnerabilityDetail{
   216  				Severity:    generalizeSeverity(erratum.Severity),
   217  				References:  references,
   218  				Title:       erratum.Title,
   219  				Description: erratum.Description,
   220  			}
   221  
   222  			input.PlatformName = platformName
   223  			input.CveID = cveID
   224  			input.Vuln = vuln
   225  			input.Erratum = erratum // For Tunnel Premium
   226  
   227  			savedInputs[cveID] = input
   228  		}
   229  	}
   230  
   231  	for _, input := range savedInputs {
   232  		err := vs.Put(tx, input)
   233  		if err != nil {
   234  			return xerrors.Errorf("db put error: %w", err)
   235  		}
   236  	}
   237  	return nil
   238  }
   239  
   240  func (r *Rocky) Put(tx *bolt.Tx, input PutInput) error {
   241  	if err := r.PutVulnerabilityDetail(tx, input.CveID, source.ID, input.Vuln); err != nil {
   242  		return xerrors.Errorf("failed to save Rocky vulnerability: %w", err)
   243  	}
   244  
   245  	// for optimization
   246  	if err := r.PutVulnerabilityID(tx, input.CveID); err != nil {
   247  		return xerrors.Errorf("failed to save the vulnerability ID: %w", err)
   248  	}
   249  
   250  	for pkgName, advisory := range input.Advisories {
   251  		for _, entry := range advisory.Entries {
   252  			sort.Strings(entry.Arches)
   253  			sort.Strings(entry.VendorIDs)
   254  		}
   255  		if err := r.PutAdvisoryDetail(tx, input.CveID, pkgName, []string{input.PlatformName}, advisory); err != nil {
   256  			return xerrors.Errorf("failed to save Rocky advisory: %w", err)
   257  		}
   258  	}
   259  	return nil
   260  }
   261  
   262  func (r *Rocky) Get(release, pkgName, arch string) ([]types.Advisory, error) {
   263  	bucket := fmt.Sprintf(platformFormat, release)
   264  	rawAdvisories, err := r.ForEachAdvisory([]string{bucket}, pkgName)
   265  	if err != nil {
   266  		return nil, xerrors.Errorf("unable to iterate advisories: %w", err)
   267  	}
   268  	var advisories []types.Advisory
   269  	for vulnID, v := range rawAdvisories {
   270  		var adv types.Advisories
   271  		if err = json.Unmarshal(v.Content, &adv); err != nil {
   272  			return nil, xerrors.Errorf("failed to unmarshal advisory JSON: %w", err)
   273  		}
   274  
   275  		// For backward compatibility
   276  		// The old tunnel-db has no entries, but has fixed versions and custom fields.
   277  		if len(adv.Entries) == 0 {
   278  			advisories = append(advisories, types.Advisory{
   279  				VulnerabilityID: vulnID,
   280  				FixedVersion:    adv.FixedVersion,
   281  				DataSource:      &v.Source,
   282  				Custom:          adv.Custom,
   283  			})
   284  			continue
   285  		}
   286  
   287  		for _, entry := range adv.Entries {
   288  			if !slices.Contains(entry.Arches, arch) {
   289  				continue
   290  			}
   291  			entry.VulnerabilityID = vulnID
   292  			entry.DataSource = &v.Source
   293  			advisories = append(advisories, entry)
   294  		}
   295  	}
   296  
   297  	return advisories, nil
   298  }
   299  
   300  func generalizeSeverity(severity string) types.Severity {
   301  	switch strings.ToLower(severity) {
   302  	case "low":
   303  		return types.SeverityLow
   304  	case "moderate":
   305  		return types.SeverityMedium
   306  	case "important":
   307  		return types.SeverityHigh
   308  	case "critical":
   309  		return types.SeverityCritical
   310  	}
   311  	return types.SeverityUnknown
   312  }
   313  
   314  // fixedVersion checks for the arch and only updates version for `x86_64`
   315  // only used for types.Advisories.FixedVersion for backward compatibility
   316  func fixedVersion(prevVersion, newVersion, arch string) string {
   317  	if arch == "x86_64" || arch == "noarch" {
   318  		return newVersion
   319  	}
   320  	return prevVersion
   321  }