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 }