feat: add k8s cve collector (#239)

This commit is contained in:
chenk 2023-09-27 16:18:16 +03:00 committed by GitHub
parent 26dae1a5f7
commit b98364d3e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1639 additions and 6 deletions

41
.github/workflows/k8s.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Update vuln-list-k8s repo
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
jobs:
update:
name: Update vuln-list-k8s
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
VULN_LIST_DIR: "vuln-list-k8s"
REPOSITORY_OWNER: ${{ github.repository_owner }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
- name: Check out vuln-list-k8s repo
uses: actions/checkout@v4
with:
repository: ${{ env.REPOSITORY_OWNER }}/${{ env.VULN_LIST_DIR }}
token: ${{ secrets.ACCESS_TOKEN }}
path: ${{ env.VULN_LIST_DIR }}
- name: Setup github user email and name
run: |
git config --global user.email "action@github.com"
git config --global user.name "GitHub Action"
- name: Compile vuln-list-update
run: go build -o vuln-list-update .
- if: always()
name: K8s official vulnerability advisory
run: ./create_pr.sh k8s

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ vuln-list-update
# MacOs
.DS_Store
.vscode

46
create_pr.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
TARGET=$1
if [ -z "$TARGET" ]; then
echo "target required"
exit 1
fi
./vuln-list-update -vuln-list-dir "$VULN_LIST_DIR" -target "$TARGET"
cd "$VULN_LIST_DIR" || exit 1
if [[ -n $(git status --porcelain) ]]; then
# List changed files
CHANGED_FILES=$(git ls-files . --exclude-standard --others | grep "CVE")
REPO="$REPOSITORY_OWNER/$VULN_LIST_DIR"
BASE_BRANCH="main"
# Loop through changed files and create PRs
for FILE in $CHANGED_FILES; do
BRANCH_NAME=$(echo "$FILE" | tr / -)
PR_TITLE="Update $FILE"
PR_BODY="This PR updates $FILE"
# Check if a PR with the same branch name already exists
OPEN_PR_COUNT=$(gh pr list --state open --base $BASE_BRANCH --repo "$REPO" | grep "$FILE" | wc -l)
if [ "$OPEN_PR_COUNT" != 0 ]; then
echo "PR for $FILE already exists, skipping."
continue
fi
# Create a new branch and push it
git checkout -b "$BRANCH_NAME"
echo "$FILE"
git add "$FILE"
git commit -m "Update $FILE"
git push origin "$BRANCH_NAME" --force
# Create a new pull request using gh
gh pr create --base "$BASE_BRANCH" --head "$BRANCH_NAME" --title "$PR_TITLE" --body "$PR_BODY" --repo "$REPO"
git checkout $BASE_BRANCH
done
fi

4
go.mod
View File

@ -4,9 +4,12 @@ go 1.20
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492
github.com/araddon/dateparse v0.0.0-20190426192744-0d74ffceef83
github.com/cheggaaa/pb v2.0.7+incompatible
github.com/cheggaaa/pb/v3 v3.1.4
github.com/goark/go-cvss v1.6.6
github.com/hashicorp/go-getter v1.7.2
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-jsonpointer v0.0.1
@ -35,6 +38,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/goark/errs v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect

12
go.sum
View File

@ -198,6 +198,10 @@ github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAU
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4=
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg=
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M=
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU=
github.com/araddon/dateparse v0.0.0-20190426192744-0d74ffceef83 h1:ukTLOeMC0aVxbJWVg6hOsVJ0VPIo8w++PbNsze/pqF8=
github.com/araddon/dateparse v0.0.0-20190426192744-0d74ffceef83/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo=
@ -225,6 +229,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -247,6 +252,10 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/goark/errs v1.1.0 h1:FKnyw4LVyRADIjM8Nj0Up6r0/y5cfADvZAd1E+tthXE=
github.com/goark/errs v1.1.0/go.mod h1:TtaPEoadm2mzqzfXdkkfpN2xuniCFm2q4JH+c1qzaqw=
github.com/goark/go-cvss v1.6.6 h1:WJFuIWqmAw1Ilb9USv0vuX+nYzOWJp8lIujseJ/y3sU=
github.com/goark/go-cvss v1.6.6/go.mod h1:H3qbfUSUlV7XtA3EwWNunvXz6OySwWHOuO+R6ZPMQPI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -407,10 +416,12 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0 h1:T9uus1QvcPgeLShS30YOnnzk3r9Vvygp45muhlrufgY=
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
@ -434,6 +445,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

280
k8s/k8s.go Normal file
View File

@ -0,0 +1,280 @@
package k8s
import (
"encoding/json"
"errors"
"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"
vulnListRepoTarBall = "https://api.github.com/repos/aquasecurity/vuln-list-k8s/tarball"
mitreURL = "https://cveawg.mitre.org/api/cve"
cveList = "https://www.cve.org/"
)
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 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 := getExitingCvesToModifiedMap()
if err != nil {
return nil, err
}
return 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 Update() error {
if err := update(); err != nil {
return xerrors.Errorf("error in k8s update: %w", err)
}
return nil
}
func update() error {
log.Printf("Fetching k8s cves")
k8sdb, err := Collect()
if err != nil {
return err
}
for _, cve := range k8sdb.Cves {
if err = uu.Write(filepath.Join(uu.VulnListDir(), "upstream", fmt.Sprintf("%s.json", cve.ID)), cve); err != nil {
return xerrors.Errorf("failed to save k8s CVE detail: %w", err)
}
}
return nil
}
func 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, 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}, {Url: item.ExternalURL}},
})
}
}
err := validateCvesData(fullVulnerabilities)
if err != nil {
return nil, err
}
return &VulnDB{fullVulnerabilities}, nil
}
func getAffectedEvents(v []*Version, p string, cvss Cvssv3) []osv.Affected {
affected := make([]osv.Affected, 0)
for _, av := range v {
if len(av.Introduced) == 0 {
continue
}
if av.Introduced == "0.0.0" {
av.Introduced = "0"
}
events := make([]osv.Event, 0)
ranges := make([]osv.Range, 0)
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})
}
ranges = append(ranges, osv.Range{
Events: events,
})
affected = append(affected, osv.Affected{Ranges: ranges, Package: osv.Package{Name: p, Ecosystem: "kubernetes"}, Severities: []osv.Severity{{Type: cvss.Type, Score: cvss.Vector}}})
}
return affected
}
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 validateCvesData(cves []*osv.OSV) error {
var result error
for _, cve := range cves {
if len(cve.ID) == 0 {
result = errors.Join(result, fmt.Errorf("\nid is mssing on cve #%s", cve.ID))
}
if len(cve.Published) == 0 {
result = errors.Join(result, fmt.Errorf("\nCreatedAt is mssing on cve #%s", cve.ID))
}
if len(cve.Summary) == 0 {
result = errors.Join(result, fmt.Errorf("\nSummary is mssing on cve #%s", cve.ID))
}
for _, af := range cve.Affected {
if len(strings.TrimPrefix(af.Package.Name, upstreamOrgByName(af.Package.Name))) == 0 {
result = errors.Join(result, fmt.Errorf("\nComponent is mssing on cve #%s", cve.ID))
}
}
if len(cve.Details) == 0 {
result = errors.Join(result, fmt.Errorf("\nDescription is mssing on cve #%s", cve.ID))
}
if len(cve.Affected) == 0 {
result = errors.Join(result, fmt.Errorf("\nAffected Version is missing on cve #%s", cve.ID))
}
if len(cve.Affected) > 0 {
for _, v := range cve.Affected {
for _, s := range v.Severities {
if len(s.Type) == 0 {
result = errors.Join(result, fmt.Errorf("\nVector is mssing on cve #%s", cve.ID))
}
}
for _, r := range v.Ranges {
for i := 1; i < len(r.Events); i++ {
if len(r.Events[i-1].Introduced) == 0 {
result = errors.Join(result, fmt.Errorf("\nAffectedVersion Introduced is missing from cve #%s", cve.ID))
}
if len(r.Events[i].Fixed) == 0 && len(r.Events[i].LastAffected) == 0 {
result = errors.Join(result, fmt.Errorf("\nAffectedVersion Fixed and LastAffected are missing from cve #%s", cve.ID))
}
}
}
}
}
if len(cve.References) == 0 {
result = errors.Join(result, fmt.Errorf("\nUrls is mssing on cve #%s", cve.ID))
}
}
return result
}
func cveMissingImportantData(vulnerability *Cve) bool {
return len(vulnerability.versions) == 0 ||
len(vulnerability.Package) == 0 ||
len(vulnerability.CvssV3.Vector) == 0
}
// getExitingCvesToModifiedMap get the existing cves from vuln-list-k8s repo and map it to cve id and last updated
func getExitingCvesToModifiedMap() (map[string]string, error) {
response, err := http.Get(vulnListRepoTarBall)
if err != nil {
return nil, err
}
defer response.Body.Close()
return cveIDToModifiedMap(utils.VulnListDir())
}
// 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
}
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
}

62
k8s/k8s_test.go Normal file
View File

@ -0,0 +1,62 @@
package k8s
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ParseVulneDB(t *testing.T) {
b, err := os.ReadFile("./testdata/k8s-db.json")
assert.NoError(t, err)
var bi CVE
err = json.Unmarshal(b, &bi)
assert.NoError(t, err)
kvd, err := ParseVulnDBData(bi, map[string]string{})
assert.NoError(t, err)
err = validateCvesData(kvd.Cves)
assert.NoError(t, err)
gotVulnDB, err := json.Marshal(kvd.Cves)
assert.NoError(t, err)
wantVulnDB, err := os.ReadFile("./testdata/expected-vulndb.json")
assert.NoError(t, err)
assert.Equal(t, string(wantVulnDB), string(gotVulnDB))
}
func Test_cveIDToModifiedMap(t *testing.T) {
t.Run("valid folder with cve", func(t *testing.T) {
tm, err := cveIDToModifiedMap("./testdata/happy/upstream")
assert.NoError(t, err)
assert.Equal(t, tm["CVE-2018-1002102"], "2018-11-26T11:07:36Z")
})
t.Run("non existing folder", func(t *testing.T) {
tm, err := cveIDToModifiedMap("./test")
assert.NoError(t, err)
assert.True(t, len(tm) == 0)
})
}
func Test_OlderCve(t *testing.T) {
tests := []struct {
Name string
currentCveID string
currentModified string
cveModified map[string]string
want bool
}{
{Name: "match CVE but older Modified", currentCveID: "CVE-2018-1002102", currentModified: "2018-11-25T11:07:36Z", cveModified: map[string]string{"CVE-2018-1002102": "2018-11-26T11:07:36Z"}, want: true},
{Name: "match CVE but older not Modified", currentCveID: "CVE-2018-1002102", currentModified: "2018-11-27T11:07:36Z", cveModified: map[string]string{"CVE-2018-1002102": "2018-11-26T11:07:36Z"}, want: false},
{Name: "match CVE same time", currentCveID: "CVE-2018-1002102", currentModified: "2018-11-27T11:07:36Z", cveModified: map[string]string{"CVE-2018-1002102": "2018-11-27T11:07:36Z"}, want: true},
{Name: "no existing cve", currentCveID: "CVE-2018-1002102", currentModified: "2018-11-27T11:07:36Z", cveModified: map[string]string{}, want: false},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
got := olderCve(tt.currentCveID, tt.currentModified, tt.cveModified)
assert.Equal(t, got, tt.want)
})
}
}

286
k8s/mitre.go Normal file
View File

@ -0,0 +1,286 @@
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 string, 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
var vectorType string
for _, metric := range cve.Containers.Cna.Metrics {
vectorString = metric.CvssV3_0.VectorString
vectorType = "CVSS_V3_0"
if len(vectorString) == 0 {
vectorString = metric.CvssV3_1.VectorString
vectorType = "CVSS_V3_1"
}
}
return vectorString, vectorType
}
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
}

75
k8s/mitre_test.go Normal file
View File

@ -0,0 +1,75 @@
package k8s
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSanitizedVersion(t *testing.T) {
tests := []struct {
Name string
Version *MitreVersion
Want *MitreVersion
}{
{Name: "validate n/a version ", Version: &MitreVersion{Version: "n/a"}, Want: &MitreVersion{Version: "n/a"}},
{Name: "validate unspecified version ", Version: &MitreVersion{Version: "unspecified"}, Want: &MitreVersion{Version: "unspecified"}},
{Name: "validate less equal sign and version", Version: &MitreVersion{LessThanOrEqual: "<=", Version: "1.3.4"}, Want: &MitreVersion{Version: "1.3.4", LessThanOrEqual: "1.3.4"}},
{Name: "validate less sign in version", Version: &MitreVersion{Version: "< 1.3.4"}, Want: &MitreVersion{Version: "< 1.3.4", LessThan: "1.3.4"}},
{Name: "validate prior to then sign in version", Version: &MitreVersion{Version: "prior to 1.3.4"}, Want: &MitreVersion{Version: "prior to 1.3.4", LessThan: "1.3.4"}},
{Name: "validate prior to with minor in version", Version: &MitreVersion{Version: "prior to 1.3"}, Want: &MitreVersion{Version: "1.3.0", LessThan: "1.3.0"}},
{Name: "validate less with astrix", Version: &MitreVersion{LessThan: "1.3*"}, Want: &MitreVersion{Version: "1.3"}},
{Name: "validate less with x", Version: &MitreVersion{Version: "1.3.x"}, Want: &MitreVersion{Version: "1.3"}},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
got, _ := sanitizedVersions(tt.Version)
assert.Equal(t, got, tt.Want)
})
}
}
func TestMergedVersion(t *testing.T) {
tests := []struct {
Name string
affectedVersions []*Version
WantAffectedVersions []*Version
}{
{Name: "merge regular version", affectedVersions: []*Version{
{Introduced: "1.2"},
{Introduced: "1.3"},
{Introduced: "1.4.1", LastAffected: "1.4.6"},
}, WantAffectedVersions: []*Version{
{Introduced: "1.2.0", LastAffected: "1.4.6"}},
},
{Name: "merge mixed version", affectedVersions: []*Version{
{Introduced: "1.3"},
{Introduced: "1.4"},
{Introduced: "1.5"},
{Introduced: "1.6"},
{Introduced: "1.7.0", Fixed: "1.7.14"},
{Introduced: "1.8.0", Fixed: "1.8.9"},
}, WantAffectedVersions: []*Version{
{Introduced: "1.3.0", Fixed: "1.7.14"},
{Introduced: "1.8.0", Fixed: "1.8.9"}},
},
{Name: "merge all minor version", affectedVersions: []*Version{
{Introduced: "1.3"},
{Introduced: "1.4"},
{Introduced: "1.5"},
{Introduced: "1.6"},
{Introduced: "1.7"},
}, WantAffectedVersions: []*Version{
{Introduced: "1.3.0", Fixed: "1.8.0"},
}},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
gotLastAffected, err := mergeVersionRange(tt.affectedVersions)
assert.NoError(t, err)
assert.Equal(t, gotLastAffected, tt.WantAffectedVersions)
})
}
}

140
k8s/parser.go Normal file
View File

@ -0,0 +1,140 @@
package k8s
import (
"fmt"
"strings"
version "github.com/aquasecurity/go-version/pkg/version"
)
var (
UpstreamOrgName = map[string]string{
"k8s.io": "controller-manager,kubelet,apiserver,kubectl,kubernetes,kube-scheduler,kube-proxy",
"sigs.k8s.io": "secrets-store-csi-driver",
}
UpstreamRepoName = map[string]string{
"kube-controller-manager": "controller-manager",
"kubelet": "kubelet",
"kube-apiserver": "apiserver",
"kubectl": "kubectl",
"kubernetes": "kubernetes",
"kube-scheduler": "kube-scheduler",
"kube-proxy": "kube-proxy",
"api server": "apiserver",
"secrets-store-csi-driver": "secrets-store-csi-driver",
}
)
func trimString(s string, trimValues []string) string {
for _, v := range trimValues {
s = strings.Trim(s, v)
}
return strings.TrimSpace(s)
}
func updateVersions(to, introduce string) (string, string) {
// Example: https://cveawg.mitre.org/api/cve/CVE-2023-2878
if introduce == "0" {
return introduce, to
}
// Example: https://cveawg.mitre.org/api/cve/CVE-2019-11243
if minorVersion(introduce) {
return introduce + ".0", to
}
// Example: https://cveawg.mitre.org/api/cve/CVE-2019-1002100
if lIndex := strings.LastIndex(to, "."); lIndex != -1 {
return strings.TrimSpace(fmt.Sprintf("%s.%s", to[:lIndex], "0")), to
}
return introduce, to
}
func extractRangeVersions(introduce string) (string, string) {
// Example https://cveawg.mitre.org/api/cve/CVE-2021-25749
var lastAffected string
validVersion := make([]string, 0)
// clean unwanted strings from versions
versionRangeParts := strings.Split(introduce, " ")
for _, p := range versionRangeParts {
candidate, err := version.Parse(p)
if err != nil {
continue
}
validVersion = append(validVersion, candidate.String())
}
if len(validVersion) >= 1 {
introduce = strings.TrimSpace(validVersion[0])
}
if len(validVersion) == 2 {
lastAffected = strings.TrimSpace(validVersion[1])
}
return introduce, lastAffected
}
func getMultiIDs(id string) []string {
var idsList []string
if strings.Contains(id, ",") {
idParts := strings.Split(id, ",")
for _, p := range idParts {
if strings.HasPrefix(strings.TrimSpace(p), "CVE-") {
idsList = append(idsList, strings.TrimSpace(p))
}
}
return idsList
}
return []string{id}
}
func upstreamOrgByName(component string) string {
for key, components := range UpstreamOrgName {
for _, c := range strings.Split(components, ",") {
if strings.TrimSpace(c) == strings.ToLower(component) {
return key
}
}
}
return ""
}
func upstreamRepoByName(component string) string {
if val, ok := UpstreamRepoName[component]; ok {
return val
}
return component
}
func getComponentFromDescription(descriptions string, currentComponent string) string {
if strings.ToLower(currentComponent) != "kubernetes" {
return currentComponent
}
var compName string
var compCounter int
var kubeCtlVersionFound bool
CoreComponentsNaming := []string{"kube-controller-manager", "kubelet", "kube-apiserver", "kubectl", "kube-scheduler", "kube-proxy", "secrets-store-csi-driver", "api server"}
for _, key := range CoreComponentsNaming {
if strings.Contains(strings.ToLower(descriptions), key) {
c := strings.Count(strings.ToLower(descriptions), key)
if UpstreamRepoName[key] == compName {
compCounter = compCounter + c
}
if strings.Contains(strings.ToLower(descriptions), "kubectl version") {
kubeCtlVersionFound = true
}
if c > compCounter {
compCounter = c
compName = UpstreamRepoName[key]
}
}
}
// in case found kubectl in env description and only one component found or no component found then fallback to k8s.io/kubernetes component
if len(compName) == 0 || (kubeCtlVersionFound && compName == "kubectl" && compCounter == 1) {
return currentComponent
}
return compName
}
// MinorVersion returns true if version is minor version 1.1 or 2.2 and etc
func minorVersion(version string) bool {
return strings.Count(version, ".") == 1
}

47
k8s/parser_test.go Normal file
View File

@ -0,0 +1,47 @@
package k8s
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ExtractVersions(t *testing.T) {
tests := []struct {
name string
version string
less string
wantIntroduce string
wantLastAffected string
}{
{name: "range less with minor", version: "1.2", less: "1.2.5", wantIntroduce: "1.2.0", wantLastAffected: "1.2.5"},
{name: "range less", version: "", less: "1.2.5", wantIntroduce: "1.2.0", wantLastAffected: "1.2.5"},
{name: "range lessThen", version: "", less: "1.2.5", wantIntroduce: "1.2.0", wantLastAffected: "1.2.5"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIntoduce, gotLastAffected := updateVersions(tt.less, tt.version)
assert.Equal(t, gotIntoduce, tt.wantIntroduce)
assert.Equal(t, gotLastAffected, tt.wantLastAffected)
})
}
}
func Test_ExtractRangeVersions(t *testing.T) {
tests := []struct {
name string
version string
wantIntroduce string
wantLastAffected string
}{
{name: "range versions", version: "1.2.3 - 1.2.5", wantIntroduce: "1.2.3", wantLastAffected: "1.2.5"},
{name: "single versions", version: "1.2.5", wantIntroduce: "1.2.5", wantLastAffected: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIntoduce, gotLastAffected := extractRangeVersions(tt.version)
assert.Equal(t, gotIntoduce, tt.wantIntroduce)
assert.Equal(t, gotLastAffected, tt.wantLastAffected)
})
}
}

1
k8s/testdata/expected-vulndb.json vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,91 @@
{
"id": "CVE-2018-1002102",
"modified": "2018-11-26T11:07:36Z",
"published": "2018-11-26T11:07:36Z",
"summary": "proxy request handling in kube-apiserver can leave vulnerable TCP connections",
"details": "In all Kubernetes versions prior to v1.10.11, v1.11.5, and v1.12.3, incorrect handling of error responses to proxied upgrade requests in the kube-apiserver
allowed specially crafted requests to establish a connection through the Kubernetes API server to backend servers, then send arbitrary requests over the same connection
directly to the backend, authenticated with the Kubernetes API server's TLS credentials used to establish the backend connection.",
"affected": [
{
"package": {
"ecosystem": "kubernetes",
"name": "k8s.io/apiserver"
},
"severity": [
{
"type": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"score": "9.8"
}
],
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "1.10.11"
}
]
}
]
},
{
"package": {
"ecosystem": "kubernetes",
"name": "k8s.io/apiserver"
},
"severity": [
{
"type": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"score": "9.8"
}
],
"ranges": [
{
"events": [
{
"introduced": "1.11.0"
},
{
"fixed": "1.11.5"
}
]
}
]
},
{
"package": {
"ecosystem": "kubernetes",
"name": "k8s.io/apiserver"
},
"severity": [
{
"type": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"score": "9.8"
}
],
"ranges": [
{
"events": [
{
"introduced": "1.12.0"
},
{
"fixed": "1.12.3"
}
]
}
]
}
],
"references": [
{
"url": "https://github.com/kubernetes/kubernetes/issues/71411"
},
{
"url": "https://www.cve.org/cverecord?id=CVE-2018-1002105"
}
]
}

535
k8s/testdata/k8s-db.json vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ import (
"github.com/aquasecurity/vuln-list-update/debian/tracker"
"github.com/aquasecurity/vuln-list-update/ghsa"
"github.com/aquasecurity/vuln-list-update/glad"
"github.com/aquasecurity/vuln-list-update/k8s"
"github.com/aquasecurity/vuln-list-update/kevc"
"github.com/aquasecurity/vuln-list-update/mariner"
"github.com/aquasecurity/vuln-list-update/nvd"
@ -38,7 +39,7 @@ import (
var (
target = flag.String("target", "", "update target (nvd, alpine, alpine-unfixed, redhat, redhat-oval, "+
"debian, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, mariner, kevc, wolfi, chainguard)")
"debian, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, mariner, kevc, wolfi, chainguard, k8s)")
vulnListDir = flag.String("vuln-list-dir", "", "vuln-list dir")
targetUri = flag.String("target-uri", "", "alternative repository URI (only glad)")
targetBranch = flag.String("target-branch", "", "alternative repository branch (only glad)")
@ -171,6 +172,10 @@ func run() error {
if err := cu.Update(); err != nil {
return xerrors.Errorf("Chainguard update error: %w", err)
}
case "k8s":
if err := k8s.Update(); err != nil {
return xerrors.Errorf("Chainguard update error: %w", err)
}
default:
return xerrors.New("unknown target")
}

View File

@ -2,12 +2,19 @@ package osv
type Affected struct {
Package Package `json:"package,omitempty"`
Severities []Severity `json:"severity,omitempty"`
Ranges []Range `json:"ranges,omitempty"`
Versions []string `json:"versions,omitempty"`
Ecosystem interface{} `json:"ecosystem_specific,omitempty"` //The meaning of the values within the object is entirely defined by the ecosystem
Database interface{} `json:"database_specific,omitempty"` //The meaning of the values within the object is entirely defined by the database
}
type Severity struct {
Type string `json:"type"`
Score string `json:"score"`
}
type Package struct {
Ecosystem string `json:"ecosystem,omitempty"`
Name string `json:"name,omitempty"`