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 }