vuln-list-update/k8s/k8s.go
DmitriyLewen a948784f3a
test(k8s): remove internet access (#256)
Co-authored-by: chenk <hen.keinan@gmail.com>
2023-11-14 16:25:34 +09:00

265 lines
6.6 KiB
Go

package k8s
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/xerrors"
"github.com/aquasecurity/vuln-list-update/osv"
"github.com/aquasecurity/vuln-list-update/utils"
uu "github.com/aquasecurity/vuln-list-update/utils"
)
const (
k8svulnDBURL = "https://kubernetes.io/docs/reference/issues-security/official-cve-feed/index.json"
mitreURL = "https://cveawg.mitre.org/api/cve"
cveList = "https://www.cve.org/"
upstreamFolder = "upstream"
)
type options struct {
mitreURL string
}
type option func(*options)
func WithMitreURL(mitreURL string) option {
return func(opts *options) {
opts.mitreURL = mitreURL
}
}
type Updater struct {
*options
}
func NewUpdater(opts ...option) Updater {
o := &options{
mitreURL: mitreURL,
}
for _, opt := range opts {
opt(o)
}
return Updater{
options: o,
}
}
type VulnDB struct {
Cves []*osv.OSV
}
type CVE struct {
Items []Item `json:"items,omitempty"`
}
type Item struct {
ID string `json:"id,omitempty"`
Summary string `json:"summary,omitempty"`
ContentText string `json:"content_text,omitempty"`
DatePublished string `json:"date_published,omitempty"`
ExternalURL string `json:"external_url,omitempty"`
URL string `json:"url,omitempty"`
}
func (u Updater) Collect() (*VulnDB, error) {
response, err := http.Get(k8svulnDBURL)
if err != nil {
return nil, err
}
defer response.Body.Close()
var db CVE
if err = json.NewDecoder(response.Body).Decode(&db); err != nil {
return nil, err
}
cvesMap, err := cveIDToModifiedMap(filepath.Join(utils.VulnListDir(), upstreamFolder))
if err != nil {
return nil, err
}
return u.ParseVulnDBData(db, cvesMap)
}
const (
// excludeNonCoreComponentsCves exclude cves with missing data or non k8s core components
excludeNonCoreComponentsCves = "CVE-2019-11255,CVE-2020-10749,CVE-2020-8554"
)
func (u Updater) Update() error {
if err := u.update(); err != nil {
return xerrors.Errorf("error in k8s update: %w", err)
}
return nil
}
func (u Updater) update() error {
log.Printf("Fetching k8s cves")
k8sdb, err := u.Collect()
if err != nil {
return err
}
for _, cve := range k8sdb.Cves {
if err = uu.Write(filepath.Join(uu.VulnListDir(), upstreamFolder, fmt.Sprintf("%s.json", cve.ID)), cve); err != nil {
return xerrors.Errorf("failed to save k8s CVE detail: %w", err)
}
}
return nil
}
func (u Updater) ParseVulnDBData(db CVE, cvesMap map[string]string) (*VulnDB, error) {
var fullVulnerabilities []*osv.OSV
for _, item := range db.Items {
for _, cveID := range getMultiIDs(item.ID) {
// check if the current cve is older than the existing one on the vuln-list-k8s repo
if strings.Contains(excludeNonCoreComponentsCves, item.ID) || olderCve(cveID, item.DatePublished, cvesMap) {
continue
}
vulnerability, err := parseMitreCve(item.ExternalURL, u.mitreURL, cveID)
if err != nil {
return nil, err
}
if cveMissingImportantData(vulnerability) {
continue
}
descComponent := getComponentFromDescription(item.ContentText, vulnerability.Package)
fullVulnerabilities = append(fullVulnerabilities, &osv.OSV{
ID: cveID,
Modified: item.DatePublished,
Published: item.DatePublished,
Summary: item.Summary,
Details: vulnerability.Description,
Affected: getAffectedEvents(vulnerability.versions, getComponentName(descComponent, vulnerability.Package), vulnerability.CvssV3),
References: []osv.Reference{
{
Url: item.URL, Type: "ADVISORY",
}, {
Url: item.ExternalURL, Type: "ADVISORY",
},
},
})
}
}
return &VulnDB{fullVulnerabilities}, nil
}
func getAffectedEvents(v []*Version, p string, cvss Cvssv3) []osv.Affected {
events := make([]osv.Event, 0)
for _, av := range v {
if len(av.Introduced) == 0 {
continue
}
if len(av.Introduced) > 0 {
events = append(events, osv.Event{Introduced: av.Introduced})
}
if len(av.Fixed) > 0 {
events = append(events, osv.Event{Fixed: av.Fixed})
} else if len(av.LastAffected) > 0 {
events = append(events, osv.Event{LastAffected: av.LastAffected})
} else if len(av.Introduced) > 0 && len(av.LastAffected) == 0 && len(av.Fixed) == 0 {
events = append(events, osv.Event{LastAffected: av.Introduced})
}
}
return []osv.Affected{
{
Ranges: []osv.Range{
{
Events: events,
Type: "SEMVER",
},
},
Package: osv.Package{
Name: p,
Ecosystem: "kubernetes",
},
Severities: []osv.Severity{
{
Type: cvss.Type,
Score: cvss.Vector,
},
},
},
}
}
func getComponentName(k8sComponent string, mitreComponent string) string {
if len(k8sComponent) == 0 {
k8sComponent = mitreComponent
}
if strings.ToLower(mitreComponent) != "kubernetes" {
k8sComponent = mitreComponent
}
return strings.ToLower(fmt.Sprintf("%s/%s", upstreamOrgByName(k8sComponent), upstreamRepoByName(k8sComponent)))
}
func cveMissingImportantData(vulnerability *Cve) bool {
return len(vulnerability.versions) == 0 ||
len(vulnerability.Package) == 0 ||
len(vulnerability.CvssV3.Vector) == 0
}
// cveIDToModifiedMap read existing cves from vulnList folder and map it to cve id and last updated
func cveIDToModifiedMap(cveFolderPath string) (map[string]string, error) {
mapCveTime := make(map[string]string)
if _, err := os.Stat(cveFolderPath); os.IsNotExist(err) {
return mapCveTime, nil
}
fileInfo, err := os.ReadDir(cveFolderPath)
if err != nil {
return mapCveTime, err
}
for _, file := range fileInfo {
if file.IsDir() {
continue
}
if !(strings.Contains(file.Name(), "CVE-") && strings.HasSuffix(file.Name(), ".json")) {
continue
}
b, err := os.ReadFile(filepath.Join(cveFolderPath, file.Name()))
if err != nil {
return nil, err
}
var cve osv.OSV
err = json.Unmarshal([]byte(strings.ReplaceAll(string(b), "\n", "")), &cve)
if err != nil {
return nil, err
}
mapCveTime[cve.ID] = cve.Modified
}
return mapCveTime, nil
}
// olderCve check if the current cve is older than the existing one
func olderCve(cveID string, currentCVEUpdated string, existCveLastUpdated map[string]string) bool {
if len(existCveLastUpdated) == 0 {
return false
}
var lastUpdated string
var ok bool
if lastUpdated, ok = existCveLastUpdated[cveID]; !ok {
return false
}
existLastUpdated, err := time.Parse(time.RFC3339, lastUpdated)
if err != nil {
return false
}
currentLastUpdated, err := time.Parse(time.RFC3339, currentCVEUpdated)
if err != nil {
return false
}
// check if the current collcted cve is older or same as the existing one
if currentLastUpdated.Before(existLastUpdated) || currentLastUpdated == existLastUpdated {
return true
}
return false
}