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

     1  package debian
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	bolt "go.etcd.io/bbolt"
    13  	"golang.org/x/xerrors"
    14  
    15  	debver "github.com/khulnasoft-lab/go-deb-version"
    16  	"github.com/khulnasoft-lab/tunnel-db/pkg/db"
    17  	"github.com/khulnasoft-lab/tunnel-db/pkg/types"
    18  	"github.com/khulnasoft-lab/tunnel-db/pkg/utils"
    19  	ustrings "github.com/khulnasoft-lab/tunnel-db/pkg/utils/strings"
    20  	"github.com/khulnasoft-lab/tunnel-db/pkg/vulnsrc/vulnerability"
    21  )
    22  
    23  const (
    24  	debianDir = "vuln-list-debian"
    25  
    26  	// Type
    27  	packageType = "package"
    28  	xrefType    = "xref"
    29  
    30  	// File or directory to parse
    31  	distributionsFile = "distributions.json"
    32  	sourcesDir        = "source"
    33  	updateSourcesDir  = "updates-source"
    34  	cveDir            = "CVE"
    35  	dlaDir            = "DLA"
    36  	dsaDir            = "DSA"
    37  
    38  	// e.g. debian 8
    39  	platformFormat = "debian %s"
    40  )
    41  
    42  var (
    43  	// NOTE: "removed" should not be marked as "not-affected".
    44  	// ref. https://security-team.debian.org/security_tracker.html#removed-packages
    45  	skipStatuses = []string{
    46  		"not-affected",
    47  		"undetermined",
    48  	}
    49  
    50  	source = types.DataSource{
    51  		ID:   vulnerability.Debian,
    52  		Name: "Debian Security Tracker",
    53  		URL:  "https://salsa.debian.org/security-tracker-team/security-tracker",
    54  	}
    55  )
    56  
    57  type Option func(src *VulnSrc)
    58  
    59  func WithCustomPut(put db.CustomPut) Option {
    60  	return func(src *VulnSrc) {
    61  		src.put = put
    62  	}
    63  }
    64  
    65  type VulnSrc struct {
    66  	put db.CustomPut
    67  	dbc db.Operation
    68  
    69  	// Hold a map of codenames and major versions from distributions.json
    70  	// e.g. "buster" => "10"
    71  	distributions map[string]string
    72  
    73  	// Hold vulnerability details per vulnerability id
    74  	// e.g. {"CVE-2021-33560": "Libgcrypt before 1.8.8 and 1.9.x before 1.9.3 mishandles ElGamal encry ..."}
    75  	details map[string]VulnerabilityDetail
    76  
    77  	// Hold the latest versions of each codename in Sources.json
    78  	// e.g. {"buster", "bash"} => "5.0-4"
    79  	pkgVersions map[bucket]string
    80  
    81  	// Hold the fixed versions of vulnerabilities in sid
    82  	// e.g. {"putty", "CVE-2021-36367"} => "0.75-3" // fixed vulnerability
    83  	//      {"ndpi",  "CVE-2021-36082"} => ""       // unfixed vulnerability
    84  	sidFixedVersions map[bucket]string
    85  
    86  	// Hold debian advisories
    87  	// e.g. {"buster", "connman", "CVE-2021-33833"} => {"FixedVersion": 1.36-2.1~deb10u2, ...}
    88  	bktAdvisories map[bucket]Advisory
    89  
    90  	// Hold not-affected versions
    91  	// e.g. {"buster", "linux", "CVE-2021-3739"} => {}
    92  	notAffected map[bucket]struct{}
    93  }
    94  
    95  func NewVulnSrc(opts ...Option) VulnSrc {
    96  	src := VulnSrc{
    97  		put:              defaultPut,
    98  		dbc:              db.Config{},
    99  		distributions:    map[string]string{},
   100  		details:          map[string]VulnerabilityDetail{},
   101  		pkgVersions:      map[bucket]string{},
   102  		sidFixedVersions: map[bucket]string{},
   103  		bktAdvisories:    map[bucket]Advisory{},
   104  		notAffected:      map[bucket]struct{}{},
   105  	}
   106  
   107  	for _, opt := range opts {
   108  		opt(&src)
   109  	}
   110  
   111  	return src
   112  }
   113  
   114  func (vs VulnSrc) Name() types.SourceID {
   115  	return source.ID
   116  }
   117  
   118  func (vs VulnSrc) Update(dir string) error {
   119  	if err := vs.parse(dir); err != nil {
   120  		return xerrors.Errorf("parse error: %w", err)
   121  	}
   122  
   123  	if err := vs.save(); err != nil {
   124  		return xerrors.Errorf("save error: %w", err)
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  func (vs VulnSrc) parse(dir string) error {
   131  	rootDir := filepath.Join(dir, debianDir, "tracker")
   132  
   133  	// Parse distributions.json
   134  	if err := vs.parseDistributions(rootDir); err != nil {
   135  		return xerrors.Errorf("distributions error: %w", err)
   136  	}
   137  
   138  	// Parse source/**.json
   139  	if err := vs.parseSources(filepath.Join(rootDir, sourcesDir)); err != nil {
   140  		return xerrors.Errorf("source parse error: %w", err)
   141  	}
   142  
   143  	// Parse updates-source/**.json
   144  	if err := vs.parseSources(filepath.Join(rootDir, updateSourcesDir)); err != nil {
   145  		return xerrors.Errorf("updates-source parse error: %w", err)
   146  	}
   147  
   148  	// Parse CVE/*.json
   149  	if err := vs.parseCVE(rootDir); err != nil {
   150  		return xerrors.Errorf("CVE error: %w", err)
   151  	}
   152  
   153  	// Parse DLA/*.json
   154  	if err := vs.parseDLA(rootDir); err != nil {
   155  		return xerrors.Errorf("DLA error: %w", err)
   156  	}
   157  
   158  	// Parse DSA/*.json
   159  	if err := vs.parseDSA(rootDir); err != nil {
   160  		return xerrors.Errorf("DSA error: %w", err)
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  func (vs VulnSrc) parseBug(dir string, fn func(bug) error) error {
   167  	err := utils.FileWalk(dir, func(r io.Reader, path string) error {
   168  		var bg bug
   169  		if err := json.NewDecoder(r).Decode(&bg); err != nil {
   170  			return xerrors.Errorf("json decode error: %w", err)
   171  		}
   172  
   173  		if err := fn(bg); err != nil {
   174  			return xerrors.Errorf("parse debian bug error: %w", err)
   175  		}
   176  		return nil
   177  	})
   178  
   179  	if err != nil {
   180  		return xerrors.Errorf("walk error: %w", err)
   181  	}
   182  	return nil
   183  }
   184  
   185  func (vs VulnSrc) parseCVE(dir string) error {
   186  	log.Println("  Parsing CVE JSON files...")
   187  	err := vs.parseBug(filepath.Join(dir, cveDir), func(bug bug) error {
   188  		// Hold severities per the packages
   189  		severities := map[string]string{}
   190  		cveID := bug.Header.ID
   191  		vs.details[cveID] = VulnerabilityDetail{
   192  			Description: strings.Trim(bug.Header.Description, "()"),
   193  		}
   194  
   195  		for _, ann := range bug.Annotations {
   196  			if ann.Type != packageType {
   197  				continue
   198  			}
   199  
   200  			bkt := bucket{
   201  				codeName: ann.Release, // It will be empty in the case of sid.
   202  				pkgName:  ann.Package,
   203  				vulnID:   cveID,
   204  			}
   205  
   206  			// Skip not-affected, removed or undetermined advisories
   207  			if ustrings.InSlice(ann.Kind, skipStatuses) {
   208  				vs.notAffected[bkt] = struct{}{}
   209  				continue
   210  			}
   211  
   212  			// For sid
   213  			if ann.Release == "" {
   214  				sidBkt := bucket{
   215  					pkgName: ann.Package,
   216  					vulnID:  cveID,
   217  				}
   218  				if ann.Severity != "" {
   219  					severities[ann.Package] = ann.Severity
   220  					sidBkt.severity = ann.Severity
   221  				}
   222  
   223  				vs.sidFixedVersions[sidBkt] = ann.Version // it may be empty for unfixed vulnerabilities
   224  
   225  				continue
   226  			}
   227  
   228  			advisory := Advisory{
   229  				FixedVersion: ann.Version, // It might be empty because of no-dsa.
   230  				Severity:     severities[ann.Package],
   231  			}
   232  
   233  			if ann.Version == "" {
   234  				// Populate State only when FixedVersion is empty.
   235  				// e.g. no-dsa
   236  				advisory.State = ann.Kind
   237  			}
   238  
   239  			// This advisory might be overwritten by DLA/DSA.
   240  			vs.bktAdvisories[bkt] = advisory
   241  		}
   242  
   243  		return nil
   244  	})
   245  	if err != nil {
   246  		return xerrors.Errorf("CVE parse error: %w", err)
   247  	}
   248  	return nil
   249  }
   250  
   251  func (vs VulnSrc) parseDLA(dir string) error {
   252  	log.Println("  Parsing DLA JSON files...")
   253  	if err := vs.parseAdvisory(filepath.Join(dir, dlaDir)); err != nil {
   254  		return xerrors.Errorf("DLA parse error: %w", err)
   255  	}
   256  	return nil
   257  }
   258  
   259  func (vs VulnSrc) parseDSA(dir string) error {
   260  	log.Println("  Parsing DSA JSON files...")
   261  	if err := vs.parseAdvisory(filepath.Join(dir, dsaDir)); err != nil {
   262  		return xerrors.Errorf("DSA parse error: %w", err)
   263  	}
   264  	return nil
   265  }
   266  
   267  func (vs VulnSrc) parseAdvisory(dir string) error {
   268  	return vs.parseBug(dir, func(bug bug) error {
   269  		var cveIDs []string
   270  		advisoryID := bug.Header.ID
   271  		vs.details[advisoryID] = VulnerabilityDetail{
   272  			Description: strings.Trim(bug.Header.Description, "()"),
   273  		}
   274  
   275  		for _, ann := range bug.Annotations {
   276  			// DLA/DSA is associated with CVE-IDs
   277  			// e.g. "DSA-4931-1" => "{CVE-2021-0089 CVE-2021-26313 CVE-2021-28690 CVE-2021-28692}"
   278  			if ann.Type == xrefType {
   279  				cveIDs = ann.Bugs
   280  				continue
   281  			} else if ann.Type != packageType {
   282  				continue
   283  			}
   284  
   285  			// Some advisories don't have any CVE-IDs
   286  			// e.g. https://security-tracker.debian.org/tracker/DSA-3714-1
   287  			vulnIDs := cveIDs
   288  			if len(vulnIDs) == 0 {
   289  				// Use DLA-ID or DSA-ID instead of CVE-ID
   290  				vulnIDs = []string{advisoryID}
   291  			}
   292  
   293  			for _, vulnID := range vulnIDs {
   294  				bkt := bucket{
   295  					codeName: ann.Release,
   296  					pkgName:  ann.Package,
   297  					vulnID:   vulnID,
   298  				}
   299  
   300  				// Skip not-affected, removed or undetermined advisories
   301  				if ustrings.InSlice(ann.Kind, skipStatuses) {
   302  					vs.notAffected[bkt] = struct{}{}
   303  					continue
   304  				}
   305  
   306  				adv, ok := vs.bktAdvisories[bkt]
   307  				if ok {
   308  					// If some advisories fix the same CVE-ID, the latest version will be taken.
   309  					// We assume that the first fix was insufficient and the next advisory fixed it correctly.
   310  					res, err := compareVersions(ann.Version, adv.FixedVersion)
   311  					if err != nil {
   312  						return xerrors.Errorf("version error %s: %w", advisoryID, err)
   313  					}
   314  
   315  					// Replace the fixed version with the newer version.
   316  					if res > 0 {
   317  						adv.FixedVersion = ann.Version
   318  						adv.State = "" // State should be empty because this advisory has fixed version, actually.
   319  					}
   320  					adv.VendorIDs = append(adv.VendorIDs, advisoryID)
   321  				} else {
   322  					adv = Advisory{
   323  						FixedVersion: ann.Version,
   324  						VendorIDs:    []string{advisoryID},
   325  					}
   326  				}
   327  
   328  				vs.bktAdvisories[bkt] = adv
   329  			}
   330  		}
   331  
   332  		return nil
   333  	})
   334  }
   335  
   336  func (vs VulnSrc) save() error {
   337  	log.Println("Saving Debian DB")
   338  	err := vs.dbc.BatchUpdate(func(tx *bolt.Tx) error {
   339  		return vs.commit(tx)
   340  	})
   341  	if err != nil {
   342  		return xerrors.Errorf("batch update error: %w", err)
   343  	}
   344  	log.Println("Saved Debian DB")
   345  	return nil
   346  }
   347  
   348  func (vs VulnSrc) commit(tx *bolt.Tx) error {
   349  	// Iterate all pairs of package name and CVE-ID in sid
   350  	for sidBkt, sidVer := range vs.sidFixedVersions {
   351  		pkgName := sidBkt.pkgName
   352  		cveID := sidBkt.vulnID
   353  
   354  		// Skip if the advisory is stated as "not-affected" for all distributions.
   355  		if _, ok := vs.notAffected[bucket{
   356  			pkgName: pkgName,
   357  			vulnID:  cveID,
   358  		}]; ok {
   359  			continue
   360  		}
   361  
   362  		// Iterate all codenames, e.g. buster
   363  		for code := range vs.distributions {
   364  			bkt := bucket{
   365  				codeName: code,
   366  				pkgName:  pkgName,
   367  				vulnID:   cveID,
   368  			}
   369  
   370  			// Skip if the advisory is stated as "not-affected" for the specific distribution.
   371  			if _, ok := vs.notAffected[bkt]; ok {
   372  				continue
   373  			}
   374  
   375  			// Check if the advisory already exists for the codename
   376  			// If yes, it will be inserted into DB later.
   377  			adv, ok := vs.bktAdvisories[bkt]
   378  			if ok && adv.State == "" {
   379  				// "no-dsa" or "postponed" might be wrong, and it may have a fixed version.
   380  				// e.g.
   381  				//  - https://security-tracker.debian.org/tracker/CVE-2020-8631 (buster no-dsa is wrong)
   382  				//  - https://security-tracker.debian.org/tracker/CVE-2020-25670 (bullseye postponed is wrong)
   383  				continue
   384  			}
   385  
   386  			// If no, the fixed version needs to be determined by comparing with the fixed version in sid.
   387  			pkgBkt := bucket{
   388  				codeName: code,
   389  				pkgName:  pkgName,
   390  			}
   391  
   392  			// Get the latest version in the release
   393  			// e.g. {"buster", "bash"} => "5.0-4"
   394  			codeVer, ok := vs.pkgVersions[pkgBkt]
   395  			if !ok {
   396  				continue
   397  			}
   398  
   399  			// Check if the release has the fixed version
   400  			fixed, err := hasFixedVersion(sidVer, codeVer)
   401  			if err != nil {
   402  				return xerrors.Errorf("version error: %w", err)
   403  			}
   404  
   405  			if fixed {
   406  				adv.FixedVersion = sidVer
   407  				adv.State = "" // Overwrite state such as "no-dsa" and "postponed" because it is wrong.
   408  				delete(vs.bktAdvisories, bkt)
   409  			}
   410  
   411  			// Add severity
   412  			adv.Severity = sidBkt.severity
   413  
   414  			bkt.vulnID = cveID
   415  			if err = vs.putAdvisory(tx, bkt, adv); err != nil {
   416  				return xerrors.Errorf("put advisory error: %w", err)
   417  			}
   418  		}
   419  	}
   420  
   421  	// All advisories with codename and fixed version are inserted into DB here.
   422  	for bkt, advisory := range vs.bktAdvisories {
   423  		if err := vs.putAdvisory(tx, bkt, advisory); err != nil {
   424  			return xerrors.Errorf("put advisory error: %w", err)
   425  		}
   426  	}
   427  	return nil
   428  }
   429  
   430  func (vs VulnSrc) putAdvisory(tx *bolt.Tx, bkt bucket, advisory Advisory) error {
   431  	// Convert codename to major version
   432  	// e.g. "buster" => "10"
   433  	majorVersion, ok := vs.distributions[bkt.codeName]
   434  	if !ok {
   435  		// Stale codename such as squeeze and sarge
   436  		return nil
   437  	}
   438  
   439  	// Fill information for the buckets.
   440  	advisory.VulnerabilityID = bkt.vulnID
   441  	advisory.PkgName = bkt.pkgName
   442  	advisory.Platform = fmt.Sprintf(platformFormat, majorVersion)
   443  	advisory.Title = vs.details[bkt.vulnID].Description // The Debian description is short, so we'll use it as a title.
   444  
   445  	if err := vs.put(vs.dbc, tx, advisory); err != nil {
   446  		return xerrors.Errorf("put error: %w", err)
   447  	}
   448  
   449  	return nil
   450  }
   451  
   452  // defaultPut puts the advisory into Tunnel DB, but it can be overwritten.
   453  func defaultPut(dbc db.Operation, tx *bolt.Tx, advisory interface{}) error {
   454  	adv, ok := advisory.(Advisory)
   455  	if !ok {
   456  		return xerrors.New("unknown type")
   457  	}
   458  
   459  	detail := types.Advisory{
   460  		VendorIDs:    adv.VendorIDs,
   461  		Status:       newStatus(adv.State),
   462  		Severity:     severityFromUrgency(adv.Severity),
   463  		FixedVersion: adv.FixedVersion,
   464  	}
   465  
   466  	if err := dbc.PutAdvisoryDetail(tx, adv.VulnerabilityID, adv.PkgName, []string{adv.Platform}, &detail); err != nil {
   467  		return xerrors.Errorf("failed to save Debian advisory: %w", err)
   468  	}
   469  
   470  	vuln := types.VulnerabilityDetail{
   471  		Title: adv.Title,
   472  	}
   473  	if err := dbc.PutVulnerabilityDetail(tx, adv.VulnerabilityID, source.ID, vuln); err != nil {
   474  		return xerrors.Errorf("failed to save Debian vulnerability detail: %w", err)
   475  	}
   476  
   477  	// for optimization
   478  	if err := dbc.PutVulnerabilityID(tx, adv.VulnerabilityID); err != nil {
   479  		return xerrors.Errorf("failed to save the vulnerability ID: %w", err)
   480  	}
   481  
   482  	if err := dbc.PutDataSource(tx, adv.Platform, source); err != nil {
   483  		return xerrors.Errorf("failed to put data source: %w", err)
   484  	}
   485  
   486  	return nil
   487  }
   488  
   489  func (vs VulnSrc) Get(release string, pkgName string) ([]types.Advisory, error) {
   490  	bkt := fmt.Sprintf(platformFormat, release)
   491  	advisories, err := vs.dbc.GetAdvisories(bkt, pkgName)
   492  	if err != nil {
   493  		return nil, xerrors.Errorf("failed to get Debian advisories: %w", err)
   494  	}
   495  
   496  	return advisories, nil
   497  }
   498  
   499  func severityFromUrgency(urgency string) types.Severity {
   500  	switch urgency {
   501  	case "not yet assigned", "end-of-life":
   502  		return types.SeverityUnknown
   503  
   504  	case "unimportant", "low", "low*", "low**":
   505  		return types.SeverityLow
   506  
   507  	case "medium", "medium*", "medium**":
   508  		return types.SeverityMedium
   509  
   510  	case "high", "high*", "high**":
   511  		return types.SeverityHigh
   512  	default:
   513  		return types.SeverityUnknown
   514  	}
   515  }
   516  
   517  func (vs VulnSrc) parseDistributions(rootDir string) error {
   518  	log.Println("  Parsing distributions...")
   519  	f, err := os.Open(filepath.Join(rootDir, distributionsFile))
   520  	if err != nil {
   521  		return xerrors.Errorf("failed to open file: %w", err)
   522  	}
   523  	defer f.Close()
   524  
   525  	// To parse distributions.json
   526  	var parsed map[string]struct {
   527  		MajorVersion string `json:"major-version"`
   528  	}
   529  	if err = json.NewDecoder(f).Decode(&parsed); err != nil {
   530  		return xerrors.Errorf("failed to decode Debian distribution JSON: %w", err)
   531  	}
   532  	for dist, val := range parsed {
   533  		if val.MajorVersion == "" {
   534  			// Empty code refers to sid(development) codeName
   535  			//vs.distributions[dist] = defaultCode
   536  			continue
   537  		}
   538  		vs.distributions[dist] = val.MajorVersion
   539  	}
   540  	return nil
   541  }
   542  
   543  func (vs VulnSrc) parseSources(dir string) error {
   544  	for code := range vs.distributions {
   545  		codePath := filepath.Join(dir, code)
   546  		if ok, _ := utils.Exists(codePath); !ok {
   547  			continue
   548  		}
   549  
   550  		log.Printf("  Parsing %s sources...", code)
   551  		err := utils.FileWalk(codePath, func(r io.Reader, path string) error {
   552  			// To parse Sources.json
   553  			var pkg struct {
   554  				Package []string
   555  				Version []string
   556  			}
   557  			if err := json.NewDecoder(r).Decode(&pkg); err != nil {
   558  				return xerrors.Errorf("failed to decode %s: %w", path, err)
   559  			}
   560  
   561  			if len(pkg.Package) == 0 || len(pkg.Version) == 0 {
   562  				return nil
   563  			}
   564  
   565  			bkt := bucket{
   566  				codeName: code,
   567  				pkgName:  pkg.Package[0],
   568  			}
   569  
   570  			version := pkg.Version[0]
   571  
   572  			// Skip the update when the stored version is greater than the processing version.
   573  			if v, ok := vs.pkgVersions[bkt]; ok {
   574  				res, err := compareVersions(v, version)
   575  				if err != nil {
   576  					return xerrors.Errorf("version comparison error: %w", err)
   577  				}
   578  
   579  				if res >= 0 {
   580  					return nil
   581  				}
   582  			}
   583  
   584  			// Store package name and version per codename
   585  			vs.pkgVersions[bkt] = version
   586  
   587  			return nil
   588  		})
   589  		if err != nil {
   590  			return xerrors.Errorf("filepath walk error: %w", err)
   591  		}
   592  	}
   593  
   594  	return nil
   595  }
   596  
   597  // There are 3 cases when the fixed version of each release is not stated in list files.
   598  //
   599  // Case 1
   600  //
   601  //	  When the latest version in the release is greater than the fixed version in sid,
   602  //	  we can assume that the vulnerability was already fixed at the fixed version.
   603  //	  e.g.
   604  //		latest version (buster) : "5.0-4"
   605  //	    fixed version (sid)     : "5.0-2"
   606  //	    => the vulnerability was fixed at "5.0-2".
   607  //
   608  // Case 2
   609  //
   610  //	  When the latest version in the release less than the fixed version in sid,
   611  //	  it means the vulnerability has not been fixed yet.
   612  //	  e.g.
   613  //		latest version (buster) : "5.0-4"
   614  //	    fixed version (sid)     : "5.0-5"
   615  //	     => the vulnerability hasn't been fixed yet.
   616  //
   617  // Case 3
   618  //
   619  //	  When the fixed version in sid is empty,
   620  //	  it means the vulnerability has not been fixed yet.
   621  //	  e.g.
   622  //		   latest version (buster) : "5.0-4"
   623  //	    fixed version (sid)     : ""
   624  //	     => the vulnerability hasn't been fixed yet.
   625  func hasFixedVersion(sidVer, codeVer string) (bool, error) {
   626  	// No fixed version even in sid
   627  	if sidVer == "" {
   628  		return false, nil
   629  	}
   630  
   631  	res, err := compareVersions(codeVer, sidVer)
   632  	if err != nil {
   633  		return false, xerrors.Errorf("version comparison error")
   634  	}
   635  
   636  	// Greater than or equal
   637  	return res >= 0, nil
   638  }
   639  
   640  func compareVersions(v1, v2 string) (int, error) {
   641  	// v1 or v2 might be empty.
   642  	switch {
   643  	case v1 == "" && v2 == "":
   644  		return 0, nil
   645  	case v1 == "":
   646  		return -1, nil
   647  	case v2 == "":
   648  		return 1, nil
   649  	}
   650  
   651  	ver1, err := debver.NewVersion(v1)
   652  	if err != nil {
   653  		return 0, xerrors.Errorf("version error: %w", err)
   654  	}
   655  
   656  	ver2, err := debver.NewVersion(v2)
   657  	if err != nil {
   658  		return 0, xerrors.Errorf("version error: %w", err)
   659  	}
   660  
   661  	return ver1.Compare(ver2), nil
   662  }
   663  
   664  func newStatus(s string) types.Status {
   665  	switch strings.ToLower(s) {
   666  	// "end-of-life" is considered as vulnerable
   667  	// e.g. https://security-tracker.debian.org/tracker/CVE-2022-1488
   668  	case "no-dsa", "unfixed":
   669  		return types.StatusAffected
   670  	case "ignored":
   671  		return types.StatusWillNotFix
   672  	case "postponed":
   673  		return types.StatusFixDeferred
   674  	case "end-of-life":
   675  		return types.StatusEndOfLife
   676  	}
   677  	return types.StatusUnknown
   678  }