From e421ef11cf0039e1ae9c270f4dc7b417423f10c6 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Tue, 14 Nov 2023 16:51:22 -0600 Subject: [PATCH] Improve marshal performance by using pointers within CycloneDX Vulnerability data structures (#65) * Improve performance by using pointers within CycloneDX Vulnerability structs Signed-off-by: Matt Rutkowski * Migrate Vuln. struct members to pointers and update marhsal routines Signed-off-by: Matt Rutkowski * Introduce const for JSON indent. spacing and set to conventional defaults Signed-off-by: Matt Rutkowski --------- Signed-off-by: Matt Rutkowski --- cmd/query_test.go | 11 +- cmd/root_test.go | 10 ++ cmd/trim.go | 16 ++- cmd/trim_test.go | 22 +++ cmd/vulnerability.go | 145 +------------------ cmd/vulnerability_test.go | 4 +- schema/bom_hash.go | 36 +++-- schema/cyclonedx.go | 12 +- schema/cyclonedx_marshal.go | 130 ++++++++++++----- schema/cyclonedx_vulnerability.go | 84 +++++------ schema/license_policy_config.go | 1 - test/trim/trim-cdx-1-4-sample-vex.json | 191 +++++++++++++++++++++++++ utils/json.go | 9 -- 13 files changed, 404 insertions(+), 267 deletions(-) create mode 100644 test/trim/trim-cdx-1-4-sample-vex.json diff --git a/cmd/query_test.go b/cmd/query_test.go index 13e0ff7f..f5706a41 100644 --- a/cmd/query_test.go +++ b/cmd/query_test.go @@ -61,7 +61,7 @@ func innerQuery(t *testing.T, filename string, queryRequest *common.QueryRequest } // This will print results ONLY if --quiet mode is `false` - printResult(result) + printMarshaledResultOnlyIfNotQuiet(result) return } @@ -122,15 +122,6 @@ func VerifySelectedFieldsInJsonMap(t *testing.T, keys []string, results interfac return } -func printResult(iResult interface{}) { - if !*TestLogQuiet { - // Format results in JSON - fResult, _ := utils.MarshalAnyToFormattedJsonString(iResult) - // Output the JSON data directly to stdout (not subject to log-level) - fmt.Printf("%s\n", fResult) - } -} - // ---------------------------------------- // Command flag tests // ---------------------------------------- diff --git a/cmd/root_test.go b/cmd/root_test.go index 1b9251b9..84a3b9bf 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -283,3 +283,13 @@ func bufferContainsValues(buffer bytes.Buffer, values ...string) bool { } return true } + +// TODO: find a better way using some log package feature +func printMarshaledResultOnlyIfNotQuiet(iResult interface{}) { + if !*TestLogQuiet { + // Format results in JSON + fResult, _ := utils.MarshalAnyToFormattedJsonString(iResult) + // Output the JSON data directly to stdout (not subject to log-level) + fmt.Printf("%s\n", fResult) + } +} diff --git a/cmd/trim.go b/cmd/trim.go index c71f5a45..98a2e3f1 100644 --- a/cmd/trim.go +++ b/cmd/trim.go @@ -34,6 +34,14 @@ const ( FLAG_TRIM_MAP_KEYS = "keys" ) +// TODO: make flag configurable: +// NOTE: 4-space indent is accepted convention: +// https://docs.openstack.org/doc-contrib-guide/json-conv.html +const ( + TRIM_OUTPUT_PREFIX = "" + TRIM_OUTPUT_INDENT = " " +) + // flag help (translate) const ( FLAG_TRIM_OUTPUT_FORMAT_HELP = "format output using the specified type" @@ -207,12 +215,12 @@ func Trim(writer io.Writer, persistentFlags utils.PersistentCommandFlags, trimFl getLogger().Infof("Outputting listing (`%s` format)...", format) switch format { case FORMAT_JSON: - err = document.EncodeAsFormattedJSON(writer, "", " ") + err = document.EncodeAsFormattedJSON(writer, TRIM_OUTPUT_PREFIX, TRIM_OUTPUT_INDENT) default: // Default to Text output for anything else (set as flag default) - getLogger().Warningf("Stats not supported for `%s` format; defaulting to `%s` format...", - format, FORMAT_TEXT) - err = document.EncodeAsFormattedJSON(writer, "", " ") + getLogger().Warningf("Trim not supported for `%s` format; defaulting to `%s` format...", + format, FORMAT_JSON) + err = document.EncodeAsFormattedJSON(writer, TRIM_OUTPUT_PREFIX, TRIM_OUTPUT_INDENT) } return diff --git a/cmd/trim_test.go b/cmd/trim_test.go index b46adcef..8643478e 100644 --- a/cmd/trim_test.go +++ b/cmd/trim_test.go @@ -37,6 +37,7 @@ const ( TEST_TRIM_CDX_1_4_ENCODED_CHARS = "test/trim/trim-cdx-1-4-sample-encoded-chars.sbom.json" TEST_TRIM_CDX_1_4_SAMPLE_XXL_1 = "test/trim/trim-cdx-1-4-sample-xxl-1.sbom.json" TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY = "test/trim/trim-cdx-1-5-sample-small-components-only.sbom.json" + TEST_TRIM_CDX_1_4_SAMPLE_VEX = "test/trim/trim-cdx-1-4-sample-vex.json" TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1 = "test/trim/trim-cdx-1-5-sample-medium-1.sbom.json" ) @@ -352,3 +353,24 @@ func TestTrimCdx15FooFromTools(t *testing.T) { t.Error(fmt.Errorf("invalid trim result: string not found: %s", TEST_STRING_1)) } } + +func TestTrimCdx14SourceFromVulnerabilities(t *testing.T) { + ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_4_SAMPLE_VEX, nil) + ti.Keys = append(ti.Keys, "source") + ti.FromPaths = []string{"vulnerabilities"} + ti.TestOutputVariantName = utils.GetCallerFunctionName(2) + ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_4_SAMPLE_VEX) + + buffer, _, err := innerTestTrim(t, ti) + s := buffer.String() + if err != nil { + getLogger().Debugf("result: %s", s) + t.Error(err) + } + + // Assure JSON map does not contain the trimmed key(s) + err = VerifyTrimOutputFileResult(t, ti, ti.Keys, ti.FromPaths[0]) + if err != nil { + t.Error(err) + } +} diff --git a/cmd/vulnerability.go b/cmd/vulnerability.go index af6e374c..59c68ad9 100644 --- a/cmd/vulnerability.go +++ b/cmd/vulnerability.go @@ -22,7 +22,6 @@ import ( "encoding/csv" "fmt" "io" - "reflect" "sort" "strings" "text/tabwriter" @@ -260,7 +259,7 @@ func loadDocumentVulnerabilities(document *schema.BOM, whereFilters []common.Whe // Hash all components found in the (root).components[] (+ "nested" components) pVulnerabilities := document.GetCdxVulnerabilities() if pVulnerabilities != nil && len(*pVulnerabilities) > 0 { - if err = hashVulnerabilities(document, *pVulnerabilities, whereFilters); err != nil { + if err = document.HashVulnerabilities(*pVulnerabilities, whereFilters); err != nil { return } } @@ -268,148 +267,6 @@ func loadDocumentVulnerabilities(document *schema.BOM, whereFilters []common.Whe return } -// We need to hash our own informational structure around the CDX data in order -// to simplify --where queries to command line users -func hashVulnerabilities(bom *schema.BOM, vulnerabilities []schema.CDXVulnerability, whereFilters []common.WhereFilter) (err error) { - getLogger().Enter() - defer getLogger().Exit(err) - - for _, cdxVulnerability := range vulnerabilities { - _, err = hashVulnerability(bom, cdxVulnerability, whereFilters) - if err != nil { - return - } - } - return -} - -// Hash a CDX Component and recursively those of any "nested" components -// TODO we should WARN if version is not a valid semver (e.g., examples/cyclonedx/BOM/laravel-7.12.0/bom.1.3.json) -func hashVulnerability(bom *schema.BOM, cdxVulnerability schema.CDXVulnerability, whereFilters []common.WhereFilter) (vi *schema.VulnerabilityInfo, err error) { - getLogger().Enter() - defer getLogger().Exit(err) - var vulnInfo schema.VulnerabilityInfo - vi = &vulnInfo - - if reflect.DeepEqual(cdxVulnerability, schema.CDXVulnerability{}) { - err = getLogger().Errorf("invalid vulnerability info: missing or empty : %v ", cdxVulnerability) - return - } - - if cdxVulnerability.Id == "" { - getLogger().Warningf("vulnerability missing required value `id` : %v ", cdxVulnerability) - } - - if cdxVulnerability.Published == "" { - getLogger().Warningf("vulnerability (`%s`) missing `published` date", cdxVulnerability.Id) - } - - if cdxVulnerability.Created == "" { - getLogger().Warningf("vulnerability (`%s`) missing `created` date", cdxVulnerability.Id) - } - - if len(cdxVulnerability.Ratings) == 0 { - getLogger().Warningf("vulnerability (`%s`) missing `ratings`", cdxVulnerability.Id) - } - - // hash any component w/o a license using special key name - vulnInfo.Vulnerability = cdxVulnerability - if cdxVulnerability.BOMRef != nil { - vulnInfo.BOMRef = cdxVulnerability.BOMRef.String() - } - vulnInfo.Id = cdxVulnerability.Id - - // Truncate dates from 2023-02-02T00:00:00.000Z to 2023-02-02 - // Note: if validation errors are found by the "truncate" function, - // it will emit an error and return the original (failing) value - dateTime, _ := utils.TruncateTimeStampISO8601Date(cdxVulnerability.Created) - vulnInfo.Created = dateTime - - dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Published) - vulnInfo.Published = dateTime - - dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Updated) - vulnInfo.Updated = dateTime - - dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Rejected) - vulnInfo.Rejected = dateTime - - vulnInfo.Description = cdxVulnerability.Description - - // Source object: retrieve report fields from nested objects - if cdxVulnerability.Source != nil { - source := *cdxVulnerability.Source - vulnInfo.Source = source - vulnInfo.SourceName = source.Name - vulnInfo.SourceUrl = source.Url - } - - // TODO: replace empty Analysis values with "UNDEFINED" - vulnInfo.AnalysisState = cdxVulnerability.Analysis.State - if vulnInfo.AnalysisState == "" { - vulnInfo.AnalysisState = schema.VULN_ANALYSIS_STATE_EMPTY - } - - vulnInfo.AnalysisJustification = cdxVulnerability.Analysis.Justification - if vulnInfo.AnalysisJustification == "" { - vulnInfo.AnalysisJustification = schema.VULN_ANALYSIS_STATE_EMPTY - } - vulnInfo.AnalysisResponse = cdxVulnerability.Analysis.Response - if len(vulnInfo.AnalysisResponse) == 0 { - vulnInfo.AnalysisResponse = []string{schema.VULN_ANALYSIS_STATE_EMPTY} - } - - // Convert []int to []string for --where filter - // TODO see if we can eliminate this conversion and handle while preparing report data - // as this SHOULD appear there as []interface{} - if len(cdxVulnerability.Cwes) > 0 { - vulnInfo.CweIds = strings.Fields(strings.Trim(fmt.Sprint(cdxVulnerability.Cwes), "[]")) - } - - // CVSS Score Qualitative Rating - // 0.0 None - // 0.1 – 3.9 Low - // 4.0 – 6.9 Medium - // 7.0 – 8.9 High - // 9.0 – 10.0 Critical - - // TODO: if summary report, see if more than one severity can be shown without clogging up column data - numRatings := len(cdxVulnerability.Ratings) - if numRatings > 0 { - //var sourceMatch int - for _, rating := range cdxVulnerability.Ratings { - // defer to same source as the top-level vuln. declares - fSeverity := fmt.Sprintf("%s: %v (%s)", rating.Method, rating.Score, rating.Severity) - // give listing priority to ratings that matches top-level vuln. reporting source - if rating.Source.Name == cdxVulnerability.Source.Name { - // prepend to slice - vulnInfo.CvssSeverity = append([]string{fSeverity}, vulnInfo.CvssSeverity...) - continue - } - vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, fSeverity) - } - - } else { - // Set first entry to empty value (i.e., "none") - vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, schema.VULN_RATING_EMPTY) - } - - var match bool = true - if len(whereFilters) > 0 { - mapVulnInfo, _ := utils.MarshalStructToJsonMap(vulnInfo) - match, _ = whereFilterMatch(mapVulnInfo, whereFilters) - } - - if match { - bom.VulnerabilityMap.Put(vulnInfo.Id, vulnInfo) - - getLogger().Tracef("Put: %s (`%s`), `%s`)", - vulnInfo.Id, vulnInfo.Description, vulnInfo.BOMRef) - } - - return -} - // NOTE: This list is NOT de-duplicated // TODO: Add a --no-title flag to skip title output func DisplayVulnListText(bom *schema.BOM, output io.Writer, flags utils.VulnerabilityCommandFlags) { diff --git a/cmd/vulnerability_test.go b/cmd/vulnerability_test.go index b5a55c2b..2f00da1b 100644 --- a/cmd/vulnerability_test.go +++ b/cmd/vulnerability_test.go @@ -232,7 +232,6 @@ func TestVulnListCdx13JSON(t *testing.T) { testInfo.ResultExpectedLineCount = 185 result, _, _ := innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) getLogger().Debugf("result:\n%s", result.String()) - //fmt.Printf("result:\n%s", result.String()) } // ------------------------------------------- @@ -254,7 +253,8 @@ func TestVulnListTextCdx14WhereClauseAndResultsByIdStartsWith(t *testing.T) { nil) testInfo.ResultLineContainsValues = TEST_OUTPUT_CONTAINS testInfo.ResultLineContainsValuesAtLineNum = 2 - innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) + result, _, _ := innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) + getLogger().Debugf("result:\n%s", result.String()) } func TestVulnListTextCdx14WhereClauseDescContains(t *testing.T) { diff --git a/schema/bom_hash.go b/schema/bom_hash.go index 74d0f88d..ac07439a 100644 --- a/schema/bom_hash.go +++ b/schema/bom_hash.go @@ -346,7 +346,7 @@ func (bom *BOM) HashVulnerability(cdxVulnerability CDXVulnerability, whereFilter getLogger().Warningf("vulnerability (`%s`) missing `created` date", cdxVulnerability.Id) } - if len(cdxVulnerability.Ratings) == 0 { + if cdxVulnerability.Ratings == nil || len(*cdxVulnerability.Ratings) == 0 { getLogger().Warningf("vulnerability (`%s`) missing `ratings`", cdxVulnerability.Id) } @@ -383,24 +383,32 @@ func (bom *BOM) HashVulnerability(cdxVulnerability CDXVulnerability, whereFilter } // TODO: replace empty Analysis values with "UNDEFINED" - vulnInfo.AnalysisState = cdxVulnerability.Analysis.State - if vulnInfo.AnalysisState == "" { - vulnInfo.AnalysisState = VULN_ANALYSIS_STATE_EMPTY - } + if cdxVulnerability.Analysis != nil { + vulnInfo.AnalysisState = cdxVulnerability.Analysis.State + if vulnInfo.AnalysisState == "" { + vulnInfo.AnalysisState = VULN_ANALYSIS_STATE_EMPTY + } + + vulnInfo.AnalysisJustification = cdxVulnerability.Analysis.Justification + if vulnInfo.AnalysisJustification == "" { + vulnInfo.AnalysisJustification = VULN_ANALYSIS_STATE_EMPTY + } - vulnInfo.AnalysisJustification = cdxVulnerability.Analysis.Justification - if vulnInfo.AnalysisJustification == "" { + vulnInfo.AnalysisResponse = *cdxVulnerability.Analysis.Response + if len(vulnInfo.AnalysisResponse) == 0 { + vulnInfo.AnalysisResponse = []string{VULN_ANALYSIS_STATE_EMPTY} + } + } else { + vulnInfo.AnalysisState = VULN_ANALYSIS_STATE_EMPTY vulnInfo.AnalysisJustification = VULN_ANALYSIS_STATE_EMPTY - } - vulnInfo.AnalysisResponse = cdxVulnerability.Analysis.Response - if len(vulnInfo.AnalysisResponse) == 0 { vulnInfo.AnalysisResponse = []string{VULN_ANALYSIS_STATE_EMPTY} } // Convert []int to []string for --where filter // TODO see if we can eliminate this conversion and handle while preparing report data // as this SHOULD appear there as []interface{} - if len(cdxVulnerability.Cwes) > 0 { + if cdxVulnerability.Cwes != nil && len(*cdxVulnerability.Cwes) > 0 { + // strip off slice/array brackets vulnInfo.CweIds = strings.Fields(strings.Trim(fmt.Sprint(cdxVulnerability.Cwes), "[]")) } @@ -412,10 +420,9 @@ func (bom *BOM) HashVulnerability(cdxVulnerability CDXVulnerability, whereFilter // 9.0 – 10.0 Critical // TODO: if summary report, see if more than one severity can be shown without clogging up column data - numRatings := len(cdxVulnerability.Ratings) - if numRatings > 0 { + if cdxVulnerability.Ratings != nil && len(*cdxVulnerability.Ratings) > 0 { //var sourceMatch int - for _, rating := range cdxVulnerability.Ratings { + for _, rating := range *cdxVulnerability.Ratings { // defer to same source as the top-level vuln. declares fSeverity := fmt.Sprintf("%s: %v (%s)", rating.Method, rating.Score, rating.Severity) // give listing priority to ratings that matches top-level vuln. reporting source @@ -426,7 +433,6 @@ func (bom *BOM) HashVulnerability(cdxVulnerability CDXVulnerability, whereFilter } vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, fSeverity) } - } else { // Set first entry to empty value (i.e., "none") vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, VULN_RATING_EMPTY) diff --git a/schema/cyclonedx.go b/schema/cyclonedx.go index fabf1b22..1f551978 100644 --- a/schema/cyclonedx.go +++ b/schema/cyclonedx.go @@ -59,8 +59,8 @@ type CDXBom struct { // v1.3: added "licenses", "properties" // v1.5: added "lifecycles" type CDXMetadata struct { - Timestamp string `json:"timestamp,omitempty"` - Tools interface{} `json:"tools,omitempty"` // v1.2: added.v1.5: "tools" is now an interface{} + Timestamp string `json:"timestamp,omitempty" scvs:"bom:core:timestamp"` // urn:owasp:scvs:bom:core:timestamp + Tools interface{} `json:"tools,omitempty"` // v1.2: added.v1.5: "tools" is now an interface{} Authors *[]CDXOrganizationalContact `json:"authors,omitempty"` Component *CDXComponent `json:"component,omitempty"` Manufacturer *CDXOrganizationalEntity `json:"manufacturer,omitempty"` @@ -96,10 +96,10 @@ type CDXComponent struct { Hashes *[]CDXHash `json:"hashes,omitempty"` Licenses *[]CDXLicenseChoice `json:"licenses,omitempty"` Copyright string `json:"copyright,omitempty"` - Cpe string `json:"cpe,omitempty"` // See: https://nvd.nist.gov/products/cpe - Purl string `json:"purl,omitempty"` // See: https://github.com/package-url/purl-spec - Swid *CDXSwid `json:"swid,omitempty"` // See: https://www.iso.org/standard/65666.html - Pedigree *CDXPedigree `json:"pedigree,omitempty"` // anon. type + Cpe string `json:"cpe,omitempty"` // See: https://nvd.nist.gov/products/cpe + Purl string `json:"purl,omitempty" scvs:"bom:resource:identifiers:purl"` // See: https://github.com/package-url/purl-spec + Swid *CDXSwid `json:"swid,omitempty"` // See: https://www.iso.org/standard/65666.html + Pedigree *CDXPedigree `json:"pedigree,omitempty"` // anon. type ExternalReferences *[]CDXExternalReference `json:"externalReferences,omitempty"` Components *[]CDXComponent `json:"components,omitempty"` Evidence *CDXComponentEvidence `json:"evidence,omitempty"` // v1.3: added diff --git a/schema/cyclonedx_marshal.go b/schema/cyclonedx_marshal.go index 6320a4d4..bf66a050 100644 --- a/schema/cyclonedx_marshal.go +++ b/schema/cyclonedx_marshal.go @@ -47,7 +47,7 @@ func IsInterfaceASlice(testValue interface{}) bool { // all struct fields are marshalled regardless of such constraints. // -------------------------------------------------------------------------------- -var ENCODED_EMPTY_STRUCT = []byte("{}") +var BYTE_ENCODED_ZERO_STRUCT = []byte("{}") var ENCODED_EMPTY_SLICE_OF_STRUCT = []byte("[{}]") // -------------------------- @@ -127,6 +127,7 @@ func (value *CDXAttachment) MarshalJSON() ([]byte, error) { // recreate a representation of the struct, but only include values in map that are not empty func (value *CDXVulnerability) MarshalJSON() ([]byte, error) { + var testEmpty []byte temp := map[string]interface{}{} if value.BOMRef != nil && *value.BOMRef != "" { @@ -162,44 +163,51 @@ func (value *CDXVulnerability) MarshalJSON() ([]byte, error) { } // CDXVulnerabilitySource - if value.Source != nil && *value.Source != (CDXVulnerabilitySource{}) { - temp["source"] = value.Source + if value.Source != nil { + testEmpty, _ = json.Marshal(value.Source) + if !bytes.Equal(testEmpty, BYTE_ENCODED_ZERO_STRUCT) { + temp["source"] = value.Source + } } // CDXCredit (anon. type) - testEmpty, _ := json.Marshal(&value.Credits) - if !bytes.Equal(testEmpty, ENCODED_EMPTY_STRUCT) { - temp["credits"] = &value.Credits + if value.Credits != nil { + testEmpty, _ = json.Marshal(value.Credits) + if !bytes.Equal(testEmpty, BYTE_ENCODED_ZERO_STRUCT) { + temp["credits"] = value.Credits + } } // CDXAnalysis (anon. type) - testEmpty, _ = json.Marshal(&value.Analysis) - if !bytes.Equal(testEmpty, ENCODED_EMPTY_STRUCT) { - temp["analysis"] = &value.Analysis + if value.Analysis != nil { + testEmpty, _ = json.Marshal(value.Analysis) + if !bytes.Equal(testEmpty, BYTE_ENCODED_ZERO_STRUCT) { + temp["analysis"] = value.Analysis + } } // CDXAffects - if len(value.Affects) > 0 { - testEmpty, _ = json.Marshal(&value.Affects) + if value.Affects != nil && len(*value.Affects) > 0 { + testEmpty, _ = json.Marshal(value.Affects) if !bytes.Equal(testEmpty, ENCODED_EMPTY_SLICE_OF_STRUCT) { - temp["affects"] = &value.Affects + temp["affects"] = value.Affects } } - if len(value.References) > 0 { - temp["references"] = &value.References + if value.References != nil && len(*value.References) > 0 { + temp["references"] = value.References } - if len(value.Ratings) > 0 { - temp["ratings"] = &value.Ratings + if value.Ratings != nil && len(*value.Ratings) > 0 { + temp["ratings"] = value.Ratings } - if len(value.Advisories) > 0 { - temp["advisories"] = &value.Advisories + if value.Advisories != nil && len(*value.Advisories) > 0 { + temp["advisories"] = value.Advisories } - if len(value.Cwes) > 0 { - temp["cwes"] = &value.Cwes + if value.Cwes != nil && len(*value.Cwes) > 0 { + temp["cwes"] = value.Cwes } // v1.5 allows tools to be either an array of (legacy) tool object or a new tool object @@ -216,7 +224,7 @@ func (value *CDXVulnerability) MarshalJSON() ([]byte, error) { } } - if len(value.Properties) > 0 { + if value.Properties != nil && len(*value.Properties) > 0 { temp["properties"] = value.Properties } @@ -232,10 +240,10 @@ func (value *CDXVulnerability) MarshalJSON() ([]byte, error) { func (value *CDXVulnerabilityReference) MarshalJSON() ([]byte, error) { temp := map[string]interface{}{} if len(value.Id) > 0 { - temp["id"] = &value.Id + temp["id"] = value.Id } - if value.Source != (CDXVulnerabilitySource{}) { - temp["source"] = &value.Source + if value.Source != nil && *value.Source != (CDXVulnerabilitySource{}) { + temp["source"] = value.Source } // reuse built-in json encoder, which accepts a map primitive return json.Marshal(temp) @@ -244,10 +252,10 @@ func (value *CDXVulnerabilityReference) MarshalJSON() ([]byte, error) { func (value *CDXVulnerabilitySource) MarshalJSON() ([]byte, error) { temp := map[string]interface{}{} if len(value.Url) > 0 { - temp["url"] = &value.Url + temp["url"] = value.Url } if len(value.Name) > 0 { - temp["name"] = &value.Name + temp["name"] = value.Name } // reuse built-in json encoder, which accepts a map primitive return json.Marshal(temp) @@ -255,14 +263,14 @@ func (value *CDXVulnerabilitySource) MarshalJSON() ([]byte, error) { func (value *CDXCredit) MarshalJSON() ([]byte, error) { temp := map[string]interface{}{} - if len(value.Individuals) > 0 { - temp["individuals"] = &value.Individuals + if value.Individuals != nil && len(*value.Individuals) > 0 { + temp["individuals"] = value.Individuals } - if len(value.Organizations) > 0 { - temp["organizations"] = &value.Organizations + if value.Organizations != nil && len(*value.Organizations) > 0 { + temp["organizations"] = value.Organizations } if len(temp) == 0 { - return ENCODED_EMPTY_STRUCT, nil + return BYTE_ENCODED_ZERO_STRUCT, nil } // reuse built-in json encoder, which accepts a map primitive return json.Marshal(temp) @@ -270,11 +278,65 @@ func (value *CDXCredit) MarshalJSON() ([]byte, error) { func (value *CDXAffect) MarshalJSON() ([]byte, error) { temp := map[string]interface{}{} - if len(value.Versions) > 0 { - temp["versions"] = &value.Versions + if value.Versions != nil && len(*value.Versions) > 0 { + temp["versions"] = value.Versions + } + if len(temp) == 0 { + return BYTE_ENCODED_ZERO_STRUCT, nil + } + // reuse built-in json encoder, which accepts a map primitive + return json.Marshal(temp) +} + +// type CDXOrganizationalEntity struct { +// Name string `json:"name,omitempty"` +// Url []string `json:"url,omitempty"` +// Contact *[]CDXOrganizationalContact `json:"contact,omitempty"` +// BOMRef *CDXRefType `json:"bom-ref,omitempty"` // v1.5 added +// } +func (value *CDXOrganizationalEntity) MarshalJSON() ([]byte, error) { + temp := map[string]interface{}{} + if value.Name != "" { + temp["name"] = value.Name + } + if len(value.Url) > 0 { + temp["url"] = value.Url + } + if value.Contact != nil && len(*value.Contact) > 0 { + temp["contact"] = value.Contact + } + if value.BOMRef != nil && *value.BOMRef != "" { + temp["bom-ref"] = value.BOMRef + } + if len(temp) == 0 { + return BYTE_ENCODED_ZERO_STRUCT, nil + } + // reuse built-in json encoder, which accepts a map primitive + return json.Marshal(temp) +} + +// type CDXOrganizationalContact struct { +// Name string `json:"name,omitempty"` +// Email string `json:"email,omitempty"` +// Phone string `json:"phone,omitempty"` +// BOMRef *CDXRefType `json:"bom-ref,omitempty"` // v1.5 added +// } +func (value *CDXOrganizationalContact) MarshalJSON() ([]byte, error) { + temp := map[string]interface{}{} + if value.Name != "" { + temp["name"] = value.Name + } + if value.Email != "" { + temp["email"] = value.Email + } + if value.Phone != "" { + temp["phone"] = value.Phone + } + if value.BOMRef != nil && *value.BOMRef != "" { + temp["bom-ref"] = value.BOMRef } if len(temp) == 0 { - return ENCODED_EMPTY_STRUCT, nil + return BYTE_ENCODED_ZERO_STRUCT, nil } // reuse built-in json encoder, which accepts a map primitive return json.Marshal(temp) diff --git a/schema/cyclonedx_vulnerability.go b/schema/cyclonedx_vulnerability.go index bf8fda24..d6b21543 100644 --- a/schema/cyclonedx_vulnerability.go +++ b/schema/cyclonedx_vulnerability.go @@ -24,33 +24,33 @@ package schema // Note: "cwes" is a array of "cwe" which is a constrained `int` // NOTE: CDXRefType is a named `string` type as of v1.5 type CDXVulnerability struct { - BOMRef *CDXRefType `json:"bom-ref,omitempty"` // v1.4 - Id string `json:"id,omitempty"` // v1.4 - Source *CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 - References []CDXVulnerabilityReference `json:"references"` // v1.4: anon. type - Ratings []CDXRating `json:"ratings,omitempty"` // v1.4 - Cwes []int `json:"cwes,omitempty"` // v1.4 - Description string `json:"description,omitempty"` // v1.4 - Detail string `json:"detail,omitempty"` // v1.4 - Recommendation string `json:"recommendation,omitempty"` // v1.4 - Advisories []CDXAdvisory `json:"advisories,omitempty"` // v1.4 - Created string `json:"created,omitempty"` // v1.4 - Published string `json:"published,omitempty"` // v1.4 - Updated string `json:"updated,omitempty"` // v1.4 - Credits CDXCredit `json:"credits,omitempty"` // v1.4: anon. type - Tools interface{} `json:"tools,omitempty"` // v1.4: added; v1.5: changed to interface{} - Analysis CDXAnalysis `json:"analysis,omitempty"` // v1.4: anon. type - Affects []CDXAffect `json:"affects,omitempty"` // v1.4: anon. type - Properties []CDXProperty `json:"properties,omitempty"` // v1.4: added - Workaround string `json:"workaround,omitempty"` // v1.5: added - ProofOfConcept CDXProofOfConcept `json:"proofOfConcept,omitempty"` // v1.5: added - Rejected string `json:"rejected,omitempty"` // v1.5: added + BOMRef *CDXRefType `json:"bom-ref,omitempty"` // v1.4 + Id string `json:"id,omitempty"` // v1.4 + Source *CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 + References *[]CDXVulnerabilityReference `json:"references"` // v1.4: anon. type + Ratings *[]CDXRating `json:"ratings,omitempty"` // v1.4 + Cwes *[]int `json:"cwes,omitempty"` // v1.4 + Description string `json:"description,omitempty"` // v1.4 + Detail string `json:"detail,omitempty"` // v1.4 + Recommendation string `json:"recommendation,omitempty"` // v1.4 + Advisories *[]CDXAdvisory `json:"advisories,omitempty"` // v1.4 + Created string `json:"created,omitempty"` // v1.4 + Published string `json:"published,omitempty"` // v1.4 + Updated string `json:"updated,omitempty"` // v1.4 + Credits *CDXCredit `json:"credits,omitempty"` // v1.4: anon. type + Tools interface{} `json:"tools,omitempty"` // v1.4: added; v1.5: changed to interface{} + Analysis *CDXAnalysis `json:"analysis,omitempty"` // v1.4: anon. type + Affects *[]CDXAffect `json:"affects,omitempty"` // v1.4: anon. type + Properties *[]CDXProperty `json:"properties,omitempty"` // v1.4: added + Workaround string `json:"workaround,omitempty"` // v1.5: added + ProofOfConcept *CDXProofOfConcept `json:"proofOfConcept,omitempty"` // v1.5: added + Rejected string `json:"rejected,omitempty"` // v1.5: added } // v1.4 This is an anonymous type used in CDXVulnerability type CDXVulnerabilityReference struct { - Id string `json:"id,omitempty"` // v1.4 - Source CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 + Id string `json:"id,omitempty"` // v1.4 + Source *CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 } // v1.4: created "vulnerabilitySource" defn. @@ -66,12 +66,12 @@ type CDXVulnerabilitySource struct { // Note: "severity" is of type "severity" which is a constrained `string` // Note: "method" is of type "scoreMethod" which is a constrained `string` type CDXRating struct { - Source CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 - Score float64 `json:"score,omitempty"` // v1.4 - Severity string `json:"severity,omitempty"` // v1.4 - Method string `json:"method,omitempty"` // v1.4 - Vector string `json:"vector,omitempty"` // v1.4 - Justification string `json:"justification,omitempty"` // v1.4 + Source *CDXVulnerabilitySource `json:"source,omitempty"` // v1.4 + Score float64 `json:"score,omitempty"` // v1.4 + Severity string `json:"severity,omitempty"` // v1.4 + Method string `json:"method,omitempty"` // v1.4 + Vector string `json:"vector,omitempty"` // v1.4 + Justification string `json:"justification,omitempty"` // v1.4 } // v1.4: created "releaseNotes" defn. @@ -84,8 +84,8 @@ type CDXAdvisory struct { // v1.4: created "credit" defn. to represent the in-line, anon. type // found in the "vulnerability" type defn. type CDXCredit struct { - Organizations []CDXOrganizationalEntity `json:"organizations,omitempty"` // v1.4 - Individuals []CDXOrganizationalContact `json:"individuals,omitempty"` // v1.4 + Organizations *[]CDXOrganizationalEntity `json:"organizations,omitempty"` // v1.4 + Individuals *[]CDXOrganizationalContact `json:"individuals,omitempty"` // v1.4 } // v1.4: created "analysis" def. to represent an in-line, anon. type defined in the "vulnerability" object defn. @@ -94,12 +94,12 @@ type CDXCredit struct { // Note: "justification" is an "impactAnalysisJustification" type which is a constrained enum. of type `string` // TODO: "response" is also "in-lined" as a constrained enum. of `string`, but SHOULD be declared at top-level type CDXAnalysis struct { - State string `json:"state,omitempty"` // v1.4 - Justification string `json:"justification,omitempty"` // v1.4 - Response []string `json:"response,omitempty"` // v1.4: anon. type - Detail string `json:"detail,omitempty"` // v1.4 - FirstIssued string `json:"firstIssued,omitempty"` // v1.5: added - LastUpdated string `json:"lastUpdated,omitempty"` // v1.5: added + State string `json:"state,omitempty"` // v1.4 + Justification string `json:"justification,omitempty"` // v1.4 + Response *[]string `json:"response,omitempty"` // v1.4: anon. type + Detail string `json:"detail,omitempty"` // v1.4 + FirstIssued string `json:"firstIssued,omitempty"` // v1.5: added + LastUpdated string `json:"lastUpdated,omitempty"` // v1.5: added } // v1.4: created "analysis" def. to represent an in-line, anon. type @@ -107,8 +107,8 @@ type CDXAnalysis struct { // Note: This anon. "type" ONLY includes a single array of another in-line type // TODO: create top-level defn. for "affect" anon. type type CDXAffect struct { - Versions []CDXVersionRange `json:"versions,omitempty"` // v1.4: anon. type - Ref CDXRefLinkType `json:"ref,omitempty"` // v1.5: added + Versions *[]CDXVersionRange `json:"versions,omitempty"` // v1.4: anon. type + Ref *CDXRefLinkType `json:"ref,omitempty"` // v1.5: added } // v1.4: created "version" def. to represent an in-line, anon. type @@ -126,9 +126,9 @@ type CDXVersionRange struct { // v1.5: created ("reproductionSteps", "environment", "supportingMaterial") // TODO: "supportingMaterial" should be plural as it is an "array" type CDXProofOfConcept struct { - ReproductionSteps string `json:"reproductionSteps,omitempty"` // v1.5: added - Environment string `json:"environment,omitempty"` // v1.5: added - SupportingMaterial []CDXSupportingMaterial `json:"supportingMaterial,omitempty"` // v1.5: added + ReproductionSteps string `json:"reproductionSteps,omitempty"` // v1.5: added + Environment string `json:"environment,omitempty"` // v1.5: added + SupportingMaterial *[]CDXSupportingMaterial `json:"supportingMaterial,omitempty"` // v1.5: added } // v1.5: created ("contentType", "encoding", "content") diff --git a/schema/license_policy_config.go b/schema/license_policy_config.go index 8c59d93b..ccd52bb3 100644 --- a/schema/license_policy_config.go +++ b/schema/license_policy_config.go @@ -102,7 +102,6 @@ func (config *LicensePolicyConfig) Reset() { func (config *LicensePolicyConfig) GetFamilyNameMap() (hashmap *slicemultimap.MultiMap, err error) { if config.licenseFamilyNameMap == nil { err = config.hashLicensePolicies() - fmt.Printf("!!!!!!!!!Not hashed!!!!!!!!!!") } return config.licenseFamilyNameMap, err } diff --git a/test/trim/trim-cdx-1-4-sample-vex.json b/test/trim/trim-cdx-1-4-sample-vex.json new file mode 100644 index 00000000..c92841d6 --- /dev/null +++ b/test/trim/trim-cdx-1-4-sample-vex.json @@ -0,0 +1,191 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "vulnerabilities": [ + { + "id": "CVE-2020-25649", + "cwes": [611], + "created": "2020-12-03T00:00:00.000Z", + "published": "2020-12-03T00:00:00.000Z", + "updated": "2023-02-02T00:00:00.000Z", + "description": "com.fasterxml.jackson.core:jackson-databind is a library which contains the general-purpose data-binding functionality and tree-model for Jackson Data Processor.\n\nAffected versions of this package are vulnerable to XML External Entity (XXE) Injection. A flaw was found in FasterXML Jackson Databind, where it does not have entity expansion secured properly in the DOMDeserializer class. The highest threat from this vulnerability is data integrity.", + "detail": "XXE Injection is a type of attack against an application that parses XML input. XML is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. By default, many XML processors allow specification of an external entity, a URI that is dereferenced and evaluated during XML processing. When an XML document is being parsed, the parser can make a request and include the content at the specified URI inside of the XML document.\n\nAttacks can include disclosing local files, which may contain sensitive data such as passwords or private user data, using file: schemes or relative paths in the system identifier.", + "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.6.7.4, 2.9.10.7, 2.10.5.1 or higher.", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25649" + }, + "references": [ + { + "id": "1887664", + "source": { + "name": "Red Hat, Inc.", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1887664" + } + }, + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302", + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + } + } + ], + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&version=3.1" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + }, + "score": 8.2, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "Acme Inc", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "advisories": [ + { + "title": "GitHub Commit", + "url": "https://github.com/FasterXML/jackson-databind/commit/612f971b78c60202e9cd75a299050c8f2d724a59" + }, + { + "title": "GitHub Issue", + "url": "https://github.com/FasterXML/jackson-databind/issues/2589" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1887664" + } + ], + "credits": { + "individuals": [ + { + "name": "Bartosz Baranowski" + } + ] + }, + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable", + "response": ["will_not_fix", "update"], + "detail": "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly." + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.10.0?type=jar" + } + ] + }, + { + "id": "CVE-2022-42003", + "cwes": [502], + "description": "In FasterXML jackson-databind before 2.14.0-rc1, resource exhaustion can occur because of a lack of a check in primitive value deserializers to avoid deep wrapper array nesting, when the UNWRAP_SINGLE_VALUE_ARRAYS feature is enabled. Additional fix version in 2.13.4.1 and 2.12.17.1", + "detail": "", + "recommendation": "", + "created": "2022-10-02T00:00:00.000Z", + "published": "2022-10-02T00:00:00.000Z", + "updated": "2022-10-02T00:00:00.000Z", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-42003" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2022-42003&vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H&version=3.1&source=NIST" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "references": [ + { + "id": "51020", + "source": { + "name": "oss-fuzz", + "url": "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=51020" + } + } + ], + "advisories": [ + { + "title": "GitHub Issue", + "url": "https://github.com/FasterXML/jackson-databind/issues/3590" + }, + { + "title": "GitHub Commit", + "url": "https://github.com/FasterXML/jackson-databind/commit/d78d00ee7b5245b93103fef3187f70543d67ca33" + } + ] + }, + { + "id": "CVE-2022-42004", + "cwes": [502], + "description": "In FasterXML jackson-databind before 2.13.4, resource exhaustion can occur because of a lack of a check in BeanDeserializer._deserializeFromArray to prevent use of deeply nested arrays. An application is vulnerable only with certain customized choices for deserialization.", + "detail": "", + "recommendation": "", + "created": "2022-10-02T00:00:00.000Z", + "published": "2022-10-02T00:00:00.000Z", + "updated": "2022-10-02T00:00:00.000Z", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-42004" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2022-42004&vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H&version=3.1&source=NIST" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "references": [ + { + "id": "50490", + "source": { + "name": "oss-fuzz", + "url": "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50490" + } + } + ], + "advisories": [ + { + "title": "GitHub Issue", + "url": "https://github.com/FasterXML/jackson-databind/issues/3582" + }, + { + "title": "GitHub Commit", + "url": "https://github.com/FasterXML/jackson-databind/commit/063183589218fec19a9293ed2f17ec53ea80ba88" + } + ] + } + ] +} diff --git a/utils/json.go b/utils/json.go index cc417c3a..f03e572b 100644 --- a/utils/json.go +++ b/utils/json.go @@ -84,27 +84,18 @@ func EncodeAnyToIndentedJSON(any interface{}) (outputBuffer bytes.Buffer, err er // TODO: function NOT complete, only placeholder type switch // TODO: allow generic function to be applied to types // func PrintTypes(values ...interface{}) { -// //fmt.Printf("values=%v\n", values) // for index, value := range values { -// fmt.Printf("value[%d] (%T): %+v\n", index, value, value) // switch t := value.(type) { // case nil: -// fmt.Println("Type is nil.") // case int: // case uint: // case int32: // case int64: // case uint64: -// fmt.Println("Type is an integer:", t) // case float32: // case float64: -// fmt.Println("Type is a float:", t) // case string: -// fmt.Println("Type is a string:", t) // case bool: -// fmt.Println("Type is a bool:", t) -// default: -// fmt.Printf("Type is unknown!: %v\n", t) // } // } // }