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 }