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

     1  package redhatoval
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  
    14  	bolt "go.etcd.io/bbolt"
    15  	"golang.org/x/exp/slices"
    16  	"golang.org/x/xerrors"
    17  
    18  	"github.com/khulnasoft-lab/tunnel-db/pkg/db"
    19  	"github.com/khulnasoft-lab/tunnel-db/pkg/types"
    20  	"github.com/khulnasoft-lab/tunnel-db/pkg/utils"
    21  	"github.com/khulnasoft-lab/tunnel-db/pkg/utils/ints"
    22  	ustrings "github.com/khulnasoft-lab/tunnel-db/pkg/utils/strings"
    23  	"github.com/khulnasoft-lab/tunnel-db/pkg/vulnsrc/vulnerability"
    24  )
    25  
    26  const (
    27  	rootBucket = "Red Hat"
    28  )
    29  
    30  var (
    31  	ovalDir     = "oval"
    32  	cpeDir      = "cpe"
    33  	vulnListDir = "vuln-list-redhat"
    34  
    35  	moduleRegexp = regexp.MustCompile(`Module\s+(.*)\s+is enabled`)
    36  
    37  	source = types.DataSource{
    38  		ID:   vulnerability.RedHatOVAL,
    39  		Name: "Red Hat OVAL v2",
    40  		URL:  "https://www.redhat.com/security/data/oval/v2/",
    41  	}
    42  )
    43  
    44  type VulnSrc struct {
    45  	dbc db.Operation
    46  }
    47  
    48  func NewVulnSrc() VulnSrc {
    49  	return VulnSrc{
    50  		dbc: db.Config{},
    51  	}
    52  }
    53  
    54  func (vs VulnSrc) Name() types.SourceID {
    55  	return vulnerability.RedHatOVAL
    56  }
    57  
    58  func (vs VulnSrc) Update(dir string) error {
    59  	uniqCPEs := CPEMap{}
    60  
    61  	repoToCPE, err := vs.parseRepositoryCpeMapping(dir, uniqCPEs)
    62  	if err != nil {
    63  		return xerrors.Errorf("unable to store the mapping between repositories and CPE names: %w", err)
    64  	}
    65  
    66  	nvrToCPE, err := vs.parseNvrCpeMapping(dir, uniqCPEs)
    67  	if err != nil {
    68  		return xerrors.Errorf("unable to store the mapping between NVR and CPE names: %w", err)
    69  	}
    70  
    71  	// List version directories
    72  	rootDir := filepath.Join(dir, vulnListDir, ovalDir)
    73  	versions, err := os.ReadDir(rootDir)
    74  	if err != nil {
    75  		return xerrors.Errorf("unable to list directory entries (%s): %w", rootDir, err)
    76  	}
    77  
    78  	advisories := map[bucket]Advisory{}
    79  	for _, ver := range versions {
    80  		versionDir := filepath.Join(rootDir, ver.Name())
    81  		streams, err := os.ReadDir(versionDir)
    82  		if err != nil {
    83  			return xerrors.Errorf("unable to get a list of directory entries (%s): %w", versionDir, err)
    84  		}
    85  
    86  		for _, f := range streams {
    87  			if !f.IsDir() {
    88  				continue
    89  			}
    90  
    91  			definitions, err := parseOVALStream(filepath.Join(versionDir, f.Name()), uniqCPEs)
    92  			if err != nil {
    93  				return xerrors.Errorf("failed to parse OVAL stream: %w", err)
    94  			}
    95  
    96  			advisories = vs.mergeAdvisories(advisories, definitions)
    97  		}
    98  	}
    99  
   100  	if err = vs.save(repoToCPE, nvrToCPE, advisories, uniqCPEs); err != nil {
   101  		return xerrors.Errorf("save error: %w", err)
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  func (vs VulnSrc) parseRepositoryCpeMapping(dir string, uniqCPEs CPEMap) (map[string][]string, error) {
   108  	filePath := filepath.Join(dir, vulnListDir, cpeDir, "repository-to-cpe.json")
   109  	f, err := os.Open(filePath)
   110  	if err != nil {
   111  		return nil, xerrors.Errorf("file open error: %w", err)
   112  	}
   113  	defer f.Close()
   114  
   115  	var repoToCPE map[string][]string
   116  	if err = json.NewDecoder(f).Decode(&repoToCPE); err != nil {
   117  		return nil, xerrors.Errorf("JSON parse error: %w", err)
   118  	}
   119  
   120  	for _, cpes := range repoToCPE {
   121  		updateCPEs(cpes, uniqCPEs)
   122  	}
   123  
   124  	return repoToCPE, nil
   125  }
   126  
   127  func (vs VulnSrc) parseNvrCpeMapping(dir string, uniqCPEs CPEMap) (map[string][]string, error) {
   128  	filePath := filepath.Join(dir, vulnListDir, cpeDir, "nvr-to-cpe.json")
   129  	f, err := os.Open(filePath)
   130  	if err != nil {
   131  		return nil, xerrors.Errorf("file open error: %w", err)
   132  	}
   133  	defer f.Close()
   134  
   135  	nvrToCpe := map[string][]string{}
   136  	if err = json.NewDecoder(f).Decode(&nvrToCpe); err != nil {
   137  		return nil, xerrors.Errorf("JSON parse error: %w", err)
   138  	}
   139  
   140  	for _, cpes := range nvrToCpe {
   141  		updateCPEs(cpes, uniqCPEs)
   142  	}
   143  	return nvrToCpe, nil
   144  }
   145  
   146  func (vs VulnSrc) mergeAdvisories(advisories map[bucket]Advisory, defs map[bucket]Definition) map[bucket]Advisory {
   147  	for bkt, def := range defs {
   148  		if old, ok := advisories[bkt]; ok {
   149  			found := false
   150  			for i := range old.Entries {
   151  				// New advisory should contain a single fixed version and list of arches.
   152  				if old.Entries[i].FixedVersion == def.Entry.FixedVersion && old.Entries[i].Status == def.Entry.Status &&
   153  					slices.Equal(old.Entries[i].Arches, def.Entry.Arches) && slices.Equal(old.Entries[i].Cves, def.Entry.Cves) {
   154  					found = true
   155  					old.Entries[i].AffectedCPEList = ustrings.Merge(old.Entries[i].AffectedCPEList, def.Entry.AffectedCPEList)
   156  				}
   157  			}
   158  			if !found {
   159  				old.Entries = append(old.Entries, def.Entry)
   160  			}
   161  			advisories[bkt] = old
   162  		} else {
   163  			advisories[bkt] = Advisory{
   164  				Entries: []Entry{def.Entry},
   165  			}
   166  		}
   167  	}
   168  
   169  	return advisories
   170  }
   171  
   172  func (vs VulnSrc) save(repoToCpe, nvrToCpe map[string][]string, advisories map[bucket]Advisory, uniqCPEs CPEMap) error {
   173  	cpeList := uniqCPEs.List()
   174  	err := vs.dbc.BatchUpdate(func(tx *bolt.Tx) error {
   175  		if err := vs.dbc.PutDataSource(tx, rootBucket, source); err != nil {
   176  			return xerrors.Errorf("failed to put data source: %w", err)
   177  		}
   178  
   179  		// Store the mapping between repository and CPE names
   180  		for repo, cpes := range repoToCpe {
   181  			if err := vs.dbc.PutRedHatRepositories(tx, repo, cpeList.Indices(cpes)); err != nil {
   182  				return xerrors.Errorf("repository put error: %w", err)
   183  			}
   184  		}
   185  
   186  		// Store the mapping between NVR and CPE names
   187  		for nvr, cpes := range nvrToCpe {
   188  			if err := vs.dbc.PutRedHatNVRs(tx, nvr, cpeList.Indices(cpes)); err != nil {
   189  				return xerrors.Errorf("NVR put error: %w", err)
   190  			}
   191  		}
   192  
   193  		//  Store advisories
   194  		for bkt, advisory := range advisories {
   195  			for i := range advisory.Entries {
   196  				// Convert CPE names to indices.
   197  				advisory.Entries[i].AffectedCPEIndices = cpeList.Indices(advisory.Entries[i].AffectedCPEList)
   198  			}
   199  
   200  			if err := vs.dbc.PutAdvisoryDetail(tx, bkt.vulnID, bkt.pkgName, []string{rootBucket}, advisory); err != nil {
   201  				return xerrors.Errorf("failed to save Red Hat OVAL advisory: %w", err)
   202  			}
   203  
   204  			if err := vs.dbc.PutVulnerabilityID(tx, bkt.vulnID); err != nil {
   205  				return xerrors.Errorf("failed to put severity: %w", err)
   206  			}
   207  		}
   208  
   209  		// Store CPE indices for debug information
   210  		for i, cpe := range cpeList {
   211  			if err := vs.dbc.PutRedHatCPEs(tx, i, cpe); err != nil {
   212  				return xerrors.Errorf("CPE put error: %w", err)
   213  			}
   214  		}
   215  
   216  		return nil
   217  	})
   218  	if err != nil {
   219  		return xerrors.Errorf("batch update error: %w", err)
   220  	}
   221  	return nil
   222  }
   223  
   224  func (vs VulnSrc) cpeIndices(repositories, nvrs []string) ([]int, error) {
   225  	var cpeIndices []int
   226  	for _, repo := range repositories {
   227  		results, err := vs.dbc.RedHatRepoToCPEs(repo)
   228  		if err != nil {
   229  			return nil, xerrors.Errorf("unable to convert repositories to CPEs: %w", err)
   230  		}
   231  		cpeIndices = append(cpeIndices, results...)
   232  	}
   233  
   234  	for _, nvr := range nvrs {
   235  		results, err := vs.dbc.RedHatNVRToCPEs(nvr)
   236  		if err != nil {
   237  			return nil, xerrors.Errorf("unable to convert repositories to CPEs: %w", err)
   238  		}
   239  		cpeIndices = append(cpeIndices, results...)
   240  	}
   241  
   242  	return ints.Unique(cpeIndices), nil
   243  }
   244  
   245  func (vs VulnSrc) Get(pkgName string, repositories, nvrs []string) ([]types.Advisory, error) {
   246  	cpeIndices, err := vs.cpeIndices(repositories, nvrs)
   247  	if err != nil {
   248  		return nil, xerrors.Errorf("CPE convert error: %w", err)
   249  	}
   250  
   251  	rawAdvisories, err := vs.dbc.ForEachAdvisory([]string{rootBucket}, pkgName)
   252  	if err != nil {
   253  		return nil, xerrors.Errorf("unable to iterate advisories: %w", err)
   254  	}
   255  
   256  	var advisories []types.Advisory
   257  	for vulnID, v := range rawAdvisories {
   258  		var adv Advisory
   259  		if err = json.Unmarshal(v.Content, &adv); err != nil {
   260  			return nil, xerrors.Errorf("failed to unmarshal advisory JSON: %w", err)
   261  		}
   262  
   263  		for _, entry := range adv.Entries {
   264  			if !ints.HasIntersection(cpeIndices, entry.AffectedCPEIndices) {
   265  				continue
   266  			}
   267  
   268  			for _, cve := range entry.Cves {
   269  				advisory := types.Advisory{
   270  					Severity:     cve.Severity,
   271  					FixedVersion: entry.FixedVersion,
   272  					Arches:       entry.Arches,
   273  					Status:       entry.Status,
   274  					DataSource:   &v.Source,
   275  				}
   276  
   277  				if strings.HasPrefix(vulnID, "CVE-") {
   278  					advisory.VulnerabilityID = vulnID
   279  				} else {
   280  					advisory.VulnerabilityID = cve.ID
   281  					advisory.VendorIDs = []string{vulnID}
   282  				}
   283  
   284  				advisories = append(advisories, advisory)
   285  			}
   286  		}
   287  	}
   288  
   289  	return advisories, nil
   290  }
   291  
   292  func parseOVALStream(dir string, uniqCPEs CPEMap) (map[bucket]Definition, error) {
   293  	log.Printf("    Parsing %s", dir)
   294  
   295  	// Parse tests
   296  	tests, err := parseTests(dir)
   297  	if err != nil {
   298  		return nil, xerrors.Errorf("failed to parse ovalTests: %w", err)
   299  	}
   300  
   301  	var advisories []redhatOVAL
   302  	definitionsDir := filepath.Join(dir, "definitions")
   303  	if exists, _ := utils.Exists(definitionsDir); !exists {
   304  		return nil, nil
   305  	}
   306  
   307  	err = utils.FileWalk(definitionsDir, func(r io.Reader, path string) error {
   308  		var definition redhatOVAL
   309  		if err := json.NewDecoder(r).Decode(&definition); err != nil {
   310  			return xerrors.Errorf("failed to decode %s: %w", path, err)
   311  		}
   312  		advisories = append(advisories, definition)
   313  		return nil
   314  	})
   315  
   316  	if err != nil {
   317  		return nil, xerrors.Errorf("Red Hat OVAL walk error: %w", err)
   318  	}
   319  
   320  	return parseDefinitions(advisories, tests, uniqCPEs), nil
   321  }
   322  
   323  func parseDefinitions(advisories []redhatOVAL, tests map[string]rpmInfoTest, uniqCPEs CPEMap) map[bucket]Definition {
   324  	defs := map[bucket]Definition{}
   325  
   326  	for _, advisory := range advisories {
   327  		// Skip unaffected vulnerabilities
   328  		if strings.Contains(advisory.ID, "unaffected") {
   329  			continue
   330  		}
   331  
   332  		// Parse criteria
   333  		moduleName, affectedPkgs := walkCriterion(advisory.Criteria, tests)
   334  		for _, affectedPkg := range affectedPkgs {
   335  			pkgName := affectedPkg.Name
   336  			if moduleName != "" {
   337  				// Add modular namespace
   338  				// e.g. nodejs:12::npm
   339  				pkgName = fmt.Sprintf("%s::%s", moduleName, pkgName)
   340  			}
   341  
   342  			rhsaID := vendorID(advisory.Metadata.References)
   343  
   344  			var cveEntries []CveEntry
   345  			for _, cve := range advisory.Metadata.Advisory.Cves {
   346  				cveEntries = append(cveEntries, CveEntry{
   347  					ID:       cve.CveID,
   348  					Severity: severityFromImpact(cve.Impact),
   349  				})
   350  			}
   351  			sort.Slice(cveEntries, func(i, j int) bool {
   352  				return cveEntries[i].ID < cveEntries[j].ID
   353  			})
   354  
   355  			if rhsaID != "" { // For patched vulnerabilities
   356  				bkt := bucket{
   357  					pkgName: pkgName,
   358  					vulnID:  rhsaID,
   359  				}
   360  				defs[bkt] = Definition{
   361  					Entry: Entry{
   362  						Cves:            cveEntries,
   363  						FixedVersion:    affectedPkg.FixedVersion,
   364  						AffectedCPEList: advisory.Metadata.Advisory.AffectedCpeList,
   365  						Arches:          affectedPkg.Arches,
   366  
   367  						// The status is obviously "fixed" when there is a patch.
   368  						// To keep the database size small, we don't store the status for patched vulns.
   369  						// Status:		  StatusFixed,
   370  					},
   371  				}
   372  			} else { // For unpatched vulnerabilities
   373  				for _, cve := range cveEntries {
   374  					bkt := bucket{
   375  						pkgName: pkgName,
   376  						vulnID:  cve.ID,
   377  					}
   378  					defs[bkt] = Definition{
   379  						Entry: Entry{
   380  							Cves: []CveEntry{
   381  								{
   382  									Severity: cve.Severity,
   383  								},
   384  							},
   385  							FixedVersion:    affectedPkg.FixedVersion,
   386  							AffectedCPEList: advisory.Metadata.Advisory.AffectedCpeList,
   387  							Arches:          affectedPkg.Arches,
   388  							Status:          newStatus(advisory.Metadata.Advisory.Affected.Resolution.State),
   389  						},
   390  					}
   391  				}
   392  			}
   393  		}
   394  
   395  		updateCPEs(advisory.Metadata.Advisory.AffectedCpeList, uniqCPEs)
   396  	}
   397  	return defs
   398  }
   399  
   400  func walkCriterion(cri criteria, tests map[string]rpmInfoTest) (string, []pkg) {
   401  	var moduleName string
   402  	var packages []pkg
   403  
   404  	for _, c := range cri.Criterions {
   405  		// Parse module name
   406  		m := moduleRegexp.FindStringSubmatch(c.Comment)
   407  		if len(m) > 1 && m[1] != "" {
   408  			moduleName = m[1]
   409  			continue
   410  		}
   411  
   412  		t, ok := tests[c.TestRef]
   413  		if !ok {
   414  			continue
   415  		}
   416  
   417  		// Skip red-def:signature_keyid
   418  		if t.SignatureKeyID.Text != "" {
   419  			continue
   420  		}
   421  
   422  		var arches []string
   423  		if t.Arch != "" {
   424  			arches = strings.Split(t.Arch, "|") // affected arches are merged with '|'(e.g. 'aarch64|ppc64le|x86_64')
   425  			sort.Strings(arches)
   426  		}
   427  
   428  		packages = append(packages, pkg{
   429  			Name:         t.Name,
   430  			FixedVersion: t.FixedVersion,
   431  			Arches:       arches,
   432  		})
   433  	}
   434  
   435  	if len(cri.Criterias) == 0 {
   436  		return moduleName, packages
   437  	}
   438  
   439  	for _, c := range cri.Criterias {
   440  		m, pkgs := walkCriterion(c, tests)
   441  		if m != "" {
   442  			moduleName = m
   443  		}
   444  		if len(pkgs) != 0 {
   445  			packages = append(packages, pkgs...)
   446  		}
   447  	}
   448  	return moduleName, packages
   449  }
   450  
   451  func updateCPEs(cpes []string, uniqCPEs CPEMap) {
   452  	for _, cpe := range cpes {
   453  		cpe = strings.TrimSpace(cpe)
   454  		if cpe == "" {
   455  			continue
   456  		}
   457  		uniqCPEs.Add(cpe)
   458  	}
   459  }
   460  
   461  func vendorID(refs []reference) string {
   462  	for _, ref := range refs {
   463  		switch ref.Source {
   464  		case "RHSA", "RHBA":
   465  			return ref.RefID
   466  		}
   467  	}
   468  	return ""
   469  }
   470  
   471  func severityFromImpact(sev string) types.Severity {
   472  	switch strings.ToLower(sev) {
   473  	case "low":
   474  		return types.SeverityLow
   475  	case "moderate":
   476  		return types.SeverityMedium
   477  	case "important":
   478  		return types.SeverityHigh
   479  	case "critical":
   480  		return types.SeverityCritical
   481  	}
   482  	return types.SeverityUnknown
   483  }
   484  
   485  func newStatus(s string) types.Status {
   486  	switch strings.ToLower(s) {
   487  	case "affected", "fix deferred":
   488  		return types.StatusAffected
   489  	case "under investigation":
   490  		return types.StatusUnderInvestigation
   491  	case "will not fix":
   492  		return types.StatusWillNotFix
   493  	case "out of support scope":
   494  		return types.StatusEndOfLife
   495  	}
   496  	return types.StatusUnknown
   497  }