Skip to content

Commit

Permalink
Grype vulnerability scanner parsing tool (#2114)
Browse files Browse the repository at this point in the history
* Added parsing tool

* Added associated UTs

* Added comments

* Addressed Pavan's review comments
1. Moved the parsing tool and associated UTs to a new folder pkg/tools
2. Fixed the func docs
3. unexported all types from the parsing tool
4. Fixed variable names
5. Printed the entire error stack
6. Fixed spelling issues

* Addressed Pavan's review comments
1. changed the input flag variable name
2. changed test input name
3. changed Id to ID in vulnerabilityReport struct
4. fixed error strings to match the rest of the codebase

* Final suggestions from Pavan

---------

Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 7, 2023
1 parent fdb232c commit 30e033e
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 0 deletions.
112 changes: 112 additions & 0 deletions pkg/tools/grype_report_parser_tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"os"
"strings"
)

type vulnerabilityScannerResponse struct {
Matches json.RawMessage `json:"matches"`
RelatedVulnerabilities json.RawMessage `json:"relatedVulnerabilities"`
MatchDetails json.RawMessage `json:"matchDetails"`
Artifact json.RawMessage `json:"artifact"`
}

type matchResponse struct {
Vulnerabilities vulnerabilityReport `json:"vulnerability"`
}

type fixVersionsResponse struct {
Versions []string `json:"versions"`
State string `json:"state"`
}

type vulnerabilityReport struct {
ID string `json:"id"`
Severity string `json:"severity"`
Namespace string `json:"namespace"`
Description string `json:"description"`
FixVersions fixVersionsResponse `json:"fix"`
}

// filterVulnerabilityReportMatches filters vulnerabilities based on the severity levels set in severityTypeSet
func filterVulnerabilityReportMatches(matches []matchResponse, severityTypeSet map[string]bool) ([]vulnerabilityReport, error) {
mv := make([]vulnerabilityReport, 0)
for _, m := range matches {
if severityTypeSet[m.Vulnerabilities.Severity] {
mv = append(mv, m.Vulnerabilities)
}
}
return mv, nil
}

// decodeVulnerabilityReports unmarshals the specific matches from the vulnerability report
// and returns a list of vulnerabilities based on the severity levels set in severityTypeSet
func decodeVulnerabilityReports(v vulnerabilityScannerResponse, severityTypeSet map[string]bool) ([]vulnerabilityReport, error) {
var mr []matchResponse
mv := make([]vulnerabilityReport, 0)
if err := json.Unmarshal(v.Matches, &mr); err != nil {
return mv, fmt.Errorf("failed to unmarshal matches: %v", err)
}
return filterVulnerabilityReportMatches(mr, severityTypeSet)
}

// parseVulerabilitiesReport unmarshals the vulnerability report and returns a list of vulnerabilities
// based on the severity levels set in severityTypeSet
func parseVulerabilitiesReport(filePath string, severityLevels []string) ([]vulnerabilityReport, error) {
mv := make([]vulnerabilityReport, 0)
data, err := os.ReadFile(filePath)
if err != nil {
return mv, fmt.Errorf("failed to read file at path %s: %v", filePath, err)
}
var response vulnerabilityScannerResponse

if err = json.Unmarshal(data, &response); err != nil {
return mv, fmt.Errorf("failed to unmarshal response: %v", err)
}
severityTypeSet := make(map[string]bool)
for _, severityLevel := range severityLevels {
severityTypeSet[severityLevel] = true
}
return decodeVulnerabilityReports(response, severityTypeSet)
}

// printResult Displays the filtered list of vulnerability reports to stdout
func printResult(mv []vulnerabilityReport) {
for _, vulnerability := range mv {
fmt.Printf("ID: %s\n", vulnerability.ID)
fmt.Printf("Severity: %s\n", vulnerability.Severity)
fmt.Printf("Namespace: %s\n", vulnerability.Namespace)
fmt.Printf("Description: %s\n", vulnerability.Description)
fmt.Printf("Fix Versions: %v\n", vulnerability.FixVersions)
fmt.Printf("\n")
}
}

func main() {
validSeverityLevels := []string{"Negliable", "Low", "Medium", "High", "Critical"}
severityInputList := flag.String("s", "High,Critical", "Comma separated list of severity levels to scan. Valid severity levels are: "+strings.Join(validSeverityLevels, ","))
reportJsonFilePath := flag.String("p", "", "Path to the JSON file containing the vulnerabilities report")
flag.Parse()

// passing file path is compulsory
if *reportJsonFilePath == "" {
flag.PrintDefaults()
os.Exit(1)
}
severityLevels := strings.Split(*severityInputList, ",")
mv, err := parseVulerabilitiesReport(*reportJsonFilePath, severityLevels)
if err != nil {
fmt.Printf("Failed to parse vulnerabilities report: %v\n", err)
os.Exit(1)
}
fmt.Printf("Found %d vulnerabilities\n", len(mv))
if len(mv) == 0 {
os.Exit(0)
}
printResult(mv)
os.Exit(1)
}
54 changes: 54 additions & 0 deletions pkg/tools/grype_report_parser_tool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"testing"

. "gopkg.in/check.v1"
)

type VulnerabilityParserSuite struct {
}

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

var _ = Suite(&VulnerabilityParserSuite{})

func (v *VulnerabilityParserSuite) TestNonExistentResult(c *C) {
severityLevels := []string{"High", "Critical"}
matchingVulnerabilities, err := parseVulerabilitiesReport("mock_data/result_non_existent.json", severityLevels)
c.Assert(len(matchingVulnerabilities), Equals, 0)
c.Assert(err, NotNil)
}

func (v *VulnerabilityParserSuite) TestInvalidJson(c *C) {
severityLevels := []string{"High", "Critical"}
matchingVulnerabilities, err := parseVulerabilitiesReport("mock_data/results_invalid.json", severityLevels)
c.Assert(len(matchingVulnerabilities), Equals, 0)
c.Assert(err, NotNil)
}

func (v *VulnerabilityParserSuite) TestValidJsonWithZeroVulnerabilities(c *C) {
severityLevels := []string{"High", "Critical"}
matchingVulnerabilities, err := parseVulerabilitiesReport("mock_data/results_valid_no_matches.json", severityLevels)
c.Assert(len(matchingVulnerabilities), Equals, 0)
c.Assert(err, IsNil)
}

func (v *VulnerabilityParserSuite) TestValidJsonForLowVulerabilities(c *C) {
severityLevels := []string{"Low", "Medium"}
matchingVulnerabilities, err := parseVulerabilitiesReport("mock_data/results_valid.json", severityLevels)
c.Assert(len(matchingVulnerabilities), Equals, 0)
c.Assert(err, IsNil)
}
func (v *VulnerabilityParserSuite) TestValidJsonForMatchingVulerabilities(c *C) {
severityLevels := []string{"High", "Critical"}
expectedIds := []string{"CVE-2016-10228", "CVE-2016-10229"}
matchingVulnerabilities, err := parseVulerabilitiesReport("mock_data/results_valid.json", severityLevels)
c.Assert(len(matchingVulnerabilities), Equals, 2)
c.Assert(err, IsNil)
for index, vulnerability := range matchingVulnerabilities {
c.Assert(vulnerability.ID, Equals, expectedIds[index])
c.Assert(vulnerability.Severity, Equals, severityLevels[index])
}
}
51 changes: 51 additions & 0 deletions pkg/tools/mock_data/results_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"matches": [
{
"vulnerability": {
"id": "CVE-2016-10228",
"dataSource": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"namespace": "debian:distro:debian:10",
"severity": "Medium",
"urls": [
"https://security-tracker.debian.org/tracker/CVE-2016-10228"
],
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.31 and earlier, when invoked with multiple suffixes in the destination encoding (TRANSLATE or IGNORE) along with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"cvss": [],
"fix": {
"versions": [
"2.28-10+deb10u2"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails":[],
"artifact": {}
},
{
"vulnerability": {
"id": "CVE-2016-10229",
"dataSource": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"namespace": "debian:distro:debian:10",
"severity": "High",
"urls": {
"https://security-tracker.debian.org/tracker/CVE-2016-10228"
],
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.31 and earlier, when invoked with multiple suffixes in the destination encoding (TRANSLATE or IGNORE) along with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"cvss": [],
"fix": {
"versions": [
"2.28-10+deb10u2"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails":[],
"artifact": {}
}
]
}

51 changes: 51 additions & 0 deletions pkg/tools/mock_data/results_valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"matches": [
{
"vulnerability": {
"id": "CVE-2016-10228",
"dataSource": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"namespace": "debian:distro:debian:10",
"severity": "High",
"urls": [
"https://security-tracker.debian.org/tracker/CVE-2016-10228"
],
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.31 and earlier, when invoked with multiple suffixes in the destination encoding (TRANSLATE or IGNORE) along with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"cvss": [],
"fix": {
"versions": [
"2.28-10+deb10u2"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails":[],
"artifact": {}
},
{
"vulnerability": {
"id": "CVE-2016-10229",
"dataSource": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"namespace": "debian:distro:debian:10",
"severity": "Critical",
"urls": [
"https://security-tracker.debian.org/tracker/CVE-2016-10228"
],
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.31 and earlier, when invoked with multiple suffixes in the destination encoding (TRANSLATE or IGNORE) along with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"cvss": [],
"fix": {
"versions": [
"2.28-10+deb10u2"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails":[],
"artifact": {}
}
]
}

4 changes: 4 additions & 0 deletions pkg/tools/mock_data/results_valid_no_matches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"matches": []
}

0 comments on commit 30e033e

Please sign in to comment.