vuln-list-update/k8s/mitre.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

284 lines
7.6 KiB
Go

package k8s
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
version "github.com/aquasecurity/go-version/pkg/version"
)
type MitreCVE struct {
CveMetadata CveMetadata
Containers Containers
}
type Containers struct {
Cna struct {
Affected []struct {
Product string
Vendor string
Versions []*MitreVersion
}
Descriptions []Descriptions
Metrics []struct {
CvssV3_1 struct {
VectorString string
}
CvssV3_0 struct {
VectorString string
}
}
}
}
type MitreVersion struct {
Status string
Version string
LessThanOrEqual string
LessThan string
VersionType string
}
type CveMetadata struct {
CveId string
}
type Descriptions struct {
Lang string
Value string
}
type Cve struct {
Description string
versions []*Version
CvssV3 Cvssv3
Package string
}
type Cvssv3 struct {
Vector string
Type string
}
type Version struct {
Introduced string `json:"introduced,omitempty"`
Fixed string `json:"fixed,omitempty"`
LastAffected string `json:"last_affected,omitempty"`
FixedIndex int `json:"-"`
}
func parseMitreCve(externalURL, mitreURL, cveID string) (*Cve, error) {
if !strings.HasPrefix(externalURL, cveList) {
// if no external url provided, return empty vulnerability to be skipped
return &Cve{}, nil
}
response, err := http.Get(fmt.Sprintf("%s/%s", mitreURL, cveID))
if err != nil {
return nil, err
}
defer response.Body.Close()
var cve MitreCVE
if err = json.NewDecoder(response.Body).Decode(&cve); err != nil {
return nil, err
}
vulnerableVersions := make([]*Version, 0)
var component string
var requireMerge bool
for _, a := range cve.Containers.Cna.Affected {
if len(component) == 0 {
component = strings.ToLower(a.Product)
}
for _, sv := range a.Versions {
if sv.Status != "affected" {
continue
}
var introduce, lastAffected, fixed string
v, ok := sanitizedVersions(sv)
if !ok {
continue
}
switch {
case len(v.LessThan) > 0:
if strings.HasSuffix(v.LessThan, ".0") {
v.Version = "0"
}
introduce, fixed = updateVersions(v.LessThan, v.Version)
case len(v.LessThanOrEqual) > 0:
introduce, lastAffected = updateVersions(v.LessThanOrEqual, v.Version)
case minorVersion(v.Version):
requireMerge = true
introduce = v.Version
default:
introduce, lastAffected = extractRangeVersions(v.Version)
}
vulnerableVersions = append(vulnerableVersions, &Version{
Introduced: introduce,
Fixed: fixed,
LastAffected: lastAffected,
})
}
}
if requireMerge {
vulnerableVersions, err = mergeVersionRange(vulnerableVersions)
if err != nil {
return nil, err
}
}
vector, vectorType := getMetrics(cve)
description := getDescription(cve.Containers.Cna.Descriptions)
return &Cve{
Description: description,
CvssV3: Cvssv3{
Vector: vector,
Type: vectorType,
},
Package: getComponentFromDescription(description, component),
versions: vulnerableVersions,
}, nil
}
func sanitizedVersions(v *MitreVersion) (*MitreVersion, bool) {
if strings.Contains(v.Version, "n/a") && len(v.LessThan) == 0 && len(v.LessThanOrEqual) == 0 {
return v, false
}
if (v.LessThanOrEqual == "unspecified" || v.LessThan == "unspecified") && len(v.Version) > 0 {
return v, false
}
// example https://cveawg.mitre.org/api/cve/CVE-2023-2727
if len(v.LessThanOrEqual) > 0 && v.LessThanOrEqual == "<=" {
v.LessThanOrEqual = v.Version
} else if len(v.LessThan) > 0 {
switch {
// example https://cveawg.mitre.org/api/cve/CVE-2019-11244
case strings.HasSuffix(strings.TrimSpace(v.LessThan), "*"):
v.Version = strings.TrimSpace(strings.ReplaceAll(v.LessThan, "*", ""))
v.LessThan = ""
}
} else if len(v.Version) > 0 {
switch {
// example https://cveawg.mitre.org/api/cve/CVE-2020-8566
case strings.HasPrefix(v.Version, "< "):
v.LessThan = strings.TrimPrefix(v.Version, "< ")
// example https://cveawg.mitre.org/api/cve/CVE-2020-8565
case strings.HasPrefix(v.Version, "<= "):
v.LessThanOrEqual = strings.TrimPrefix(v.Version, "<= ")
//example https://cveawg.mitre.org/api/cve/CVE-2019-11247
case strings.HasPrefix(strings.TrimSpace(v.Version), "prior to"):
priorToVersion := strings.TrimSpace(strings.TrimPrefix(v.Version, "prior to"))
if minorVersion(priorToVersion) {
priorToVersion = priorToVersion + ".0"
v.Version = priorToVersion
}
v.LessThan = priorToVersion
// all version is vulnerable : https://cveawg.mitre.org/api/cve/CVE-2017-1002101
case strings.HasSuffix(strings.TrimSpace(v.Version), ".x"):
v.Version = strings.TrimSpace(strings.ReplaceAll(v.Version, ".x", ""))
}
}
return &MitreVersion{
Version: trimString(v.Version, []string{"v", "V"}),
LessThanOrEqual: trimString(v.LessThanOrEqual, []string{"v", "V"}),
LessThan: trimString(v.LessThan, []string{"v", "V"}),
}, true
}
func getDescription(descriptions []Descriptions) string {
for _, d := range descriptions {
if d.Lang == "en" {
return d.Value
}
}
return ""
}
type byVersion []*Version
func (s byVersion) Len() int {
return len(s)
}
func (s byVersion) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s byVersion) Less(i, j int) bool {
v1, err := version.Parse(s[i].Introduced)
if err != nil {
return false
}
v2, err := version.Parse(s[j].Introduced)
if err != nil {
return false
}
return v1.LessThan(v2)
}
func mergeVersionRange(affectedVersions []*Version) ([]*Version, error) {
// this special handling is made to handle to case of conceutive vulnable minor versions.
// example: vulnerable 1.3, 1.4, 1.5, 1.6 and prior to versions 1.7.14, 1.8.9 will be form as follow:
// Introduced: 1.3.0 Fixed: 1.7.14
// Introduced: 1.8.0 Fixed: 1.8.9
// example: https://cveawg.mitre.org/api/cve/CVE-2019-11249
sort.Sort(byVersion(affectedVersions))
newAffectedVesion := make([]*Version, 0)
minorVersions := make([]*Version, 0)
for _, av := range affectedVersions {
if minorVersion(av.Introduced) {
minorVersions = append(minorVersions, av)
} else if strings.Count(av.Introduced, ".") > 1 && len(minorVersions) > 0 {
newAffectedVesion = append(newAffectedVesion, &Version{
Introduced: fmt.Sprintf("%s.0", minorVersions[0].Introduced),
LastAffected: av.LastAffected,
Fixed: av.Fixed,
})
minorVersions = minorVersions[:0]
continue
}
if len(minorVersions) == 0 {
newAffectedVesion = append(newAffectedVesion, av)
}
}
// this special handling is made to handle to case of consecutive vulnable minor versions, wheen there is no fixed version is provided.
// example: vulnerable 1.3, 1.4, 1.5, 1.6 will be form as follow:
// Introduced: 1.3.0 Fixed: 1.7.0
if len(minorVersions) > 0 {
currentVersion := fmt.Sprintf("%s.0", minorVersions[len(minorVersions)-1].Introduced)
versionParts, err := versionParts(currentVersion)
if err != nil {
return nil, err
}
fixed := fmt.Sprintf("%d.%d.%d", versionParts[0], versionParts[1]+1, versionParts[2])
newAffectedVesion = append(newAffectedVesion, &Version{Introduced: fmt.Sprintf("%s.0", minorVersions[0].Introduced), Fixed: fixed})
}
return newAffectedVesion, nil
}
func getMetrics(cve MitreCVE) (string, string) {
var vectorString string
for _, metric := range cve.Containers.Cna.Metrics {
vectorString = metric.CvssV3_0.VectorString
if len(vectorString) == 0 {
vectorString = metric.CvssV3_1.VectorString
}
}
return vectorString, "CVSS_V3"
}
func versionParts(version string) ([]int, error) {
parts := strings.Split(version, ".")
intParts := make([]int, 0)
for _, p := range parts {
i, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
intParts = append(intParts, i)
}
return intParts, nil
}