Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Red Hat CSAF VEX files to OSV database format #89

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ COPY go.sum go.sum
RUN go mod download

# Copy the go source
COPY cmd/main.go cmd/main.go
COPY cmd/manager/main.go cmd/main.go
COPY api/ api/
COPY pkg/ pkg/
COPY internal/controller/ internal/controller/
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions cmd/osv-generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"flag"

osv_generator "github.com/konflux-ci/mintmaker/tools/osv-generator"
)

// A demo which parses RPM CVE data created in the last 24 hours into OSV database format
func main() {
filename := flag.String("filename", "redhat.nedb", "Output filename for OSV database")
flag.Parse()

osv_generator.GenerateOSV(*filename)
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
github.com/go-logr/logr v1.2.4
github.com/google/go-cmp v0.6.0
github.com/google/go-github/v45 v45.2.0
github.com/konflux-ci/application-api v0.0.0-20240527211352-be061932d497
github.com/onsi/ginkgo/v2 v2.11.0
Expand Down Expand Up @@ -36,7 +37,6 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-github/v60 v60.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
Expand All @@ -45,6 +45,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jarcoal/httpmock v1.3.1
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
Expand All @@ -95,6 +97,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
40 changes: 40 additions & 0 deletions tools/osv-generator/csaf_vex_vulnerability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package osv_generator

type VEX struct {
Document struct {
AggregateSeverity struct {
Text string `json:"text"`
} `json:"aggregate_severity"`
} `json:"document"`
Vulnerabilities []*Vulnerability `json:"vulnerabilities"`
ProductTree struct {
Branches []struct {
Branches []struct {
Category string `json:"category"`
Branches []struct {
Product struct {
ProductIdentificationHelper struct {
Purl string `json:"purl"`
} `json:"product_identification_helper"`
} `json:"product"`
} `json:"branches"`
} `json:"branches"`
} `json:"branches"`
} `json:"product_tree"`
}

type Vulnerability struct {
Cve string `json:"cve"`
DiscoveryDate string `json:"discovery_date"`
Cwe struct {
Id string `json:"id"`
} `json:"cwe"`
References []struct {
Category string `json:"category"`
Url string `json:"url"`
} `json:"references"`
Notes []struct {
Category string `json:"category"`
Text string `json:"text"`
} `json:"notes"`
}
191 changes: 191 additions & 0 deletions tools/osv-generator/cve_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package osv_generator

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"
)

// Download CSAF VEX file from given URL and store into a VEX struct
func GetVEXFromUrl(url string) (VEX, error) {
resp, err := http.Get(url)
if err != nil {
return VEX{}, fmt.Errorf("could not fetch URL: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return VEX{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return VEX{}, fmt.Errorf("could not read response body: %v", err)
}

var vexData VEX

if err := json.Unmarshal([]byte(body), &vexData); err != nil {
return VEX{}, fmt.Errorf("could not unmarshal JSON: %v", err)
}

fmt.Printf("Found %d vulnerabilities at %s\n", len(vexData.Vulnerabilities), url)
return vexData, nil
}

// Convert VEX RPM data to OSV format
func ConvertToOSV(vexData VEX) []OSV {
// Get list of affected packages
affectedList := getAffectedList(vexData)

var vulnerabilities []OSV
for _, vulnerability := range vexData.Vulnerabilities {
// Create OSV vulnerability object for each CVE
osvVulnerability := OSV{
SchemaVersion: "1.6.0",
Id: vulnerability.Cve,
DatabaseSpecific: &DatabaseSpecific{
Severity: vexData.Document.AggregateSeverity.Text,
CWEids: []string{vulnerability.Cwe.Id},
},
Modified: time.Now().Format("2006-01-02T15:04:05Z"),
Published: getPublishedDate(vulnerability),
Summary: getSummary(vulnerability),
Details: getDetails(vulnerability),
References: getReferencesList(vulnerability),
Affected: affectedList,
}

vulnerabilities = append(vulnerabilities, osvVulnerability)
}
return vulnerabilities
}

// Save all CVEs to an OSV file
func StoreToFile(filename string, convertedVulnerabilities []OSV) error {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("error accessing file: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)

for _, v := range convertedVulnerabilities {
if err := encoder.Encode(v); err != nil {
return fmt.Errorf("could not encode OSV data: %v", err)
}
}

return nil
}

// Get list of affected RPM packages from VEX data
func getAffectedList(vex VEX) []*Affected {
var affectedList []*Affected

// Traverse dependencies tree
for _, branch := range vex.ProductTree.Branches {
for _, subBranch := range branch.Branches {
if subBranch.Category == "architecture" {
for _, subSubBranch := range subBranch.Branches {
// Collect only RPM dependencies
if !strings.HasPrefix(subSubBranch.Product.ProductIdentificationHelper.Purl, "pkg:rpm") {
continue
}

// Parse name and version from pURL
re := regexp.MustCompile(`pkg:rpm(?:mod)?/([^@]+)@([^?]+)`)
matches := re.FindStringSubmatch(subSubBranch.Product.ProductIdentificationHelper.Purl)
purl, packageName, version := matches[0], matches[1], matches[2]

affectedPackage := Affected{
Package: &Package{
Ecosystem: "Red Hat",
Name: packageName,
Purl: purl,
},
Ranges: []*Range{
{
Type: "ECOSYSTEM",
Events: []*Event{
{
Introduced: "0.0.0",
},
{
Fixed: version,
},
},
},
},
}

// There will be duplicated dependencied from different architectures, store data once
if !contains(affectedList, affectedPackage) {
affectedList = append(affectedList, &affectedPackage)
}
}
}
}
}
return affectedList
}

func getReferencesList(vulnerability *Vulnerability) []*Reference {
var references []*Reference

for _, reference := range vulnerability.References {
if reference.Category == "self" {
references = append(references, &Reference{
Type: "REPORT",
Url: reference.Url,
})
} else {
references = append(references, &Reference{
Type: "WEB",
Url: reference.Url,
})
}
}
return references
}

func getDetails(vulnerability *Vulnerability) string {
for _, note := range vulnerability.Notes {
if note.Category == "description" {
return note.Text
}
}
panic("No CVE details found")
}

func getSummary(vulnerability *Vulnerability) string {
for _, note := range vulnerability.Notes {
if note.Category == "summary" {
return note.Text
}
}
panic("No CVE summary found")
}

func getPublishedDate(vulnerability *Vulnerability) string {
t, err := time.Parse("2006-01-02T15:04:05+00:00", vulnerability.DiscoveryDate)
if err != nil {
fmt.Printf("Error parsing time for %s: %v\n", vulnerability.Cve, err)
panic(err)
}
return t.Format("2006-01-02T15:04:05Z")
}

func contains(affectedList []*Affected, affectedPackage Affected) bool {
for _, item := range affectedList {
if item.Package.Name == affectedPackage.Package.Name {
return true
}
}
return false
}
Loading