From 47bbdccb685b47135aedce3075e3ddfcb9849988 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 16 Jun 2023 13:57:30 -0500 Subject: [PATCH 01/28] Create a framework for validation error special case handling Signed-off-by: Matt Rutkowski --- cmd/license_policy_config.go | 16 +-- cmd/validate.go | 162 +++++++++++++++++++++++++---- cmd/validate_cdx_examples_test.go | 42 ++++---- cmd/validate_cdx_test.go | 4 +- cmd/validate_config_test.go | 8 ++ cmd/validate_custom_test.go | 4 +- cmd/validate_spdx_examples_test.go | 14 +-- cmd/validate_spdx_test.go | 2 +- cmd/validate_test.go | 26 ++++- 9 files changed, 213 insertions(+), 65 deletions(-) diff --git a/cmd/license_policy_config.go b/cmd/license_policy_config.go index 7587b572..3c8c443c 100644 --- a/cmd/license_policy_config.go +++ b/cmd/license_policy_config.go @@ -42,16 +42,18 @@ var VALID_USAGE_POLICIES = []string{POLICY_ALLOW, POLICY_DENY, POLICY_NEEDS_REVI var ALL_USAGE_POLICIES = []string{POLICY_ALLOW, POLICY_DENY, POLICY_NEEDS_REVIEW, POLICY_UNDEFINED, POLICY_CONFLICT} // Note: the SPDX spec. does not provide regex for an SPDX ID, but provides the following in ABNF: -// string = 1*(ALPHA / DIGIT / "-" / "." ) +// +// string = 1*(ALPHA / DIGIT / "-" / "." ) +// // Currently, the regex below tests composition of of only // alphanum, "-", and "." characters and disallows empty strings // TODO: -// - First and last chars are not "-" or "." -// - Enforce reasonable min/max lengths -// In theory, we can check overall length with positive lookahead -// (e.g., min 3 max 128): (?=.{3,128}$) -// However, this does not appear to be supported in `regexp` package -// or perhaps it must be a compiled expression TBD +// - First and last chars are not "-" or "." +// - Enforce reasonable min/max lengths +// In theory, we can check overall length with positive lookahead +// (e.g., min 3 max 128): (?=.{3,128}$) +// However, this does not appear to be supported in `regexp` package +// or perhaps it must be a compiled expression TBD const ( REGEX_VALID_SPDX_ID = "^[a-zA-Z0-9.-]+$" ) diff --git a/cmd/validate.go b/cmd/validate.go index 701fe730..9e90e8bb 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -38,17 +38,21 @@ const ( // validation flags const ( - FLAG_SCHEMA_FORCE = "force" - FLAG_SCHEMA_VARIANT = "variant" - FLAG_CUSTOM_VALIDATION = "custom" // TODO: document when no longer experimental - FLAG_ERR_LIMIT = "error-limit" - MSG_SCHEMA_FORCE = "force specified schema file for validation; overrides inferred schema" - MSG_SCHEMA_VARIANT = "select named schema variant (e.g., \"strict\"); variant must be declared in configuration file (i.e., \"config.json\")" - MSG_FLAG_CUSTOM_VALIDATION = "perform custom validation using custom configuration settings (i.e., \"custom.json\")" - MSG_FLAG_ERR_COLORIZE = "Colorize formatted error output (true|false); default true" - MSG_FLAG_ERR_LIMIT = "Limit number of errors output (integer); default 10" + FLAG_VALIDATE_SCHEMA_FORCE = "force" + FLAG_VALIDATE_SCHEMA_VARIANT = "variant" + FLAG_VALIDATE_CUSTOM = "custom" // TODO: document when no longer experimental + FLAG_VALIDATE_ERR_LIMIT = "error-limit" + MSG_VALIDATE_SCHEMA_FORCE = "force specified schema file for validation; overrides inferred schema" + MSG_VALIDATE_SCHEMA_VARIANT = "select named schema variant (e.g., \"strict\"); variant must be declared in configuration file (i.e., \"config.json\")" + MSG_VALIDATE_FLAG_CUSTOM = "perform custom validation using custom configuration settings (i.e., \"custom.json\")" + MSG_VALIDATE_FLAG_ERR_COLORIZE = "Colorize formatted error output (true|false); default true" + MSG_VALIDATE_FLAG_ERR_LIMIT = "Limit number of errors output (integer); default 10" + MSG_VALIDATE_FLAG_ERR_FORMAT = "format error results using the specified format type" ) +var VALIDATE_SUPPORTED_ERROR_FORMATS = MSG_VALIDATE_FLAG_ERR_FORMAT + + strings.Join([]string{FORMAT_TEXT, FORMAT_JSON}, ", ") + " (default: txt)" + // limits const ( DEFAULT_MAX_ERROR_LIMIT = 10 @@ -60,6 +64,10 @@ const ( PROTOCOL_PREFIX_FILE = "file://" ) +type ValidationErrResult struct { + gojsonschema.ResultErrorFields +} + func NewCommandValidate() *cobra.Command { // NOTE: `RunE` function takes precedent over `Run` (anonymous) function if both provided var command = new(cobra.Command) @@ -67,6 +75,8 @@ func NewCommandValidate() *cobra.Command { command.Short = "Validate input file against its declared BOM schema" command.Long = "Validate input file against its declared BOM schema, if detectable and supported." command.RunE = validateCmdImpl + command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", + MSG_VALIDATE_FLAG_ERR_FORMAT+VALIDATE_SUPPORTED_ERROR_FORMATS) command.PreRunE = func(cmd *cobra.Command, args []string) error { @@ -88,13 +98,13 @@ func initCommandValidate(command *cobra.Command) { defer getLogger().Exit() // Force a schema file to use for validation (override inferred schema) - command.Flags().StringVarP(&utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile, FLAG_SCHEMA_FORCE, "", "", MSG_SCHEMA_FORCE) + command.Flags().StringVarP(&utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile, FLAG_VALIDATE_SCHEMA_FORCE, "", "", MSG_VALIDATE_SCHEMA_FORCE) // Optional schema "variant" of inferred schema (e.g, "strict") - command.Flags().StringVarP(&utils.GlobalFlags.Variant, FLAG_SCHEMA_VARIANT, "", "", MSG_SCHEMA_VARIANT) - command.Flags().BoolVarP(&utils.GlobalFlags.CustomValidation, FLAG_CUSTOM_VALIDATION, "", false, MSG_FLAG_CUSTOM_VALIDATION) + command.Flags().StringVarP(&utils.GlobalFlags.Variant, FLAG_VALIDATE_SCHEMA_VARIANT, "", "", MSG_VALIDATE_SCHEMA_VARIANT) + command.Flags().BoolVarP(&utils.GlobalFlags.CustomValidation, FLAG_VALIDATE_CUSTOM, "", false, MSG_VALIDATE_FLAG_CUSTOM) // Colorize default: true (for historical reasons) - command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors, FLAG_COLORIZE_OUTPUT, "", true, MSG_FLAG_ERR_COLORIZE) - command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_FLAG_ERR_LIMIT) + command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) + command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_VALIDATE_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_VALIDATE_FLAG_ERR_LIMIT) } func validateCmdImpl(cmd *cobra.Command, args []string) error { @@ -125,8 +135,8 @@ func validateCmdImpl(cmd *cobra.Command, args []string) error { return nil } -// Normalize error/processValidationResults from the Validate() function -func processValidationResults(document *schema.Sbom, valid bool, err error) { +// Normalize error/normalizeValidationErrorTypes from the Validate() function +func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) { // TODO: if JSON validation resulted in !valid, turn that into an // InvalidSBOMError and test to make sure this works in all cases @@ -163,7 +173,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. // use function closure to assure consistent error output based upon error type defer func() { if err != nil { - processValidationResults(document, valid, err) + normalizeValidationErrorTypes(document, valid, err) } }() @@ -181,7 +191,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. document.GetFilename(), document.FormatInfo.CanonicalName, CMD_VALIDATE, - FLAG_CUSTOM_VALIDATION) + FLAG_VALIDATE_CUSTOM) return valid, document, schemaErrors, err } @@ -272,8 +282,23 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. MSG_SCHEMA_ERRORS, nil, schemaErrors) + + // Format error results + format := utils.GlobalFlags.OutputFormat + var formattedSchemaErrors string + getLogger().Infof("Outputting error results (`%s` format)...", format) + switch format { + case FORMAT_JSON: + formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors) + case FORMAT_TEXT: + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors) + default: + getLogger().Warningf("error results not supported for `%s` format; defaulting to `%s` format...", + format, FORMAT_TEXT) + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors) + } + // Append formatted schema errors "details" to the InvalidSBOMError type - formattedSchemaErrors := FormatSchemaErrors(schemaErrors) errInvalid.Details = formattedSchemaErrors return INVALID, document, schemaErrors, errInvalid @@ -311,7 +336,101 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. return } -func FormatSchemaErrors(errs []gojsonschema.ResultError) string { +func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) (formattedResult string) { + switch resultError.(type) { + case *gojsonschema.FalseError: + case *gojsonschema.RequiredError: + case *gojsonschema.InvalidTypeError: + case *gojsonschema.NumberAnyOfError: + case *gojsonschema.NumberOneOfError: + case *gojsonschema.NumberAllOfError: + case *gojsonschema.NumberNotError: + case *gojsonschema.MissingDependencyError: + case *gojsonschema.InternalError: + case *gojsonschema.ConstError: + case *gojsonschema.EnumError: + case *gojsonschema.ArrayNoAdditionalItemsError: + case *gojsonschema.ArrayMinItemsError: + case *gojsonschema.ArrayMaxItemsError: + case *gojsonschema.ItemsMustBeUniqueError: + getLogger().Infof("ItemsMustBeUniqueError:") + formattedResult, _ = log.FormatInterfaceAsJson(resultError) + case *gojsonschema.ArrayContainsError: + case *gojsonschema.ArrayMinPropertiesError: + case *gojsonschema.ArrayMaxPropertiesError: + case *gojsonschema.AdditionalPropertyNotAllowedError: + case *gojsonschema.InvalidPropertyPatternError: + case *gojsonschema.InvalidPropertyNameError: + case *gojsonschema.StringLengthGTEError: + case *gojsonschema.StringLengthLTEError: + case *gojsonschema.DoesNotMatchPatternError: + case *gojsonschema.DoesNotMatchFormatError: + case *gojsonschema.MultipleOfError: + case *gojsonschema.NumberGTEError: + case *gojsonschema.NumberGTError: + case *gojsonschema.NumberLTEError: + case *gojsonschema.NumberLTError: + case *gojsonschema.ConditionThenError: + case *gojsonschema.ConditionElseError: + default: + if colorize { + formattedResult, _ = log.FormatInterfaceAsColorizedJson(resultError) + } else { + formattedResult, _ = log.FormatInterfaceAsJson(resultError) + } + } + + // err.SetType(t) + // err.SetContext(context) + // err.SetValue(value) + // err.SetDetails(details) + // err.SetDescriptionFormat(d) + // details["field"] = err.Field() + // if _, exists := details["context"]; !exists && context != nil { + // details["context"] = context.String() + // } + // err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) + return +} + +func FormatSchemaErrorsJson(errs []gojsonschema.ResultError) string { + var sb strings.Builder + + lenErrs := len(errs) + if lenErrs > 0 { + errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors + colorize := utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors + + sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):", lenErrs)) + for i, resultError := range errs { + + // short-circuit if we have too many errors + if i == errLimit { + // notify users more errors exist + msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", i, len(errs)) + getLogger().Infof("%s", msg) + // always include limit message in discrete output (i.e., not turned off by --quiet flag) + sb.WriteString("\n" + msg) + break + } + + schemaErrorText := formatSchemaErrorTypes(resultError, colorize) + + // append the numbered schema error + // schemaErrorText := fmt.Sprintf("\n\t%d. Type: [%s], Field: [%s], Description: [%s] %s", + // i+1, + // resultError.Type(), + // resultError.Field(), + // description, + // failingObject) + + sb.WriteString(schemaErrorText) + } + } + return sb.String() +} + +func FormatSchemaErrorsText(errs []gojsonschema.ResultError) string { var sb strings.Builder lenErrs := len(errs) @@ -371,9 +490,6 @@ func FormatSchemaErrors(errs []gojsonschema.ResultError) string { failingObject) sb.WriteString(schemaErrorText) - - // TODO: leave commented out as we do not want to slow processing... - //getLogger().Debugf("processing error (%v): type: `%s`", i, resultError.Type()) } } return sb.String() diff --git a/cmd/validate_cdx_examples_test.go b/cmd/validate_cdx_examples_test.go index c9938d8f..fb22d42d 100644 --- a/cmd/validate_cdx_examples_test.go +++ b/cmd/validate_cdx_examples_test.go @@ -63,86 +63,86 @@ const ( ) func TestValidateExampleCdx14UseCaseAssembly(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_ASSEMBLY, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_ASSEMBLY, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseAuthenticityJsf(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_AUTHENTICITY_JSF, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_AUTHENTICITY_JSF, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseComponentKnownVulnerabilities(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_COMP_KNOWN_VULN, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_COMP_KNOWN_VULN, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseCompositionAndCompleteness(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_COMPOSITION_COMPLETENESS, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_COMPOSITION_COMPLETENESS, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseDependencyGraph(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_DEP_GRAPH, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_DEP_GRAPH, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseExternalReferences(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_EXT_REFS, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_EXT_REFS, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseIntegrityVerification(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_INTEGRITY_VERIFICATION, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_INTEGRITY_VERIFICATION, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseInventory(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_INVENTORY, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_INVENTORY, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseLicenseCompliance(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_LICENSE_COMPLIANCE, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_LICENSE_COMPLIANCE, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseOpenChainConformance(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_OPENCHAIN_CONFORMANCE, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_OPENCHAIN_CONFORMANCE, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCasePackageEvaluation(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PKG_EVALUATION, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PKG_EVALUATION, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCasePackagingDistribution(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PKG_DIST, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PKG_DIST, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCasePedigree(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PEDIGREE, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PEDIGREE, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseProvenance(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PROVENANCE, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_PROVENANCE, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseSecurityAdvisories(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_SEC_ADVISORIES, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_SEC_ADVISORIES, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseServiceDefinition(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_SVC_DEFN, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_SVC_DEFN, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseVulnerabilityExploitation(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_VULN_EXPLOITATION, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_VULN_EXPLOITATION, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleCdx14UseCaseVulnerabilityRemediation(t *testing.T) { - innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_VULN_REMEDIATION, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_EXAMPLE_CDX_1_4_USE_CASE_VULN_REMEDIATION, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } // CycloneDX - Examples func TestValidateExampleBomCdx12NpmJuiceShop(t *testing.T) { - innerValidateError(t, TEST_CDX_1_2_EXAMPLE_BOM_NPM_JUICE_SHOP, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_CDX_1_2_EXAMPLE_BOM_NPM_JUICE_SHOP, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleBomCdx13Laravel(t *testing.T) { - innerValidateError(t, TEST_CDX_1_3_EXAMPLE_BOM_LARAVEL, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_CDX_1_3_EXAMPLE_BOM_LARAVEL, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateExampleSaaSBomCdx14ApiGatewayDatastores(t *testing.T) { - innerValidateError(t, TEST_CDX_1_4_EXAMPLE_SAASBOM_APIGW_MS_DATASTORES, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_CDX_1_4_EXAMPLE_SAASBOM_APIGW_MS_DATASTORES, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } diff --git a/cmd/validate_cdx_test.go b/cmd/validate_cdx_test.go index d528b87d..fdc76970 100644 --- a/cmd/validate_cdx_test.go +++ b/cmd/validate_cdx_test.go @@ -37,11 +37,11 @@ const ( // ----------------------------------------------------------- func TestValidateCdx13MinRequiredBasic(t *testing.T) { - innerValidateError(t, TEST_CDX_1_3_MIN_REQUIRED, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_CDX_1_3_MIN_REQUIRED, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateCdx14MinRequiredBasic(t *testing.T) { - innerValidateError(t, TEST_CDX_1_4_MIN_REQUIRED, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_CDX_1_4_MIN_REQUIRED, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } // ----------------------------------------------------------- diff --git a/cmd/validate_config_test.go b/cmd/validate_config_test.go index f41448e9..63c19ce8 100644 --- a/cmd/validate_config_test.go +++ b/cmd/validate_config_test.go @@ -42,6 +42,7 @@ func TestValidateConfigInvalidFormatKey(t *testing.T) { innerValidateError(t, TEST_INVALID_FORMAT_KEY_FOO, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedFormatError{}) } @@ -49,6 +50,7 @@ func TestValidateConfigInvalidVersion(t *testing.T) { innerValidateError(t, TEST_CDX_SPEC_VERSION_INVALID, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedSchemaError{}) } @@ -56,6 +58,7 @@ func TestValidateConfigInvalidVariant(t *testing.T) { innerValidateError(t, TEST_CDX_1_4_MIN_REQUIRED, "foo", + FORMAT_TEXT, &schema.UnsupportedSchemaError{}) } @@ -63,6 +66,7 @@ func TestValidateConfigCDXBomFormatInvalid(t *testing.T) { innerValidateError(t, TEST_CDX_BOM_FORMAT_INVALID, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedFormatError{}) } @@ -70,6 +74,7 @@ func TestValidateConfigCDXBomFormatMissing(t *testing.T) { innerValidateError(t, TEST_CDX_BOM_FORMAT_MISSING, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedFormatError{}) } @@ -77,6 +82,7 @@ func TestValidateConfigCDXSpecVersionMissing(t *testing.T) { innerValidateError(t, TEST_CDX_SPEC_VERSION_MISSING, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedSchemaError{}) } @@ -84,6 +90,7 @@ func TestValidateConfigSPDXSpdxIdInvalid(t *testing.T) { innerValidateError(t, TEST_SPDX_SPDX_ID_INVALID, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedFormatError{}) } @@ -91,5 +98,6 @@ func TestValidateConfigSPDXSpdxVersionInvalid(t *testing.T) { innerValidateError(t, TEST_SPDX_SPDX_VERSION_MISSING, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &schema.UnsupportedSchemaError{}) } diff --git a/cmd/validate_custom_test.go b/cmd/validate_custom_test.go index 5234e9b4..0673ddab 100644 --- a/cmd/validate_custom_test.go +++ b/cmd/validate_custom_test.go @@ -58,7 +58,7 @@ const ( func innerCustomValidateError(t *testing.T, filename string, variant string, innerError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { utils.GlobalFlags.CustomValidation = true - document, schemaErrors, actualError = innerValidateError(t, filename, variant, innerError) + document, schemaErrors, actualError = innerValidateError(t, filename, variant, FORMAT_TEXT, innerError) utils.GlobalFlags.CustomValidation = false return } @@ -103,6 +103,7 @@ func TestValidateCustomCdx14MetadataPropsMissingDisclaimer(t *testing.T) { document, results, _ := innerValidateError(t, TEST_CUSTOM_CDX_1_4_METADATA_PROPS_DISCLAIMER_MISSING, SCHEMA_VARIANT_CUSTOM, + FORMAT_TEXT, &InvalidSBOMError{}) getLogger().Debugf("filename: `%s`, results:\n%v", document.GetFilename(), results) } @@ -111,6 +112,7 @@ func TestValidateCustomCdx14MetadataPropsMissingClassification(t *testing.T) { document, results, _ := innerValidateError(t, TEST_CUSTOM_CDX_1_4_METADATA_PROPS_CLASSIFICATION_MISSING, SCHEMA_VARIANT_CUSTOM, + FORMAT_TEXT, &InvalidSBOMError{}) getLogger().Debugf("filename: `%s`, results:\n%v", document.GetFilename(), results) } diff --git a/cmd/validate_spdx_examples_test.go b/cmd/validate_spdx_examples_test.go index ef7b0872..47c5727d 100644 --- a/cmd/validate_spdx_examples_test.go +++ b/cmd/validate_spdx_examples_test.go @@ -32,29 +32,29 @@ const ( // SPDX - Examples func TestValidateSpdx22Example1(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_1, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_1, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example2Bin(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_2_BIN, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_2_BIN, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example2Src(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_2_SRC, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_2_SRC, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example5Bin(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_5_BIN, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_5_BIN, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example5Src(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_5_SRC, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_5_SRC, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example6Lib(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_6_LIB, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_6_LIB, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } func TestValidateSPDX22Example6Src(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_6_SRC, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_EXAMPLE_6_SRC, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } diff --git a/cmd/validate_spdx_test.go b/cmd/validate_spdx_test.go index ac279013..077d3263 100644 --- a/cmd/validate_spdx_test.go +++ b/cmd/validate_spdx_test.go @@ -41,7 +41,7 @@ const ( // TODO: Need an SPDX 2.2.1 variant // TODO: Need an SPDX 2.2 "custom" variant func TestValidateSpdx22MinRequiredBasic(t *testing.T) { - innerValidateError(t, TEST_SPDX_2_2_MIN_REQUIRED, SCHEMA_VARIANT_NONE, nil) + innerValidateError(t, TEST_SPDX_2_2_MIN_REQUIRED, SCHEMA_VARIANT_NONE, FORMAT_TEXT, nil) } // ----------------------------------------------------------- diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 8e1f4b69..bbd33f2b 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -44,8 +44,12 @@ const ( TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE = "test/cyclonedx/cdx-1-4-mature-example-1.json" ) +const ( + TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE = "test/validation/cdx-1-4-validate-err-components-unique-items-1.json" +) + // Tests basic validation and expected errors -func innerValidateError(t *testing.T, filename string, variant string, expectedError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { +func innerValidateError(t *testing.T, filename string, variant string, format string, expectedError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { getLogger().Enter() defer getLogger().Exit() @@ -53,6 +57,8 @@ func innerValidateError(t *testing.T, filename string, variant string, expectedE utils.GlobalFlags.InputFile = filename // Set the schema variant where the command line flag would utils.GlobalFlags.Variant = variant + // Set the err result format + utils.GlobalFlags.OutputFormat = format // Invoke the actual validate function var isValid bool @@ -93,7 +99,7 @@ func innerValidateInvalidSBOMInnerError(t *testing.T, filename string, variant s getLogger().Enter() defer getLogger().Exit() - document, schemaErrors, actualError = innerValidateError(t, filename, variant, &InvalidSBOMError{}) + document, schemaErrors, actualError = innerValidateError(t, filename, variant, FORMAT_TEXT, &InvalidSBOMError{}) invalidSBOMError, ok := actualError.(*InvalidSBOMError) @@ -109,7 +115,7 @@ func innerValidateInvalidSBOMInnerError(t *testing.T, filename string, variant s // It also tests that the syntax error occurred at the expected line number and character offset func innerValidateSyntaxError(t *testing.T, filename string, variant string, expectedLineNum int, expectedCharNum int) (document *schema.Sbom, actualError error) { - document, _, actualError = innerValidateError(t, filename, variant, &json.SyntaxError{}) + document, _, actualError = innerValidateError(t, filename, variant, FORMAT_TEXT, &json.SyntaxError{}) syntaxError, ok := actualError.(*json.SyntaxError) if !ok { @@ -136,6 +142,7 @@ func innerTestSchemaErrorAndErrorResults(t *testing.T, document, results, _ := innerValidateError(t, filename, variant, + FORMAT_TEXT, &InvalidSBOMError{}) getLogger().Debugf("filename: `%s`, results:\n%v", document.GetFilename(), results) @@ -160,6 +167,7 @@ func TestValidateInvalidInputFileLoad(t *testing.T) { innerValidateError(t, TEST_INPUT_FILE_NON_EXISTENT, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, &fs.PathError{}) } @@ -197,6 +205,7 @@ func TestValidateForceCustomSchemaCdx13(t *testing.T) { innerValidateError(t, TEST_CDX_1_3_MATURITY_EXAMPLE_1_BASE, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, nil) } @@ -206,6 +215,7 @@ func TestValidateForceCustomSchemaCdx14(t *testing.T) { innerValidateError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, nil) } @@ -215,6 +225,7 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { innerValidateError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, SCHEMA_VARIANT_NONE, + FORMAT_TEXT, nil) } @@ -224,3 +235,12 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { // SCHEMA_VARIANT_NONE, // nil) // } + +func TestValidateCdx14ComponentsUniqueJsonResults(t *testing.T) { + //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM + innerValidateError(t, + TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, + SCHEMA_VARIANT_NONE, + FORMAT_JSON, + nil) +} From 26880486714e0c31172c7f4b60ffead56e6e85fe Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 16 Jun 2023 14:55:01 -0500 Subject: [PATCH 02/28] Create a framework for validation error special case handling Signed-off-by: Matt Rutkowski --- cmd/validate.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 9e90e8bb..79566436 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -337,6 +337,15 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. } func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) (formattedResult string) { + + var jsonErrorMap = make(map[string]interface{}) + jsonErrorMap["type"] = resultError.Type() + jsonErrorMap["context"] = resultError.Context() + jsonErrorMap["value"] = resultError.Value() + jsonErrorMap["details"] = resultError.Details() + jsonErrorMap["description"] = resultError.Description() + jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() + switch resultError.(type) { case *gojsonschema.FalseError: case *gojsonschema.RequiredError: @@ -354,7 +363,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) case *gojsonschema.ArrayMaxItemsError: case *gojsonschema.ItemsMustBeUniqueError: getLogger().Infof("ItemsMustBeUniqueError:") - formattedResult, _ = log.FormatInterfaceAsJson(resultError) + formattedResult, _ = log.FormatInterfaceAsJson(jsonErrorMap) case *gojsonschema.ArrayContainsError: case *gojsonschema.ArrayMinPropertiesError: case *gojsonschema.ArrayMaxPropertiesError: @@ -374,16 +383,12 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) case *gojsonschema.ConditionElseError: default: if colorize { - formattedResult, _ = log.FormatInterfaceAsColorizedJson(resultError) + formattedResult, _ = log.FormatInterfaceAsColorizedJson(jsonErrorMap) } else { - formattedResult, _ = log.FormatInterfaceAsJson(resultError) + formattedResult, _ = log.FormatInterfaceAsJson(jsonErrorMap) } } - // err.SetType(t) - // err.SetContext(context) - // err.SetValue(value) - // err.SetDetails(details) // err.SetDescriptionFormat(d) // details["field"] = err.Field() // if _, exists := details["context"]; !exists && context != nil { From 8369472349613287e8151b6eaa4455b518178b8d Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 19 Jun 2023 13:31:15 -0500 Subject: [PATCH 03/28] Adjust JSON output formatting as an array Signed-off-by: Matt Rutkowski --- cmd/validate.go | 143 ++++++++++++++++++++----------------------- cmd/validate_test.go | 37 +++++++++++ 2 files changed, 105 insertions(+), 75 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 79566436..82d0b36e 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -64,8 +64,47 @@ const ( PROTOCOL_PREFIX_FILE = "file://" ) +// JsonContext implements a persistent linked-list of strings type ValidationErrResult struct { - gojsonschema.ResultErrorFields + Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() + Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() + DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() + Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() + Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() + Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() +} + +func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationErrResult) { + // var jsonErrorMap = make(map[string]interface{}) + validationErrResult = &ValidationErrResult{ + Type: resultError.Type(), + Description: resultError.Description(), + DescriptionFormat: resultError.DescriptionFormat(), + Context: resultError.Context(), + Value: resultError.Value(), + Details: resultError.Details(), + } + return +} + +// details["field"] = err.Field() +// +// if _, exists := details["context"]; !exists && context != nil { +// details["context"] = context.String() +// } +// +// err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) +func (result *ValidationErrResult) Format(showValue bool, showContext bool, colorize bool) string { + + var sb strings.Builder + + formattedResult, err := log.FormatInterfaceAsJson(result) + if err != nil { + return fmt.Sprintf("formatting error: %s", err.Error()) + } + sb.WriteString(formattedResult) + + return sb.String() } func NewCommandValidate() *cobra.Command { @@ -286,7 +325,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. // Format error results format := utils.GlobalFlags.OutputFormat var formattedSchemaErrors string - getLogger().Infof("Outputting error results (`%s` format)...", format) + getLogger().Infof("Outputting error results (`%s` format)...\n", format) switch format { case FORMAT_JSON: formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors) @@ -338,13 +377,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) (formattedResult string) { - var jsonErrorMap = make(map[string]interface{}) - jsonErrorMap["type"] = resultError.Type() - jsonErrorMap["context"] = resultError.Context() - jsonErrorMap["value"] = resultError.Value() - jsonErrorMap["details"] = resultError.Details() - jsonErrorMap["description"] = resultError.Description() - jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() + validationErrorResult := NewValidationErrResult(resultError) switch resultError.(type) { case *gojsonschema.FalseError: @@ -363,7 +396,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) case *gojsonschema.ArrayMaxItemsError: case *gojsonschema.ItemsMustBeUniqueError: getLogger().Infof("ItemsMustBeUniqueError:") - formattedResult, _ = log.FormatInterfaceAsJson(jsonErrorMap) + formattedResult = validationErrorResult.Format(true, true, colorize) case *gojsonschema.ArrayContainsError: case *gojsonschema.ArrayMinPropertiesError: case *gojsonschema.ArrayMaxPropertiesError: @@ -382,19 +415,9 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) case *gojsonschema.ConditionThenError: case *gojsonschema.ConditionElseError: default: - if colorize { - formattedResult, _ = log.FormatInterfaceAsColorizedJson(jsonErrorMap) - } else { - formattedResult, _ = log.FormatInterfaceAsJson(jsonErrorMap) - } + formattedResult = validationErrorResult.Format(true, true, colorize) } - // err.SetDescriptionFormat(d) - // details["field"] = err.Field() - // if _, exists := details["context"]; !exists && context != nil { - // details["context"] = context.String() - // } - // err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) return } @@ -403,35 +426,42 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError) string { lenErrs := len(errs) if lenErrs > 0 { + sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):\n", lenErrs)) errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors colorize := utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors - sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):", lenErrs)) - for i, resultError := range errs { + // If we have more errors than the (default or user set) limit; notify user + if lenErrs > errLimit { + // notify users more errors exist + msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) + getLogger().Infof("%s", msg) + } - // short-circuit if we have too many errors - if i == errLimit { - // notify users more errors exist - msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", i, len(errs)) - getLogger().Infof("%s", msg) - // always include limit message in discrete output (i.e., not turned off by --quiet flag) - sb.WriteString("\n" + msg) + if lenErrs > 1 { + sb.WriteString("[\n") + } + + for i, resultError := range errs { + // short-circuit if too many errors (i.e., using the error limit flag value) + if i > errLimit { break } + // add to the result errors schemaErrorText := formatSchemaErrorTypes(resultError, colorize) + sb.WriteString(schemaErrorText) - // append the numbered schema error - // schemaErrorText := fmt.Sprintf("\n\t%d. Type: [%s], Field: [%s], Description: [%s] %s", - // i+1, - // resultError.Type(), - // resultError.Field(), - // description, - // failingObject) + if i < (lenErrs-1) && i < (errLimit-1) { + sb.WriteString(",") + sb.WriteString(fmt.Sprintf("i: %v, errLimit: %v", i, errLimit)) + } + } - sb.WriteString(schemaErrorText) + if lenErrs > 1 { + sb.WriteString("\n]") } } + return sb.String() } @@ -499,40 +529,3 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError) string { } return sb.String() } - -func schemaErrorExists(schemaErrors []gojsonschema.ResultError, - expectedType string, expectedField string, expectedValue interface{}) bool { - - for i, resultError := range schemaErrors { - // Some descriptions include very long enums; in those cases, - // truncate to a reasonable length using an intelligent separator - getLogger().Tracef(">> %d. Type: [%s], Field: [%s], Value: [%v]", - i+1, - resultError.Type(), - resultError.Field(), - resultError.Value()) - - actualType := resultError.Type() - actualField := resultError.Field() - actualValue := resultError.Value() - - if actualType == expectedType { - // we have matched on the type (key) field, continue to match other fields - if expectedField != "" && - actualField != expectedField { - getLogger().Tracef("expected Field: `%s`; actual Field: `%s`", expectedField, actualField) - return false - } - - if expectedValue != "" && - actualValue != expectedValue { - getLogger().Tracef("expected Value: `%s`; actual Value: `%s`", actualValue, expectedValue) - return false - } - return true - } else { - getLogger().Debugf("Skipping result[%d]: expected Type: `%s`; actual Type: `%s`", i, expectedType, actualType) - } - } - return false -} diff --git a/cmd/validate_test.go b/cmd/validate_test.go index bbd33f2b..55f19489 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -157,6 +157,43 @@ func innerTestSchemaErrorAndErrorResults(t *testing.T, } } +func schemaErrorExists(schemaErrors []gojsonschema.ResultError, + expectedType string, expectedField string, expectedValue interface{}) bool { + + for i, resultError := range schemaErrors { + // Some descriptions include very long enums; in those cases, + // truncate to a reasonable length using an intelligent separator + getLogger().Tracef(">> %d. Type: [%s], Field: [%s], Value: [%v]", + i+1, + resultError.Type(), + resultError.Field(), + resultError.Value()) + + actualType := resultError.Type() + actualField := resultError.Field() + actualValue := resultError.Value() + + if actualType == expectedType { + // we have matched on the type (key) field, continue to match other fields + if expectedField != "" && + actualField != expectedField { + getLogger().Tracef("expected Field: `%s`; actual Field: `%s`", expectedField, actualField) + return false + } + + if expectedValue != "" && + actualValue != expectedValue { + getLogger().Tracef("expected Value: `%s`; actual Value: `%s`", actualValue, expectedValue) + return false + } + return true + } else { + getLogger().Debugf("Skipping result[%d]: expected Type: `%s`; actual Type: `%s`", i, expectedType, actualType) + } + } + return false +} + // ----------------------------------------------------------- // Command tests // ----------------------------------------------------------- From bfa698599420eddba80dadf969bc584a7348ff8d Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 19 Jun 2023 17:35:28 -0500 Subject: [PATCH 04/28] Use an ordered map to control JSON output marshaling order Signed-off-by: Matt Rutkowski --- cmd/validate.go | 83 +++++++++++++++++++++++++++---------------------- go.mod | 1 + go.sum | 2 ++ 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 82d0b36e..5c1b26bd 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -17,6 +17,7 @@ package cmd +// "github.com/iancoleman/orderedmap" import ( "encoding/json" "fmt" @@ -27,6 +28,7 @@ import ( "github.com/CycloneDX/sbom-utility/resources" "github.com/CycloneDX/sbom-utility/schema" "github.com/CycloneDX/sbom-utility/utils" + "github.com/iancoleman/orderedmap" "github.com/spf13/cobra" "github.com/xeipuuv/gojsonschema" ) @@ -66,7 +68,9 @@ const ( // JsonContext implements a persistent linked-list of strings type ValidationErrResult struct { + resultMap *orderedmap.OrderedMap Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() + Field string `json:"field"` // details["field"] = err.Field() Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() @@ -75,18 +79,25 @@ type ValidationErrResult struct { } func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationErrResult) { - // var jsonErrorMap = make(map[string]interface{}) validationErrResult = &ValidationErrResult{ - Type: resultError.Type(), - Description: resultError.Description(), DescriptionFormat: resultError.DescriptionFormat(), Context: resultError.Context(), Value: resultError.Value(), Details: resultError.Details(), } + validationErrResult.resultMap = orderedmap.New() + validationErrResult.resultMap.Set("type", resultError.Type()) + validationErrResult.resultMap.Set("field", resultError.Field()) + validationErrResult.resultMap.Set("context", validationErrResult.Context.String()) + validationErrResult.resultMap.Set("description", resultError.Description()) + return } +func (validationErrResult *ValidationErrResult) MarshalJSON() (marshalled []byte, err error) { + return validationErrResult.resultMap.MarshalJSON() +} + // details["field"] = err.Field() // // if _, exists := details["context"]; !exists && context != nil { @@ -98,7 +109,7 @@ func (result *ValidationErrResult) Format(showValue bool, showContext bool, colo var sb strings.Builder - formattedResult, err := log.FormatInterfaceAsJson(result) + formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) } @@ -380,40 +391,39 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) validationErrorResult := NewValidationErrResult(resultError) switch resultError.(type) { - case *gojsonschema.FalseError: - case *gojsonschema.RequiredError: - case *gojsonschema.InvalidTypeError: - case *gojsonschema.NumberAnyOfError: - case *gojsonschema.NumberOneOfError: - case *gojsonschema.NumberAllOfError: - case *gojsonschema.NumberNotError: - case *gojsonschema.MissingDependencyError: - case *gojsonschema.InternalError: - case *gojsonschema.ConstError: - case *gojsonschema.EnumError: - case *gojsonschema.ArrayNoAdditionalItemsError: - case *gojsonschema.ArrayMinItemsError: - case *gojsonschema.ArrayMaxItemsError: + // case *gojsonschema.AdditionalPropertyNotAllowedError: + // case *gojsonschema.ArrayContainsError: + // case *gojsonschema.ArrayMaxItemsError: + // case *gojsonschema.ArrayMaxPropertiesError: + // case *gojsonschema.ArrayMinItemsError: + // case *gojsonschema.ArrayMinPropertiesError: + // case *gojsonschema.ArrayNoAdditionalItemsError: + // case *gojsonschema.ConditionElseError: + // case *gojsonschema.ConditionThenError: + // case *gojsonschema.ConstError: + // case *gojsonschema.DoesNotMatchFormatError: + // case *gojsonschema.DoesNotMatchPatternError: + // case *gojsonschema.EnumError: + // case *gojsonschema.FalseError: + // case *gojsonschema.InternalError: + // case *gojsonschema.InvalidPropertyNameError: + // case *gojsonschema.InvalidPropertyPatternError: + // case *gojsonschema.InvalidTypeError: case *gojsonschema.ItemsMustBeUniqueError: - getLogger().Infof("ItemsMustBeUniqueError:") formattedResult = validationErrorResult.Format(true, true, colorize) - case *gojsonschema.ArrayContainsError: - case *gojsonschema.ArrayMinPropertiesError: - case *gojsonschema.ArrayMaxPropertiesError: - case *gojsonschema.AdditionalPropertyNotAllowedError: - case *gojsonschema.InvalidPropertyPatternError: - case *gojsonschema.InvalidPropertyNameError: - case *gojsonschema.StringLengthGTEError: - case *gojsonschema.StringLengthLTEError: - case *gojsonschema.DoesNotMatchPatternError: - case *gojsonschema.DoesNotMatchFormatError: - case *gojsonschema.MultipleOfError: - case *gojsonschema.NumberGTEError: - case *gojsonschema.NumberGTError: - case *gojsonschema.NumberLTEError: - case *gojsonschema.NumberLTError: - case *gojsonschema.ConditionThenError: - case *gojsonschema.ConditionElseError: + // case *gojsonschema.MissingDependencyError: + // case *gojsonschema.MultipleOfError: + // case *gojsonschema.NumberAllOfError: + // case *gojsonschema.NumberAnyOfError: + // case *gojsonschema.NumberGTEError: + // case *gojsonschema.NumberGTError: + // case *gojsonschema.NumberLTEError: + // case *gojsonschema.NumberLTError: + // case *gojsonschema.NumberNotError: + // case *gojsonschema.NumberOneOfError: + // case *gojsonschema.RequiredError: + // case *gojsonschema.StringLengthGTEError: + // case *gojsonschema.StringLengthLTEError: default: formattedResult = validationErrorResult.Format(true, true, colorize) } @@ -453,7 +463,6 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError) string { if i < (lenErrs-1) && i < (errLimit-1) { sb.WriteString(",") - sb.WriteString(fmt.Sprintf("i: %v, errLimit: %v", i, errLimit)) } } diff --git a/go.mod b/go.mod index 2dd180af..e4602ec6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/fatih/color v1.15.0 github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f + github.com/iancoleman/orderedmap v0.2.0 github.com/jwangsadinata/go-multimap v0.0.0-20190620162914-c29f3d7f33b6 github.com/mrutkows/go-jsondiff v0.2.0 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 75cbc322..d558c399 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= +github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jwangsadinata/go-multimap v0.0.0-20190620162914-c29f3d7f33b6 h1:OzCtZaD1uI5Fc1C+4oNAp7kZ4ibh5OIgxI29moH/IbE= From 7776c34db9f5dd10b2523ef5cd1d288112937e61 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 19 Jun 2023 17:46:49 -0500 Subject: [PATCH 05/28] Use an ordered map to control JSON output marshaling order Signed-off-by: Matt Rutkowski --- cmd/validate.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 5c1b26bd..c78bccf6 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -66,7 +66,7 @@ const ( PROTOCOL_PREFIX_FILE = "file://" ) -// JsonContext implements a persistent linked-list of strings +// JsonContext is a linked-list of JSON key strings type ValidationErrResult struct { resultMap *orderedmap.OrderedMap Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() @@ -79,16 +79,20 @@ type ValidationErrResult struct { } func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationErrResult) { + // Prepare values that are optionally output as JSON validationErrResult = &ValidationErrResult{ DescriptionFormat: resultError.DescriptionFormat(), Context: resultError.Context(), Value: resultError.Value(), Details: resultError.Details(), } + // Prepare for JSON output by adding all required fields to our ordered map validationErrResult.resultMap = orderedmap.New() validationErrResult.resultMap.Set("type", resultError.Type()) validationErrResult.resultMap.Set("field", resultError.Field()) - validationErrResult.resultMap.Set("context", validationErrResult.Context.String()) + if validationErrResult.Context != nil { + validationErrResult.resultMap.Set("context", validationErrResult.Context.String()) + } validationErrResult.resultMap.Set("description", resultError.Description()) return @@ -105,10 +109,15 @@ func (validationErrResult *ValidationErrResult) MarshalJSON() (marshalled []byte // } // // err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) -func (result *ValidationErrResult) Format(showValue bool, showContext bool, colorize bool) string { +func (result *ValidationErrResult) Format(showValue bool, colorize bool) string { var sb strings.Builder + // Conditionally, add optional values as requested + if showValue { + result.resultMap.Set("value", result.Value) + } + formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) @@ -410,7 +419,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) // case *gojsonschema.InvalidPropertyPatternError: // case *gojsonschema.InvalidTypeError: case *gojsonschema.ItemsMustBeUniqueError: - formattedResult = validationErrorResult.Format(true, true, colorize) + formattedResult = validationErrorResult.Format(true, colorize) // case *gojsonschema.MissingDependencyError: // case *gojsonschema.MultipleOfError: // case *gojsonschema.NumberAllOfError: @@ -425,7 +434,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) // case *gojsonschema.StringLengthGTEError: // case *gojsonschema.StringLengthLTEError: default: - formattedResult = validationErrorResult.Format(true, true, colorize) + formattedResult = validationErrorResult.Format(true, colorize) } return From 09bf70aff9a7734921b5bdeab40adc70ec4d346a Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 19 Jun 2023 17:47:31 -0500 Subject: [PATCH 06/28] Use an ordered map to control JSON output marshaling order Signed-off-by: Matt Rutkowski --- cmd/validate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/validate.go b/cmd/validate.go index c78bccf6..cf879ac3 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -471,7 +471,7 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError) string { sb.WriteString(schemaErrorText) if i < (lenErrs-1) && i < (errLimit-1) { - sb.WriteString(",") + sb.WriteString(",\n") } } From fd9cba38665501c605986d64752e60f071e9ff05 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 19 Jun 2023 17:58:07 -0500 Subject: [PATCH 07/28] Use an ordered map to control JSON output marshaling order Signed-off-by: Matt Rutkowski --- cmd/validate.go | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index cf879ac3..e2780409 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -69,29 +69,27 @@ const ( // JsonContext is a linked-list of JSON key strings type ValidationErrResult struct { resultMap *orderedmap.OrderedMap + ResultError gojsonschema.ResultError + Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() Field string `json:"field"` // details["field"] = err.Field() Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() - Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() } func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationErrResult) { // Prepare values that are optionally output as JSON validationErrResult = &ValidationErrResult{ - DescriptionFormat: resultError.DescriptionFormat(), - Context: resultError.Context(), - Value: resultError.Value(), - Details: resultError.Details(), + ResultError: resultError, } // Prepare for JSON output by adding all required fields to our ordered map validationErrResult.resultMap = orderedmap.New() validationErrResult.resultMap.Set("type", resultError.Type()) validationErrResult.resultMap.Set("field", resultError.Field()) - if validationErrResult.Context != nil { - validationErrResult.resultMap.Set("context", validationErrResult.Context.String()) + if context := resultError.Context(); context != nil { + validationErrResult.resultMap.Set("context", resultError.Context().String()) } validationErrResult.resultMap.Set("description", resultError.Description()) @@ -102,20 +100,31 @@ func (validationErrResult *ValidationErrResult) MarshalJSON() (marshalled []byte return validationErrResult.resultMap.MarshalJSON() } -// details["field"] = err.Field() -// -// if _, exists := details["context"]; !exists && context != nil { -// details["context"] = context.String() -// } -// -// err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) func (result *ValidationErrResult) Format(showValue bool, colorize bool) string { var sb strings.Builder // Conditionally, add optional values as requested if showValue { - result.resultMap.Set("value", result.Value) + result.resultMap.Set("value", result.ResultError.Value()) + } + + formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) + if err != nil { + return fmt.Sprintf("formatting error: %s", err.Error()) + } + sb.WriteString(formattedResult) + + return sb.String() +} + +func (result *ValidationErrResult) FormatItemsMustBeUniqueError(showValue bool, colorize bool) string { + + var sb strings.Builder + + // Conditionally, add optional values as requested + if showValue { + result.resultMap.Set("value", result.ResultError.Value()) } formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) @@ -419,7 +428,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) // case *gojsonschema.InvalidPropertyPatternError: // case *gojsonschema.InvalidTypeError: case *gojsonschema.ItemsMustBeUniqueError: - formattedResult = validationErrorResult.Format(true, colorize) + formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(true, colorize) // case *gojsonschema.MissingDependencyError: // case *gojsonschema.MultipleOfError: // case *gojsonschema.NumberAllOfError: From bc89b660ec7e85ca590361849af5de641a10df5a Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Tue, 20 Jun 2023 08:44:54 -0500 Subject: [PATCH 08/28] Separate format related functions into their own file Signed-off-by: Matt Rutkowski --- cmd/validate.go | 219 ++---------------------------------------------- utils/flags.go | 4 +- 2 files changed, 9 insertions(+), 214 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index e2780409..9815169e 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -24,7 +24,6 @@ import ( "os" "strings" - "github.com/CycloneDX/sbom-utility/log" "github.com/CycloneDX/sbom-utility/resources" "github.com/CycloneDX/sbom-utility/schema" "github.com/CycloneDX/sbom-utility/utils" @@ -66,22 +65,9 @@ const ( PROTOCOL_PREFIX_FILE = "file://" ) -// JsonContext is a linked-list of JSON key strings -type ValidationErrResult struct { - resultMap *orderedmap.OrderedMap - ResultError gojsonschema.ResultError - Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() - Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() - Field string `json:"field"` // details["field"] = err.Field() - Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() - DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() - Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() - Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() -} - -func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationErrResult) { +func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationResultFormat) { // Prepare values that are optionally output as JSON - validationErrResult = &ValidationErrResult{ + validationErrResult = &ValidationResultFormat{ ResultError: resultError, } // Prepare for JSON output by adding all required fields to our ordered map @@ -96,46 +82,6 @@ func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErr return } -func (validationErrResult *ValidationErrResult) MarshalJSON() (marshalled []byte, err error) { - return validationErrResult.resultMap.MarshalJSON() -} - -func (result *ValidationErrResult) Format(showValue bool, colorize bool) string { - - var sb strings.Builder - - // Conditionally, add optional values as requested - if showValue { - result.resultMap.Set("value", result.ResultError.Value()) - } - - formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) - if err != nil { - return fmt.Sprintf("formatting error: %s", err.Error()) - } - sb.WriteString(formattedResult) - - return sb.String() -} - -func (result *ValidationErrResult) FormatItemsMustBeUniqueError(showValue bool, colorize bool) string { - - var sb strings.Builder - - // Conditionally, add optional values as requested - if showValue { - result.resultMap.Set("value", result.ResultError.Value()) - } - - formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) - if err != nil { - return fmt.Sprintf("formatting error: %s", err.Error()) - } - sb.WriteString(formattedResult) - - return sb.String() -} - func NewCommandValidate() *cobra.Command { // NOTE: `RunE` function takes precedent over `Run` (anonymous) function if both provided var command = new(cobra.Command) @@ -171,7 +117,7 @@ func initCommandValidate(command *cobra.Command) { command.Flags().StringVarP(&utils.GlobalFlags.Variant, FLAG_VALIDATE_SCHEMA_VARIANT, "", "", MSG_VALIDATE_SCHEMA_VARIANT) command.Flags().BoolVarP(&utils.GlobalFlags.CustomValidation, FLAG_VALIDATE_CUSTOM, "", false, MSG_VALIDATE_FLAG_CUSTOM) // Colorize default: true (for historical reasons) - command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) + command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_VALIDATE_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_VALIDATE_FLAG_ERR_LIMIT) } @@ -357,13 +303,13 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. getLogger().Infof("Outputting error results (`%s` format)...\n", format) switch format { case FORMAT_JSON: - formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors) + formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors, utils.GlobalFlags.ValidateFlags) case FORMAT_TEXT: - formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors) + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) default: getLogger().Warningf("error results not supported for `%s` format; defaulting to `%s` format...", format, FORMAT_TEXT) - formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors) + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) } // Append formatted schema errors "details" to the InvalidSBOMError type @@ -403,156 +349,3 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. // All validation tests passed; return VALID return } - -func formatSchemaErrorTypes(resultError gojsonschema.ResultError, colorize bool) (formattedResult string) { - - validationErrorResult := NewValidationErrResult(resultError) - - switch resultError.(type) { - // case *gojsonschema.AdditionalPropertyNotAllowedError: - // case *gojsonschema.ArrayContainsError: - // case *gojsonschema.ArrayMaxItemsError: - // case *gojsonschema.ArrayMaxPropertiesError: - // case *gojsonschema.ArrayMinItemsError: - // case *gojsonschema.ArrayMinPropertiesError: - // case *gojsonschema.ArrayNoAdditionalItemsError: - // case *gojsonschema.ConditionElseError: - // case *gojsonschema.ConditionThenError: - // case *gojsonschema.ConstError: - // case *gojsonschema.DoesNotMatchFormatError: - // case *gojsonschema.DoesNotMatchPatternError: - // case *gojsonschema.EnumError: - // case *gojsonschema.FalseError: - // case *gojsonschema.InternalError: - // case *gojsonschema.InvalidPropertyNameError: - // case *gojsonschema.InvalidPropertyPatternError: - // case *gojsonschema.InvalidTypeError: - case *gojsonschema.ItemsMustBeUniqueError: - formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(true, colorize) - // case *gojsonschema.MissingDependencyError: - // case *gojsonschema.MultipleOfError: - // case *gojsonschema.NumberAllOfError: - // case *gojsonschema.NumberAnyOfError: - // case *gojsonschema.NumberGTEError: - // case *gojsonschema.NumberGTError: - // case *gojsonschema.NumberLTEError: - // case *gojsonschema.NumberLTError: - // case *gojsonschema.NumberNotError: - // case *gojsonschema.NumberOneOfError: - // case *gojsonschema.RequiredError: - // case *gojsonschema.StringLengthGTEError: - // case *gojsonschema.StringLengthLTEError: - default: - formattedResult = validationErrorResult.Format(true, colorize) - } - - return -} - -func FormatSchemaErrorsJson(errs []gojsonschema.ResultError) string { - var sb strings.Builder - - lenErrs := len(errs) - if lenErrs > 0 { - sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):\n", lenErrs)) - errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors - colorize := utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors - - // If we have more errors than the (default or user set) limit; notify user - if lenErrs > errLimit { - // notify users more errors exist - msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) - getLogger().Infof("%s", msg) - } - - if lenErrs > 1 { - sb.WriteString("[\n") - } - - for i, resultError := range errs { - // short-circuit if too many errors (i.e., using the error limit flag value) - if i > errLimit { - break - } - - // add to the result errors - schemaErrorText := formatSchemaErrorTypes(resultError, colorize) - sb.WriteString(schemaErrorText) - - if i < (lenErrs-1) && i < (errLimit-1) { - sb.WriteString(",\n") - } - } - - if lenErrs > 1 { - sb.WriteString("\n]") - } - } - - return sb.String() -} - -func FormatSchemaErrorsText(errs []gojsonschema.ResultError) string { - var sb strings.Builder - - lenErrs := len(errs) - if lenErrs > 0 { - errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors - colorize := utils.GlobalFlags.ValidateFlags.ColorizeJsonErrors - var formattedValue string - var description string - var failingObject string - - sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):", lenErrs)) - for i, resultError := range errs { - - // short-circuit if we have too many errors - if i == errLimit { - // notify users more errors exist - msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", i, len(errs)) - getLogger().Infof("%s", msg) - // always include limit message in discrete output (i.e., not turned off by --quiet flag) - sb.WriteString("\n" + msg) - break - } - - // Some descriptions include very long enums; in those cases, - // truncate to a reasonable length using an intelligent separator - description = resultError.Description() - // truncate output unless debug flag is used - if !utils.GlobalFlags.Debug && - len(description) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { - description, _, _ = strings.Cut(description, ":") - description = description + " ... (truncated)" - } - - // TODO: provide flag to allow users to "turn on", by default we do NOT want this - // as this slows down processing on SBOMs with large numbers of errors - if colorize { - formattedValue, _ = log.FormatInterfaceAsColorizedJson(resultError.Value()) - } - // Indent error detail output in logs - formattedValue = log.AddTabs(formattedValue) - // NOTE: if we do not colorize or indent we could simply do this: - failingObject = fmt.Sprintf("\n\tFailing object: [%v]", formattedValue) - - // truncate output unless debug flag is used - if !utils.GlobalFlags.Debug && - len(failingObject) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { - failingObject = failingObject[:DEFAULT_MAX_ERR_DESCRIPTION_LEN] - failingObject = failingObject + " ... (truncated)" - } - - // append the numbered schema error - schemaErrorText := fmt.Sprintf("\n\t%d. Type: [%s], Field: [%s], Description: [%s] %s", - i+1, - resultError.Type(), - resultError.Field(), - description, - failingObject) - - sb.WriteString(schemaErrorText) - } - } - return sb.String() -} diff --git a/utils/flags.go b/utils/flags.go index 1a03b436..81dd8c5b 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -86,7 +86,9 @@ type ValidateCommandFlags struct { ForcedJsonSchemaFile string MaxNumErrors int MaxErrorDescriptionLength int - ColorizeJsonErrors bool + ColorizeErrorOutput bool + ShowErrorValue bool + ShowErrorDetail bool } type VulnerabilityCommandFlags struct { From 086f09db11a3d6dc724bec3759c05a227fe67ed6 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Tue, 20 Jun 2023 08:45:04 -0500 Subject: [PATCH 09/28] Separate format related functions into their own file Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 238 ++++++++++++++++++ ...alidate-err-components-unique-items-1.json | 228 +++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 cmd/validate_format.go create mode 100644 test/validation/cdx-1-4-validate-err-components-unique-items-1.json diff --git a/cmd/validate_format.go b/cmd/validate_format.go new file mode 100644 index 00000000..70cba5a6 --- /dev/null +++ b/cmd/validate_format.go @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +// "github.com/iancoleman/orderedmap" +import ( + "fmt" + "strings" + + "github.com/CycloneDX/sbom-utility/log" + "github.com/CycloneDX/sbom-utility/utils" + "github.com/iancoleman/orderedmap" + "github.com/xeipuuv/gojsonschema" +) + +type ValidationResultFormatter struct { + Results []ValidationResultFormat +} + +// JsonContext is a linked-list of JSON key strings +type ValidationResultFormat struct { + resultMap *orderedmap.OrderedMap + ResultError gojsonschema.ResultError + Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() + Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() + Field string `json:"field"` // details["field"] = err.Field() + Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() + DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() + Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() + Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() +} + +func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []byte, err error) { + return validationErrResult.resultMap.MarshalJSON() +} + +func (result *ValidationResultFormat) Format(showValue bool, flags utils.ValidateCommandFlags) string { + + var sb strings.Builder + + // Conditionally, add optional values as requested + if showValue { + result.resultMap.Set("value", result.ResultError.Value()) + } + + formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) + if err != nil { + return fmt.Sprintf("formatting error: %s", err.Error()) + } + sb.WriteString(formattedResult) + + return sb.String() +} + +func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue bool, flags utils.ValidateCommandFlags) string { + + var sb strings.Builder + + // Conditionally, add optional values as requested + if showValue { + result.resultMap.Set("value", result.ResultError.Value()) + } + + formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) + if err != nil { + return fmt.Sprintf("formatting error: %s", err.Error()) + } + sb.WriteString(formattedResult) + + return sb.String() +} + +func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.ValidateCommandFlags) (formattedResult string) { + + validationErrorResult := NewValidationErrResult(resultError) + + switch resultError.(type) { + // case *gojsonschema.AdditionalPropertyNotAllowedError: + // case *gojsonschema.ArrayContainsError: + // case *gojsonschema.ArrayMaxItemsError: + // case *gojsonschema.ArrayMaxPropertiesError: + // case *gojsonschema.ArrayMinItemsError: + // case *gojsonschema.ArrayMinPropertiesError: + // case *gojsonschema.ArrayNoAdditionalItemsError: + // case *gojsonschema.ConditionElseError: + // case *gojsonschema.ConditionThenError: + // case *gojsonschema.ConstError: + // case *gojsonschema.DoesNotMatchFormatError: + // case *gojsonschema.DoesNotMatchPatternError: + // case *gojsonschema.EnumError: + // case *gojsonschema.FalseError: + // case *gojsonschema.InternalError: + // case *gojsonschema.InvalidPropertyNameError: + // case *gojsonschema.InvalidPropertyPatternError: + // case *gojsonschema.InvalidTypeError: + case *gojsonschema.ItemsMustBeUniqueError: + formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(true, flags) + // case *gojsonschema.MissingDependencyError: + // case *gojsonschema.MultipleOfError: + // case *gojsonschema.NumberAllOfError: + // case *gojsonschema.NumberAnyOfError: + // case *gojsonschema.NumberGTEError: + // case *gojsonschema.NumberGTError: + // case *gojsonschema.NumberLTEError: + // case *gojsonschema.NumberLTError: + // case *gojsonschema.NumberNotError: + // case *gojsonschema.NumberOneOfError: + // case *gojsonschema.RequiredError: + // case *gojsonschema.StringLengthGTEError: + // case *gojsonschema.StringLengthLTEError: + default: + formattedResult = validationErrorResult.Format(true, flags) + } + + return +} + +func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) string { + var sb strings.Builder + + lenErrs := len(errs) + if lenErrs > 0 { + sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):\n", lenErrs)) + errLimit := flags.MaxNumErrors + + // If we have more errors than the (default or user set) limit; notify user + if lenErrs > errLimit { + // notify users more errors exist + msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) + getLogger().Infof("%s", msg) + } + + if lenErrs > 1 { + sb.WriteString("[\n") + } + + for i, resultError := range errs { + // short-circuit if too many errors (i.e., using the error limit flag value) + if i > errLimit { + break + } + + // add to the result errors + schemaErrorText := formatSchemaErrorTypes(resultError, flags) + sb.WriteString(schemaErrorText) + + if i < (lenErrs-1) && i < (errLimit-1) { + sb.WriteString(",\n") + } + } + + if lenErrs > 1 { + sb.WriteString("\n]") + } + } + + return sb.String() +} + +func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) string { + var sb strings.Builder + + lenErrs := len(errs) + if lenErrs > 0 { + errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors + colorize := utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput + var formattedValue string + var description string + var failingObject string + + sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):", lenErrs)) + for i, resultError := range errs { + + // short-circuit if we have too many errors + if i == errLimit { + // notify users more errors exist + msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", i, len(errs)) + getLogger().Infof("%s", msg) + // always include limit message in discrete output (i.e., not turned off by --quiet flag) + sb.WriteString("\n" + msg) + break + } + + // Some descriptions include very long enums; in those cases, + // truncate to a reasonable length using an intelligent separator + description = resultError.Description() + // truncate output unless debug flag is used + if !utils.GlobalFlags.Debug && + len(description) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { + description, _, _ = strings.Cut(description, ":") + description = description + " ... (truncated)" + } + + // TODO: provide flag to allow users to "turn on", by default we do NOT want this + // as this slows down processing on SBOMs with large numbers of errors + if colorize { + formattedValue, _ = log.FormatInterfaceAsColorizedJson(resultError.Value()) + } + // Indent error detail output in logs + formattedValue = log.AddTabs(formattedValue) + // NOTE: if we do not colorize or indent we could simply do this: + failingObject = fmt.Sprintf("\n\tFailing object: [%v]", formattedValue) + + // truncate output unless debug flag is used + if !utils.GlobalFlags.Debug && + len(failingObject) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { + failingObject = failingObject[:DEFAULT_MAX_ERR_DESCRIPTION_LEN] + failingObject = failingObject + " ... (truncated)" + } + + // append the numbered schema error + schemaErrorText := fmt.Sprintf("\n\t%d. Type: [%s], Field: [%s], Description: [%s] %s", + i+1, + resultError.Type(), + resultError.Field(), + description, + failingObject) + + sb.WriteString(schemaErrorText) + } + } + return sb.String() +} diff --git a/test/validation/cdx-1-4-validate-err-components-unique-items-1.json b/test/validation/cdx-1-4-validate-err-components-unique-items-1.json new file mode 100644 index 00000000..1453e624 --- /dev/null +++ b/test/validation/cdx-1-4-validate-err-components-unique-items-1.json @@ -0,0 +1,228 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "serialNumber": "urn:uuid:1a2b3c4d-1234-abcd-9876-a3b4c5d6e7f9", + "externalReferences": [ + { + "url": "support@example.com", + "comment": "Support for questions about SBOM contents", + "type": "support" + } + ], + "metadata": { + "timestamp": "2022-10-12T19:07:00Z", + "properties": [ + { + "name": "urn:example.com:classification", + "value": "This SBOM is Confidential Information. Do not distribute." + }, + { + "name": "urn:example.com:disclaimer", + "value": "This SBOM is current as of the date it was generated and is subject to change." + } + ], + "manufacture": { + "name": "Example Co.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "email": "contact@example.com" + } + ] + }, + "supplier": { + "name": "Example Co. Distribution Dept.", + "url": [ + "https://example.com/software/" + ], + "contact": [ + { + "email": "distribution@example.com" + } + ] + }, + "component": { + "type": "application", + "bom-ref": "pkg:oci/example.com/product/application@10.0.4.0", + "purl": "pkg:oci/example.com/product/application@10.0.4.0", + "name": "Example Application v10.0.4", + "description": "Example's Do-It-All application", + "version": "10.0.4.0", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://example.com/application" + } + ], + "properties": [ + { + "name": "urn:example.com:identifier:product", + "value": "71C22290D7DB11EBAA175CFD3E629A2A" + }, + { + "name": "urn:example.com:identifier:distribution", + "value": "5737-I23" + } + ], + "hashes": [ + { + "alg": "SHA-1", + "content": "1111aaaa2222cccc3333dddd4444eeee5555ffff" + } + ], + "supplier": { + "name": "Example Co. Distribution Dept.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "email": "distribution@example.com" + } + ] + }, + "publisher": "Example Inc. EMEA" + }, + "licenses": [ + { + "license": { + "id": "Apache-1.0" + } + }, + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "MIT" + } + } + ], + "tools": [ + { + "vendor": "SecurityTools.com", + "name": "Security Scanner v1.0", + "version": "1.0.0-beta.1+0099", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + }, + { + "vendor": "SBOM.com", + "name": "SBOM Generator v2.1", + "version": "2.1.12", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:npm/sample@2.0.0", + "purl": "pkg:npm/sample@2.0.0", + "name": "sample", + "version": "2.0.0", + "description": "Node.js Sampler package", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "bom-ref": "pkg:npm/body-parser@1.19.0", + "purl": "pkg:npm/body-parser@1.19.0", + "name": "body-parser", + "version": "1.19.0", + "description": "Node.js body parsing middleware", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + }, + { + "type": "library", + "bom-ref": "pkg:npm/body-parser@1.19.0", + "purl": "pkg:npm/body-parser@1.19.0", + "name": "body-parser", + "version": "1.19.0", + "description": "Node.js body parsing middleware", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + }, + { + "type": "library", + "name": "body-parser", + "version": "1.20.0" + }, + { + "type": "library", + "bom-ref": "pkg:npm/body-parser@1.19.0", + "purl": "pkg:npm/body-parser@1.19.0", + "name": "body-parser", + "version": "1.19.0", + "description": "Node.js body parsing middleware", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + } + ] +} \ No newline at end of file From 55810a68ff69dc933b884b0f8e4c572ee229c838 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 04:27:28 -0500 Subject: [PATCH 10/28] Format value for unique item error Signed-off-by: Matt Rutkowski --- .vscode/settings.json | 6 +++++- cmd/diff.go | 5 ++--- cmd/errors.go | 3 ++- cmd/validate.go | 18 ++-------------- cmd/validate_format.go | 48 +++++++++++++++++++++++++++++++++--------- cmd/validate_test.go | 2 +- go.mod | 1 + go.sum | 2 ++ 8 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 02f66a3f..72f77f2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,7 @@ "multimap", "myservices", "NOASSERTION", + "nolint", "nosec", "NTIA", "Nyffenegger", @@ -102,5 +103,8 @@ ], "files.watcherExclude": { "**/target": true - } + }, + "cSpell.ignoreWords": [ + "iancoleman" + ] } \ No newline at end of file diff --git a/cmd/diff.go b/cmd/diff.go index 85ea8d94..64b29552 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -20,7 +20,6 @@ package cmd import ( "encoding/json" "fmt" - "io/ioutil" "os" "strings" @@ -144,7 +143,7 @@ func Diff(flags utils.CommandFlags) (err error) { getLogger().Infof("Reading file (--input-file): `%s` ...", baseFilename) // #nosec G304 (suppress warning) - bBaseData, errReadBase := ioutil.ReadFile(baseFilename) + bBaseData, errReadBase := os.ReadFile(baseFilename) if errReadBase != nil { getLogger().Debugf("%v", bBaseData[:255]) err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error()) @@ -153,7 +152,7 @@ func Diff(flags utils.CommandFlags) (err error) { getLogger().Infof("Reading file (--input-revision): `%s` ...", deltaFilename) // #nosec G304 (suppress warning) - bRevisedData, errReadDelta := ioutil.ReadFile(deltaFilename) + bRevisedData, errReadDelta := os.ReadFile(deltaFilename) if errReadDelta != nil { getLogger().Debugf("%v", bRevisedData[:255]) err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error()) diff --git a/cmd/errors.go b/cmd/errors.go index def0bb60..1c07c28b 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -116,10 +116,11 @@ func (err BaseError) Error() string { return formattedMessage } +//nolint:all func (base BaseError) AppendMessage(addendum string) { // Ignore (invalid) static linting message: // "ineffective assignment to field (SA4005)" - base.Message += addendum + base.Message += addendum //nolint:staticcheck } type UtilityError struct { diff --git a/cmd/validate.go b/cmd/validate.go index 9815169e..e9abcf7c 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -297,23 +297,9 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. nil, schemaErrors) - // Format error results + // Format error results and append to InvalidSBOMError error "details" format := utils.GlobalFlags.OutputFormat - var formattedSchemaErrors string - getLogger().Infof("Outputting error results (`%s` format)...\n", format) - switch format { - case FORMAT_JSON: - formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors, utils.GlobalFlags.ValidateFlags) - case FORMAT_TEXT: - formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) - default: - getLogger().Warningf("error results not supported for `%s` format; defaulting to `%s` format...", - format, FORMAT_TEXT) - formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) - } - - // Append formatted schema errors "details" to the InvalidSBOMError type - errInvalid.Details = formattedSchemaErrors + errInvalid.Details = FormatSchemaErrors(schemaErrors, utils.GlobalFlags.ValidateFlags, format) return INVALID, document, schemaErrors, errInvalid } diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 70cba5a6..d3f7153e 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -34,15 +34,15 @@ type ValidationResultFormatter struct { // JsonContext is a linked-list of JSON key strings type ValidationResultFormat struct { - resultMap *orderedmap.OrderedMap - ResultError gojsonschema.ResultError - Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() - Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() - Field string `json:"field"` // details["field"] = err.Field() - Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() - DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() - Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() - Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() + resultMap *orderedmap.OrderedMap + ResultError gojsonschema.ResultError + Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() + //Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() + //Field string `json:"field"` // details["field"] = err.Field() + //Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() + //DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() + //Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() + //Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() } func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []byte, err error) { @@ -73,7 +73,19 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo // Conditionally, add optional values as requested if showValue { - result.resultMap.Set("value", result.ResultError.Value()) + details := result.ResultError.Details() + valueType, typeFound := details["type"] + if typeFound && valueType == "array" { + index, indexFound := details["i"] + if indexFound { + value := result.ResultError.Value() + array, arrayValid := value.([]interface{}) + i, indexValid := index.(int) + if arrayValid && indexValid && i < len(array) { + result.resultMap.Set("value", array[i]) + } + } + } } formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) @@ -85,6 +97,22 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo return sb.String() } +func FormatSchemaErrors(schemaErrors []gojsonschema.ResultError, flags utils.ValidateCommandFlags, format string) (formattedSchemaErrors string) { + + getLogger().Infof("Formatting error results (`%s` format)...\n", format) + switch format { + case FORMAT_JSON: + formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors, utils.GlobalFlags.ValidateFlags) + case FORMAT_TEXT: + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) + default: + getLogger().Warningf("error results not supported for `%s` format; defaulting to `%s` format...", + format, FORMAT_TEXT) + formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) + } + return +} + func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.ValidateCommandFlags) (formattedResult string) { validationErrorResult := NewValidationErrResult(resultError) diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 55f19489..3bdfd882 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -279,5 +279,5 @@ func TestValidateCdx14ComponentsUniqueJsonResults(t *testing.T) { TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, SCHEMA_VARIANT_NONE, FORMAT_JSON, - nil) + &InvalidSBOMError{}) } diff --git a/go.mod b/go.mod index e4602ec6..a9b40ebc 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index d558c399..900f26f8 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mrutkows/go-jsondiff v0.2.0 h1:T+05e1QSe7qB6vhkVtv3NImD3ni+Jdxpj69iMsptAqY= github.com/mrutkows/go-jsondiff v0.2.0/go.mod h1:TuasE0Ldrf4r1Gp0uIatS9SnPZPYybjmTGjB7WXKWl4= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= From 54449f2762d2be03d489e33826e0df985a1bed32 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 09:17:40 -0500 Subject: [PATCH 11/28] Consolidate validation flags and use on top-level API call Signed-off-by: Matt Rutkowski --- cmd/validate.go | 8 ++++---- cmd/validate_custom_test.go | 8 ++++---- cmd/validate_format.go | 7 +++++++ cmd/validate_test.go | 2 +- schema/schema_formats.go | 6 +++--- utils/flags.go | 8 +++++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index e9abcf7c..e71c750b 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -114,8 +114,8 @@ func initCommandValidate(command *cobra.Command) { // Force a schema file to use for validation (override inferred schema) command.Flags().StringVarP(&utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile, FLAG_VALIDATE_SCHEMA_FORCE, "", "", MSG_VALIDATE_SCHEMA_FORCE) // Optional schema "variant" of inferred schema (e.g, "strict") - command.Flags().StringVarP(&utils.GlobalFlags.Variant, FLAG_VALIDATE_SCHEMA_VARIANT, "", "", MSG_VALIDATE_SCHEMA_VARIANT) - command.Flags().BoolVarP(&utils.GlobalFlags.CustomValidation, FLAG_VALIDATE_CUSTOM, "", false, MSG_VALIDATE_FLAG_CUSTOM) + command.Flags().StringVarP(&utils.GlobalFlags.ValidateFlags.SchemaVariant, FLAG_VALIDATE_SCHEMA_VARIANT, "", "", MSG_VALIDATE_SCHEMA_VARIANT) + command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.CustomValidation, FLAG_VALIDATE_CUSTOM, "", false, MSG_VALIDATE_FLAG_CUSTOM) // Colorize default: true (for historical reasons) command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_VALIDATE_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_VALIDATE_FLAG_ERR_LIMIT) @@ -199,7 +199,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. } // if "custom" flag exists, then assure we support the format - if utils.GlobalFlags.CustomValidation && !document.FormatInfo.IsCycloneDx() { + if utils.GlobalFlags.ValidateFlags.CustomValidation && !document.FormatInfo.IsCycloneDx() { err = schema.NewUnsupportedFormatError( schema.MSG_FORMAT_UNSUPPORTED_COMMAND, document.GetFilename(), @@ -315,7 +315,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. // Perform additional validation in document composition/structure // and "custom" required data within specified fields - if utils.GlobalFlags.CustomValidation { + if utils.GlobalFlags.ValidateFlags.CustomValidation { // Perform all custom validation err := validateCustomCDXDocument(document) if err != nil { diff --git a/cmd/validate_custom_test.go b/cmd/validate_custom_test.go index 0673ddab..69835745 100644 --- a/cmd/validate_custom_test.go +++ b/cmd/validate_custom_test.go @@ -57,16 +57,16 @@ const ( // ------------------------------------------- func innerCustomValidateError(t *testing.T, filename string, variant string, innerError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { - utils.GlobalFlags.CustomValidation = true + utils.GlobalFlags.ValidateFlags.CustomValidation = true document, schemaErrors, actualError = innerValidateError(t, filename, variant, FORMAT_TEXT, innerError) - utils.GlobalFlags.CustomValidation = false + utils.GlobalFlags.ValidateFlags.CustomValidation = false return } func innerCustomValidateInvalidSBOMInnerError(t *testing.T, filename string, variant string, innerError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { - utils.GlobalFlags.CustomValidation = true + utils.GlobalFlags.ValidateFlags.CustomValidation = true document, schemaErrors, actualError = innerValidateInvalidSBOMInnerError(t, filename, variant, innerError) - utils.GlobalFlags.CustomValidation = false + utils.GlobalFlags.ValidateFlags.CustomValidation = false return } diff --git a/cmd/validate_format.go b/cmd/validate_format.go index d3f7153e..b7e1fc27 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -72,15 +72,22 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo var sb strings.Builder // Conditionally, add optional values as requested + // For this error type, we want to reduce the information show to the end user. + // Originally, the entire array with duplicate items was show for EVERY occurrence; + // attempt to only show the failing item itself once (and only once) + // TODO: deduplication (planned) will also help shrink large error output if showValue { details := result.ResultError.Details() valueType, typeFound := details["type"] + // verify the claimed type is an array if typeFound && valueType == "array" { index, indexFound := details["i"] + // if a claimed duplicate index is provided (we use the first "i" index not the 2nd "j" one) if indexFound { value := result.ResultError.Value() array, arrayValid := value.([]interface{}) i, indexValid := index.(int) + // verify the claimed item index is within range if arrayValid && indexValid && i < len(array) { result.resultMap.Set("value", array[i]) } diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 3bdfd882..4822d30d 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -56,7 +56,7 @@ func innerValidateError(t *testing.T, filename string, variant string, format st // Copy the test filename to the command line flags where the code looks for it utils.GlobalFlags.InputFile = filename // Set the schema variant where the command line flag would - utils.GlobalFlags.Variant = variant + utils.GlobalFlags.ValidateFlags.SchemaVariant = variant // Set the err result format utils.GlobalFlags.OutputFormat = format diff --git a/schema/schema_formats.go b/schema/schema_formats.go index bf25ecb4..c3175c06 100644 --- a/schema/schema_formats.go +++ b/schema/schema_formats.go @@ -416,7 +416,7 @@ func (sbom *Sbom) FindFormatAndSchema() (err error) { // Copy format info into Sbom context sbom.FormatInfo = format - err = sbom.findSchemaVersionWithVariant(format, version, utils.GlobalFlags.Variant) + err = sbom.findSchemaVersionWithVariant(format, version, utils.GlobalFlags.ValidateFlags.SchemaVariant) return } } @@ -444,9 +444,9 @@ func (sbom *Sbom) findSchemaVersionWithVariant(format FormatSchema, version stri // If a variant is also requested, see if we can find one for that criteria // Note: the default value for "variant" is an empty string - if utils.GlobalFlags.Variant == schema.Variant { + if utils.GlobalFlags.ValidateFlags.SchemaVariant == schema.Variant { getLogger().Tracef("Match found for requested schema variant: `%s`", - FormatSchemaVariant(utils.GlobalFlags.Variant)) + FormatSchemaVariant(utils.GlobalFlags.ValidateFlags.SchemaVariant)) sbom.SchemaInfo = schema return } diff --git a/utils/flags.go b/utils/flags.go index 81dd8c5b..0f60c66f 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -57,10 +57,8 @@ type CommandFlags struct { VulnerabilityFlags VulnerabilityCommandFlags // Validate (local) flags - Variant string ValidateProperties bool ValidateFlags ValidateCommandFlags - CustomValidation bool CustomValidationOptions CustomValidationFlags // Summary formats (i.e., only valid for summary) @@ -83,7 +81,11 @@ type LicenseCommandFlags struct { } type ValidateCommandFlags struct { - ForcedJsonSchemaFile string + SchemaVariant string + ForcedJsonSchemaFile string + // Uses custom validation flags if "true"; defaults to config. "custom.json" + CustomValidation bool + // error result processing MaxNumErrors int MaxErrorDescriptionLength int ColorizeErrorOutput bool From e3bf5c43968846a30c44d591d6267b6ca7216077 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 09:42:08 -0500 Subject: [PATCH 12/28] Adjust JSON error result output prefix and indent Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 31 +++++++++++++++++++++++++------ log/format.go | 8 ++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cmd/validate_format.go b/cmd/validate_format.go index b7e1fc27..48e3065c 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -28,6 +28,18 @@ import ( "github.com/xeipuuv/gojsonschema" ) +const ( + ERROR_DETAIL_KEY_DATA_TYPE = "type" + ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY = "array" + ERROR_DETAIL_ARRAY_ITEM_INDEX_I = "i" + ERROR_DETAIL_ARRAY_ITEM_INDEX_J = "j" +) + +const ( + ERROR_DETAIL_JSON_DEFAULT_PREFIX = "...." + ERROR_DETAIL_JSON_DEFAULT_INDENT = " " +) + type ValidationResultFormatter struct { Results []ValidationResultFormat } @@ -58,7 +70,8 @@ func (result *ValidationResultFormat) Format(showValue bool, flags utils.Validat result.resultMap.Set("value", result.ResultError.Value()) } - formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) + // TODO: add a general JSON formatting flag + formattedResult, err := log.FormatIndentedInterfaceAsJson(result.resultMap, ERROR_DETAIL_JSON_DEFAULT_PREFIX, ERROR_DETAIL_JSON_DEFAULT_INDENT) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) } @@ -78,10 +91,10 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo // TODO: deduplication (planned) will also help shrink large error output if showValue { details := result.ResultError.Details() - valueType, typeFound := details["type"] + valueType, typeFound := details[ERROR_DETAIL_KEY_DATA_TYPE] // verify the claimed type is an array - if typeFound && valueType == "array" { - index, indexFound := details["i"] + if typeFound && valueType == ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY { + index, indexFound := details[ERROR_DETAIL_ARRAY_ITEM_INDEX_I] // if a claimed duplicate index is provided (we use the first "i" index not the 2nd "j" one) if indexFound { value := result.ResultError.Value() @@ -89,13 +102,16 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo i, indexValid := index.(int) // verify the claimed item index is within range if arrayValid && indexValid && i < len(array) { - result.resultMap.Set("value", array[i]) + result.resultMap.Set( + fmt.Sprintf("item[%v]", i), + array[i]) } } } } - formattedResult, err := log.FormatInterfaceAsJson(result.resultMap) + // TODO: add a general JSON formatting flag + formattedResult, err := log.FormatIndentedInterfaceAsJson(result.resultMap, ERROR_DETAIL_JSON_DEFAULT_PREFIX, ERROR_DETAIL_JSON_DEFAULT_INDENT) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) } @@ -192,6 +208,9 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat // add to the result errors schemaErrorText := formatSchemaErrorTypes(resultError, flags) + // NOTE: we must add the prefix (indent) ourselves + // see issue: https://github.com/golang/go/issues/49261 + sb.WriteString(ERROR_DETAIL_JSON_DEFAULT_PREFIX) sb.WriteString(schemaErrorText) if i < (lenErrs-1) && i < (errLimit-1) { diff --git a/log/format.go b/log/format.go index d632fcde..834fa22d 100644 --- a/log/format.go +++ b/log/format.go @@ -162,6 +162,14 @@ func FormatInterfaceAsColorizedJson(data interface{}) (string, error) { } // TODO: make indent length configurable +func FormatIndentedInterfaceAsJson(data interface{}, prefix string, indent string) (string, error) { + bytes, err := json.MarshalIndent(data, prefix, indent) + if err != nil { + return "", err + } + return string(bytes), nil +} + func FormatInterfaceAsJson(data interface{}) (string, error) { bytes, err := json.MarshalIndent(data, "", " ") if err != nil { From 7d23529f7e142d08cd341d549162c88a9fdb3b16 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 10:23:34 -0500 Subject: [PATCH 13/28] Add validation test case for bad iri-format Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 8 +- cmd/validate_test.go | 14 +- ...e-err-components-format-iri-reference.json | 159 ++++++++++++++++++ ...alidate-err-components-unique-items-1.json | 40 ----- 4 files changed, 176 insertions(+), 45 deletions(-) create mode 100644 test/validation/cdx-1-4-validate-err-components-format-iri-reference.json diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 48e3065c..0e17c4dc 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -29,6 +29,7 @@ import ( ) const ( + ERROR_DETAIL_KEY_VALUE = "value" ERROR_DETAIL_KEY_DATA_TYPE = "type" ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY = "array" ERROR_DETAIL_ARRAY_ITEM_INDEX_I = "i" @@ -36,7 +37,7 @@ const ( ) const ( - ERROR_DETAIL_JSON_DEFAULT_PREFIX = "...." + ERROR_DETAIL_JSON_DEFAULT_PREFIX = " " ERROR_DETAIL_JSON_DEFAULT_INDENT = " " ) @@ -67,7 +68,7 @@ func (result *ValidationResultFormat) Format(showValue bool, flags utils.Validat // Conditionally, add optional values as requested if showValue { - result.resultMap.Set("value", result.ResultError.Value()) + result.resultMap.Set(ERROR_DETAIL_KEY_VALUE, result.ResultError.Value()) } // TODO: add a general JSON formatting flag @@ -140,7 +141,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va validationErrorResult := NewValidationErrResult(resultError) - switch resultError.(type) { + switch errorType := resultError.(type) { // case *gojsonschema.AdditionalPropertyNotAllowedError: // case *gojsonschema.ArrayContainsError: // case *gojsonschema.ArrayMaxItemsError: @@ -175,6 +176,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va // case *gojsonschema.StringLengthGTEError: // case *gojsonschema.StringLengthLTEError: default: + getLogger().Debugf("default formatting: ResultError Type: [%v]", errorType) formattedResult = validationErrorResult.Format(true, flags) } diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 4822d30d..cbb997f6 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -45,7 +45,8 @@ const ( ) const ( - TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE = "test/validation/cdx-1-4-validate-err-components-unique-items-1.json" + TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE = "test/validation/cdx-1-4-validate-err-components-unique-items-1.json" + TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE = "test/validation/cdx-1-4-validate-err-components-format-iri-reference.json" ) // Tests basic validation and expected errors @@ -273,7 +274,7 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { // nil) // } -func TestValidateCdx14ComponentsUniqueJsonResults(t *testing.T) { +func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, @@ -281,3 +282,12 @@ func TestValidateCdx14ComponentsUniqueJsonResults(t *testing.T) { FORMAT_JSON, &InvalidSBOMError{}) } + +func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { + //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM + innerValidateError(t, + TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, + SCHEMA_VARIANT_NONE, + FORMAT_JSON, + &InvalidSBOMError{}) +} diff --git a/test/validation/cdx-1-4-validate-err-components-format-iri-reference.json b/test/validation/cdx-1-4-validate-err-components-format-iri-reference.json new file mode 100644 index 00000000..d832d92d --- /dev/null +++ b/test/validation/cdx-1-4-validate-err-components-format-iri-reference.json @@ -0,0 +1,159 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "serialNumber": "urn:uuid:1a2b3c4d-1234-abcd-9876-a3b4c5d6e7f9", + "metadata": { + "component": { + "type": "application", + "bom-ref": "pkg:oci/example.com/product/application@10.0.4.0", + "purl": "pkg:oci/example.com/product/application@10.0.4.0", + "name": "Example Application v10.0.4", + "description": "Example's Do-It-All application", + "version": "10.0.4.0", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://example.com/application" + } + ], + "properties": [ + { + "name": "urn:example.com:identifier:product", + "value": "71C22290D7DB11EBAA175CFD3E629A2A" + }, + { + "name": "urn:example.com:identifier:distribution", + "value": "5737-I23" + } + ], + "hashes": [ + { + "alg": "SHA-1", + "content": "1111aaaa2222cccc3333dddd4444eeee5555ffff" + } + ], + "supplier": { + "name": "Example Co. Distribution Dept.", + "url": [ + "https://example.com" + ], + "contact": [ + { + "email": "distribution@example.com" + } + ] + }, + "publisher": "Example Inc. EMEA" + }, + "licenses": [ + { + "license": { + "id": "Apache-1.0" + } + }, + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "MIT" + } + } + ], + "tools": [ + { + "vendor": "SecurityTools.com", + "name": "Security Scanner v1.0", + "version": "1.0.0-beta.1+0099", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + }, + { + "vendor": "SBOM.com", + "name": "SBOM Generator v2.1", + "version": "2.1.12", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ] + } + ] + }, + "components": [ + { + "type": "operating-system", + "name": "debian", + "version": "10", + "description": "Debian GNU/Linux 10 (buster)", + "externalReferences": [ + { + "url": "https://www.debian.org/", + "type": "website" + }, + { + "url": "https://www.debian.org/support", + "type": "other", + "comment": "support" + } + ] + }, + { + "type": "library", + "bom-ref": "pkg:empty", + "name": "Empty", + "externalReferences": [ + { + "url": "", + "type": "build-meta" + } + ] + }, + { + "type": "library", + "bom-ref": "pkg:npm/asn1.js@5.4.1?package-id=e24b6ffc41aa39e5", + "author": "Fedor Indutny", + "name": "asn1.js", + "version": "5.4.1", + "description": "ASN.1 encoder and decoder", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/asn1.js@5.4.1", + "externalReferences": [ + { + "url": "git@github.com:indutny/asn1.js", + "type": "distribution" + }, + { + "url": "https://github.com/indutny/asn1.js", + "type": "website" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/validation/cdx-1-4-validate-err-components-unique-items-1.json b/test/validation/cdx-1-4-validate-err-components-unique-items-1.json index 1453e624..5c360221 100644 --- a/test/validation/cdx-1-4-validate-err-components-unique-items-1.json +++ b/test/validation/cdx-1-4-validate-err-components-unique-items-1.json @@ -3,47 +3,7 @@ "specVersion": "1.4", "version": 1, "serialNumber": "urn:uuid:1a2b3c4d-1234-abcd-9876-a3b4c5d6e7f9", - "externalReferences": [ - { - "url": "support@example.com", - "comment": "Support for questions about SBOM contents", - "type": "support" - } - ], "metadata": { - "timestamp": "2022-10-12T19:07:00Z", - "properties": [ - { - "name": "urn:example.com:classification", - "value": "This SBOM is Confidential Information. Do not distribute." - }, - { - "name": "urn:example.com:disclaimer", - "value": "This SBOM is current as of the date it was generated and is subject to change." - } - ], - "manufacture": { - "name": "Example Co.", - "url": [ - "https://example.com" - ], - "contact": [ - { - "email": "contact@example.com" - } - ] - }, - "supplier": { - "name": "Example Co. Distribution Dept.", - "url": [ - "https://example.com/software/" - ], - "contact": [ - { - "email": "distribution@example.com" - } - ] - }, "component": { "type": "application", "bom-ref": "pkg:oci/example.com/product/application@10.0.4.0", From 38511c1950d5a51279ede923b08361522fad8255 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 10:25:36 -0500 Subject: [PATCH 14/28] Add validation test case for bad iri-format Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 0e17c4dc..08b3a333 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -215,7 +215,7 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat sb.WriteString(ERROR_DETAIL_JSON_DEFAULT_PREFIX) sb.WriteString(schemaErrorText) - if i < (lenErrs-1) && i < (errLimit-1) { + if i < (lenErrs-1) && i < (errLimit) { sb.WriteString(",\n") } } From 292a82d4875eacd06fc838f97d51889f82194fa2 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 12:16:45 -0500 Subject: [PATCH 15/28] Consolidate persistent command flags into a struct Signed-off-by: Matt Rutkowski --- cmd/diff.go | 40 +++++++++++++++++++------------------- cmd/diff_test.go | 6 +++--- cmd/document.go | 16 ++++++++------- cmd/license_list.go | 11 ++++++----- cmd/license_policy.go | 10 +++++----- cmd/license_policy_test.go | 8 ++++---- cmd/license_test.go | 6 +++--- cmd/query.go | 17 +++++++++++----- cmd/query_test.go | 2 +- cmd/resource.go | 11 ++++++----- cmd/resource_test.go | 2 +- cmd/root.go | 18 ++++++++--------- cmd/root_test.go | 6 +++--- cmd/schema.go | 13 +++++++------ cmd/validate.go | 9 +++++---- cmd/validate_format.go | 10 ++-------- cmd/validate_test.go | 6 +++--- cmd/vulnerability.go | 9 +++++---- cmd/vulnerability_test.go | 2 +- schema/schema_formats.go | 4 ++-- utils/flags.go | 24 ++++++++++++----------- 21 files changed, 120 insertions(+), 110 deletions(-) diff --git a/cmd/diff.go b/cmd/diff.go index 64b29552..e99d885d 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -50,7 +50,7 @@ func NewCommandDiff() *cobra.Command { command.Use = CMD_USAGE_DIFF command.Short = "Report on differences between two BOM files using RFC 6902 format" command.Long = "Report on differences between two BOM files using RFC 6902 format" - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_DIFF_OUTPUT_FORMAT_HELP+DIFF_OUTPUT_SUPPORTED_FORMATS) command.Flags().StringVarP(&utils.GlobalFlags.DiffFlags.RevisedFile, FLAG_DIFF_FILENAME_REVISION, @@ -74,7 +74,7 @@ func preRunTestForFiles(cmd *cobra.Command, args []string) error { getLogger().Tracef("args: %v", args) // Make sure the base (input) file is present and exists - baseFilename := utils.GlobalFlags.InputFile + baseFilename := utils.GlobalFlags.PersistentFlags.InputFile if baseFilename == "" { return getLogger().Errorf("Missing required argument(s): %s", FLAG_FILENAME_INPUT) } else if _, err := os.Stat(baseFilename); err != nil { @@ -97,7 +97,8 @@ func diffCmdImpl(cmd *cobra.Command, args []string) (err error) { defer getLogger().Exit() // Create output writer - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer) // use function closure to assure consistent error output based upon error type @@ -108,7 +109,7 @@ func diffCmdImpl(cmd *cobra.Command, args []string) (err error) { if err != nil { return } - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.PersistentFlags.OutputFile) } }() @@ -122,11 +123,11 @@ func Diff(flags utils.CommandFlags) (err error) { defer getLogger().Exit() // create locals - format := utils.GlobalFlags.OutputFormat - baseFilename := utils.GlobalFlags.InputFile - outputFilename := utils.GlobalFlags.OutputFile - outputFormat := utils.GlobalFlags.OutputFormat - deltaFilename := utils.GlobalFlags.DiffFlags.RevisedFile + format := utils.GlobalFlags.PersistentFlags.OutputFormat + inputFilename := utils.GlobalFlags.PersistentFlags.InputFile + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFormat := utils.GlobalFlags.PersistentFlags.OutputFormat + revisedFilename := utils.GlobalFlags.DiffFlags.RevisedFile deltaColorize := utils.GlobalFlags.DiffFlags.Colorize // Create output writer @@ -137,31 +138,31 @@ func Diff(flags utils.CommandFlags) (err error) { // always close the output file if outputFile != nil { err = outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", outputFilename) } }() - getLogger().Infof("Reading file (--input-file): `%s` ...", baseFilename) + getLogger().Infof("Reading file (--input-file): `%s` ...", inputFilename) // #nosec G304 (suppress warning) - bBaseData, errReadBase := os.ReadFile(baseFilename) + bBaseData, errReadBase := os.ReadFile(inputFilename) if errReadBase != nil { getLogger().Debugf("%v", bBaseData[:255]) - err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error()) + err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", inputFilename, err.Error()) return } - getLogger().Infof("Reading file (--input-revision): `%s` ...", deltaFilename) + getLogger().Infof("Reading file (--input-revision): `%s` ...", revisedFilename) // #nosec G304 (suppress warning) - bRevisedData, errReadDelta := os.ReadFile(deltaFilename) + bRevisedData, errReadDelta := os.ReadFile(revisedFilename) if errReadDelta != nil { getLogger().Debugf("%v", bRevisedData[:255]) - err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error()) + err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", inputFilename, err.Error()) return } // Compare the base with the revision differ := diff.New() - getLogger().Infof("Comparing files: `%s` (base) to `%s` (revised) ...", baseFilename, deltaFilename) + getLogger().Infof("Comparing files: `%s` (base) to `%s` (revised) ...", inputFilename, revisedFilename) d, err := differ.Compare(bBaseData, bRevisedData) if err != nil { err = getLogger().Errorf("Failed to Compare data: %s\n", err.Error()) @@ -177,7 +178,7 @@ func Diff(flags utils.CommandFlags) (err error) { err = json.Unmarshal(bBaseData, &aJson) if err != nil { - err = getLogger().Errorf("json.Unmarshal() failed '%s': %s\n", utils.GlobalFlags.InputFile, err.Error()) + err = getLogger().Errorf("json.Unmarshal() failed '%s': %s\n", inputFilename, err.Error()) return } @@ -200,8 +201,7 @@ func Diff(flags utils.CommandFlags) (err error) { } else { getLogger().Infof("No deltas found. baseFilename: `%s`, revisedFilename=`%s` match.", - utils.GlobalFlags.InputFile, - utils.GlobalFlags.DiffFlags.RevisedFile) + inputFilename, revisedFilename) } return diff --git a/cmd/diff_test.go b/cmd/diff_test.go index 0c6e4965..31307ac8 100644 --- a/cmd/diff_test.go +++ b/cmd/diff_test.go @@ -48,15 +48,15 @@ func innerDiffError(t *testing.T, baseFilename string, revisedFilename string, f defer getLogger().Exit() // Copy the test filename to the command line flags where the code looks for it - utils.GlobalFlags.OutputFormat = format - utils.GlobalFlags.InputFile = baseFilename + utils.GlobalFlags.PersistentFlags.OutputFormat = format + utils.GlobalFlags.PersistentFlags.InputFile = baseFilename utils.GlobalFlags.DiffFlags.RevisedFile = revisedFilename utils.GlobalFlags.DiffFlags.Colorize = true actualError = Diff(utils.GlobalFlags) getLogger().Tracef("baseFilename: `%s`, revisedFilename=`%s`, actualError=`%T`", - utils.GlobalFlags.InputFile, + utils.GlobalFlags.PersistentFlags.InputFile, utils.GlobalFlags.DiffFlags.RevisedFile, actualError) diff --git a/cmd/document.go b/cmd/document.go index 9e04520d..79822670 100644 --- a/cmd/document.go +++ b/cmd/document.go @@ -28,26 +28,28 @@ func LoadInputSbomFileAndDetectSchema() (document *schema.Sbom, err error) { getLogger().Enter() defer getLogger().Exit() + inputFile := utils.GlobalFlags.PersistentFlags.InputFile + // check for required fields on command - getLogger().Tracef("utils.Flags.InputFile: `%s`", utils.GlobalFlags.InputFile) - if utils.GlobalFlags.InputFile == "" { - return nil, fmt.Errorf("invalid input file (-%s): `%s` ", FLAG_FILENAME_INPUT_SHORT, utils.GlobalFlags.InputFile) + getLogger().Tracef("utils.Flags.InputFile: `%s`", inputFile) + if inputFile == "" { + return nil, fmt.Errorf("invalid input file (-%s): `%s` ", FLAG_FILENAME_INPUT_SHORT, inputFile) } // Construct an Sbom object around the input file - document = schema.NewSbom(utils.GlobalFlags.InputFile) + document = schema.NewSbom(inputFile) // Load the raw, candidate SBOM (file) as JSON data - getLogger().Infof("Attempting to load and unmarshal file `%s`...", utils.GlobalFlags.InputFile) + getLogger().Infof("Attempting to load and unmarshal file `%s`...", inputFile) err = document.UnmarshalSBOMAsJsonMap() // i.e., utils.Flags.InputFile if err != nil { return } - getLogger().Infof("Successfully unmarshalled data from: `%s`", utils.GlobalFlags.InputFile) + getLogger().Infof("Successfully unmarshalled data from: `%s`", inputFile) // Search the document keys/values for known SBOM formats and schema in the config. file getLogger().Infof("Determining file's SBOM format and version...") - err = document.FindFormatAndSchema() + err = document.FindFormatAndSchema(utils.GlobalFlags.PersistentFlags.InputFile) if err != nil { return } diff --git a/cmd/license_list.go b/cmd/license_list.go index 41b2e84f..0ee50247 100644 --- a/cmd/license_list.go +++ b/cmd/license_list.go @@ -50,7 +50,7 @@ const ( MSG_OUTPUT_NO_LICENSES_ONLY_NOASSERTION = "no valid licenses found in BOM document (only licenses marked NOASSERTION)" ) -//"Type", "ID/Name/Expression", "Component(s)", "BOM ref.", "Document location" +// "Type", "ID/Name/Expression", "Component(s)", "BOM ref.", "Document location" // filter keys const ( LICENSE_FILTER_KEY_USAGE_POLICY = "usage-policy" @@ -106,7 +106,7 @@ func NewCommandList() *cobra.Command { command.Use = CMD_USAGE_LICENSE_LIST command.Short = "List licenses found in the BOM input file" command.Long = "List licenses and associated policies found in the BOM input file" - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", FLAG_LICENSE_LIST_OUTPUT_FORMAT_HELP+ LICENSE_LIST_SUPPORTED_FORMATS+ LICENSE_LIST_SUMMARY_SUPPORTED_FORMATS) @@ -162,14 +162,15 @@ func listCmdImpl(cmd *cobra.Command, args []string) (err error) { defer getLogger().Exit() // Create output writer - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) // use function closure to assure consistent error output based upon error type defer func() { // always close the output file if outputFile != nil { err = outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", outputFilename) } }() @@ -177,7 +178,7 @@ func listCmdImpl(cmd *cobra.Command, args []string) (err error) { whereFilters, err := processWhereFlag(cmd) if err == nil { - err = ListLicenses(writer, utils.GlobalFlags.OutputFormat, whereFilters, utils.GlobalFlags.LicenseFlags.Summary) + err = ListLicenses(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, whereFilters, utils.GlobalFlags.LicenseFlags.Summary) } return diff --git a/cmd/license_policy.go b/cmd/license_policy.go index 6b1b6ea3..e90f5816 100644 --- a/cmd/license_policy.go +++ b/cmd/license_policy.go @@ -122,7 +122,7 @@ func NewCommandPolicy() *cobra.Command { command.Use = CMD_USAGE_LICENSE_POLICY command.Short = "List policies associated with known licenses" command.Long = "List caller-supplied, \"allow/deny\"-style policies associated with known software, hardware or data licenses" - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_POLICY_OUTPUT_FORMAT_HELP+LICENSE_POLICY_SUPPORTED_FORMATS) command.Flags().BoolVarP( &utils.GlobalFlags.LicenseFlags.Summary, // re-use license flag @@ -161,14 +161,14 @@ func policyCmdImpl(cmd *cobra.Command, args []string) (err error) { getLogger().Enter(args) defer getLogger().Exit() - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) + outputFile, writer, err := createOutputFile(utils.GlobalFlags.PersistentFlags.OutputFile) // use function closure to assure consistent error output based upon error type defer func() { // always close the output file if outputFile != nil { err = outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.PersistentFlags.OutputFile) } }() @@ -211,7 +211,7 @@ func ListLicensePolicies(writer io.Writer, whereFilters []WhereFilter, flags uti } // default output (writer) to standard out - switch utils.GlobalFlags.OutputFormat { + switch utils.GlobalFlags.PersistentFlags.OutputFormat { case FORMAT_DEFAULT: // defaults to text if no explicit `--format` parameter err = DisplayLicensePoliciesTabbedText(writer, filteredMap, flags) @@ -224,7 +224,7 @@ func ListLicensePolicies(writer io.Writer, whereFilters []WhereFilter, flags uti default: // default to text format for anything else getLogger().Warningf("Unsupported format: `%s`; using default format.", - utils.GlobalFlags.OutputFormat) + utils.GlobalFlags.PersistentFlags.OutputFormat) err = DisplayLicensePoliciesTabbedText(writer, filteredMap, flags) } return diff --git a/cmd/license_policy_test.go b/cmd/license_policy_test.go index 17bba6db..e83012c3 100644 --- a/cmd/license_policy_test.go +++ b/cmd/license_policy_test.go @@ -81,8 +81,8 @@ func innerTestLicensePolicyListCustomAndBuffered(t *testing.T, testInfo *License } // Use the test data to set the BOM input file and output format - utils.GlobalFlags.InputFile = testInfo.InputFile - utils.GlobalFlags.OutputFormat = testInfo.ListFormat + utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile + utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.ListFormat utils.GlobalFlags.LicenseFlags.Summary = testInfo.ListSummary // TODO: pass GlobalConfig to every Command to allow per-instance changes for tests @@ -118,9 +118,9 @@ func innerTestLicensePolicyList(t *testing.T, testInfo *LicenseTestInfo) (output return } -//----------------------------------- +// ----------------------------------- // Usage Policy: allowed value tests -//----------------------------------- +// ----------------------------------- func TestLicensePolicyUsageValueAllow(t *testing.T) { value := POLICY_ALLOW if !IsValidUsagePolicy(value) { diff --git a/cmd/license_test.go b/cmd/license_test.go index 8dfcd60b..ace9b760 100644 --- a/cmd/license_test.go +++ b/cmd/license_test.go @@ -92,7 +92,7 @@ func innerTestLicenseListBuffered(t *testing.T, testInfo *LicenseTestInfo, where defer outputWriter.Flush() // Use a test input SBOM formatted in SPDX - utils.GlobalFlags.InputFile = testInfo.InputFile + utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile // Invoke the actual List command (API) err = ListLicenses(outputWriter, testInfo.ListFormat, whereFilters, testInfo.ListSummary) @@ -288,9 +288,9 @@ func TestLicenseListPolicyCdx14InvalidLicenseName(t *testing.T) { innerTestLicenseList(t, lti) } -//--------------------------- +// --------------------------- // Where filter tests -//--------------------------- +// --------------------------- func TestLicenseListSummaryTextCdx13WhereUsageNeedsReview(t *testing.T) { lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) lti.WhereClause = "usage-policy=needs-review" diff --git a/cmd/query.go b/cmd/query.go index 46489aa7..98f3bbe9 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -64,13 +64,20 @@ var QUERY_SUPPORTED_FORMATS = MSG_SUPPORTED_OUTPUT_FORMATS_HELP + // query JSON map and return selected subset // SELECT -// , , ... // "firstname, lastname, email" || * (default) +// +// , , ... // "firstname, lastname, email" || * (default) +// // FROM -// // "product.customers" +// +// // "product.customers" +// // WHERE -// == // "country='Germany'" +// +// == // "country='Germany'" +// // ORDER BY -// // "lastname" +// +// // "lastname" // // e.g.,SELECT * FROM product.customers WHERE country="Germany"; type QueryRequest struct { @@ -138,7 +145,7 @@ func initCommandQuery(command *cobra.Command) { defer getLogger().Exit() // Add local flags to command - command.PersistentFlags().StringVar(&utils.GlobalFlags.OutputFormat, FLAG_OUTPUT_FORMAT, FORMAT_JSON, + command.PersistentFlags().StringVar(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_OUTPUT_FORMAT, FORMAT_JSON, FLAG_QUERY_OUTPUT_FORMAT_HELP+QUERY_SUPPORTED_FORMATS) command.Flags().StringP(FLAG_QUERY_SELECT, "", QUERY_TOKEN_WILDCARD, FLAG_QUERY_SELECT_HELP) // NOTE: TODO: There appears to be a bug in Cobra where the type of the `from`` flag is `--from` (i.e., not string) diff --git a/cmd/query_test.go b/cmd/query_test.go index f79e6511..3e33b5a8 100644 --- a/cmd/query_test.go +++ b/cmd/query_test.go @@ -46,7 +46,7 @@ func innerQuery(t *testing.T, filename string, queryRequest *QueryRequest, autof } // Copy the test filename to the command line flags were the code looks for it - utils.GlobalFlags.InputFile = filename + utils.GlobalFlags.PersistentFlags.InputFile = filename // allocate response/result object and invoke query var response = new(QueryResponse) diff --git a/cmd/resource.go b/cmd/resource.go index f07661ec..01029c41 100644 --- a/cmd/resource.go +++ b/cmd/resource.go @@ -112,7 +112,7 @@ func NewCommandResource() *cobra.Command { command.Use = CMD_USAGE_RESOURCE_LIST command.Short = "Report on resources found in BOM input file" command.Long = "Report on resources found in BOM input file" - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_RESOURCE_OUTPUT_FORMAT_HELP+RESOURCE_LIST_OUTPUT_SUPPORTED_FORMATS) command.Flags().StringP(FLAG_RESOURCE_TYPE, "", RESOURCE_TYPE_DEFAULT, FLAG_RESOURCE_TYPE_HELP) command.Flags().StringP(FLAG_REPORT_WHERE, "", "", FLAG_REPORT_WHERE_HELP) @@ -168,15 +168,16 @@ func resourceCmdImpl(cmd *cobra.Command, args []string) (err error) { defer getLogger().Exit() // Create output writer - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) - getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer) + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) + getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFilename, writer) // use function closure to assure consistent error output based upon error type defer func() { // always close the output file if outputFile != nil { outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", outputFilename) } }() @@ -187,7 +188,7 @@ func resourceCmdImpl(cmd *cobra.Command, args []string) (err error) { var resourceType string resourceType, err = retrieveResourceType(cmd) - ListResources(writer, utils.GlobalFlags.OutputFormat, resourceType, whereFilters) + ListResources(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, resourceType, whereFilters) return } diff --git a/cmd/resource_test.go b/cmd/resource_test.go index aa4a2a1c..65118783 100644 --- a/cmd/resource_test.go +++ b/cmd/resource_test.go @@ -88,7 +88,7 @@ func innerTestResourceList(t *testing.T, testInfo *ResourceTestInfo) (outputBuff } // The command looks for the input filename in global flags struct - utils.GlobalFlags.InputFile = testInfo.InputFile + utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile // invoke resource list command with a byte buffer outputBuffer, err = innerBufferedTestResourceList(t, testInfo, whereFilters) diff --git a/cmd/root.go b/cmd/root.go index 123b8e1f..040d3105 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -155,15 +155,15 @@ func init() { //rootCmd.PersistentFlags().StringVarP(&utils.GlobalFlags.ConfigCustomValidationFile, FLAG_CONFIG_CUSTOM_VALIDATION, "", DEFAULT_CUSTOM_VALIDATION_CONFIG, "TODO") // Declare top-level, persistent flags and where to place the post-parse values - rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.Trace, FLAG_TRACE, FLAG_TRACE_SHORT, false, MSG_FLAG_TRACE) - rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.Debug, FLAG_DEBUG, FLAG_DEBUG_SHORT, false, MSG_FLAG_DEBUG) - rootCmd.PersistentFlags().StringVarP(&utils.GlobalFlags.InputFile, FLAG_FILENAME_INPUT, FLAG_FILENAME_INPUT_SHORT, "", MSG_FLAG_INPUT) - rootCmd.PersistentFlags().StringVarP(&utils.GlobalFlags.OutputFile, FLAG_FILENAME_OUTPUT, FLAG_FILENAME_OUTPUT_SHORT, "", MSG_FLAG_OUTPUT) + rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.PersistentFlags.Trace, FLAG_TRACE, FLAG_TRACE_SHORT, false, MSG_FLAG_TRACE) + rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.PersistentFlags.Debug, FLAG_DEBUG, FLAG_DEBUG_SHORT, false, MSG_FLAG_DEBUG) + rootCmd.PersistentFlags().StringVarP(&utils.GlobalFlags.PersistentFlags.InputFile, FLAG_FILENAME_INPUT, FLAG_FILENAME_INPUT_SHORT, "", MSG_FLAG_INPUT) + rootCmd.PersistentFlags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFile, FLAG_FILENAME_OUTPUT, FLAG_FILENAME_OUTPUT_SHORT, "", MSG_FLAG_OUTPUT) // NOTE: Although we check for the quiet mode flag in main; we track the flag // using Cobra framework in order to enable more comprehensive help // and take advantage of other features. - rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.Quiet, FLAG_QUIET_MODE, FLAG_QUIET_MODE_SHORT, false, MSG_FLAG_LOG_QUIET) + rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.PersistentFlags.Quiet, FLAG_QUIET_MODE, FLAG_QUIET_MODE_SHORT, false, MSG_FLAG_LOG_QUIET) // Optionally, allow log callstack trace to be indented rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.LogOutputIndentCallstack, FLAG_LOG_OUTPUT_INDENT, "", false, MSG_FLAG_LOG_INDENT) @@ -260,11 +260,11 @@ func preRunTestForInputFile(cmd *cobra.Command, args []string) error { getLogger().Tracef("args: %v", args) // Make sure the input filename is present and exists - file := utils.GlobalFlags.InputFile - if file == "" { + inputFilename := utils.GlobalFlags.PersistentFlags.InputFile + if inputFilename == "" { return getLogger().Errorf("Missing required argument(s): %s", FLAG_FILENAME_INPUT) - } else if _, err := os.Stat(file); err != nil { - return getLogger().Errorf("File not found: `%s`", file) + } else if _, err := os.Stat(inputFilename); err != nil { + return getLogger().Errorf("File not found: `%s`", inputFilename) } return nil } diff --git a/cmd/root_test.go b/cmd/root_test.go index c3579931..a5d6fe8e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -119,9 +119,9 @@ func TestMain(m *testing.M) { flag.Parse() } getLogger().Tracef("Setting Debug=`%t`, Trace=`%t`, Quiet=`%t`,", *TestLogLevelDebug, *TestLogLevelTrace, *TestLogQuiet) - utils.GlobalFlags.Trace = *TestLogLevelTrace - utils.GlobalFlags.Debug = *TestLogLevelDebug - utils.GlobalFlags.Quiet = *TestLogQuiet + utils.GlobalFlags.PersistentFlags.Trace = *TestLogLevelTrace + utils.GlobalFlags.PersistentFlags.Debug = *TestLogLevelDebug + utils.GlobalFlags.PersistentFlags.Quiet = *TestLogQuiet // Load configs, create logger, etc. // NOTE: Be sure ALL "go test" flags are parsed/processed BEFORE initializing diff --git a/cmd/schema.go b/cmd/schema.go index c349027a..40515bdd 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -73,7 +73,7 @@ func NewCommandSchema() *cobra.Command { command.Use = CMD_USAGE_SCHEMA_LIST // "schema" command.Short = "View supported SBOM schemas" command.Long = fmt.Sprintf("View built-in SBOM schemas supported by the utility. The default command produces a list based upon `%s`.", DEFAULT_SCHEMA_CONFIG) - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_SCHEMA_OUTPUT_FORMAT_HELP+SCHEMA_LIST_SUPPORTED_FORMATS) command.Flags().StringP(FLAG_REPORT_WHERE, "", "", FLAG_REPORT_WHERE_HELP) command.RunE = schemaCmdImpl @@ -106,7 +106,8 @@ func schemaCmdImpl(cmd *cobra.Command, args []string) (err error) { defer getLogger().Exit() // Create output writer - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer) // use function closure to assure consistent error output based upon error type @@ -114,7 +115,7 @@ func schemaCmdImpl(cmd *cobra.Command, args []string) (err error) { // always close the output file if outputFile != nil { err = outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", outputFilename) } }() @@ -206,7 +207,8 @@ func ListSchemas(writer io.Writer, whereFilters []WhereFilter) (err error) { } // default output (writer) to standard out - switch utils.GlobalFlags.OutputFormat { + format := utils.GlobalFlags.PersistentFlags.OutputFormat + switch format { case FORMAT_DEFAULT: // defaults to text if no explicit `--format` parameter err = DisplaySchemasTabbedText(writer, filteredSchemas) @@ -218,8 +220,7 @@ func ListSchemas(writer io.Writer, whereFilters []WhereFilter) (err error) { err = DisplaySchemasMarkdown(writer, filteredSchemas) default: // default to text format for anything else - getLogger().Warningf("Unsupported format: `%s`; using default format.", - utils.GlobalFlags.OutputFormat) + getLogger().Warningf("unsupported format: `%s`; using default format.", format) err = DisplaySchemasTabbedText(writer, filteredSchemas) } return diff --git a/cmd/validate.go b/cmd/validate.go index e71c750b..1a33b377 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -89,13 +89,13 @@ func NewCommandValidate() *cobra.Command { command.Short = "Validate input file against its declared BOM schema" command.Long = "Validate input file against its declared BOM schema, if detectable and supported." command.RunE = validateCmdImpl - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", MSG_VALIDATE_FLAG_ERR_FORMAT+VALIDATE_SUPPORTED_ERROR_FORMATS) command.PreRunE = func(cmd *cobra.Command, args []string) error { // This command can be called with this persistent flag, but does not make sense... - inputFile := utils.GlobalFlags.InputFile + inputFile := utils.GlobalFlags.PersistentFlags.InputFile if inputFile != "" { getLogger().Warningf("Invalid flag for command: `%s` (`%s`). Ignoring...", FLAG_FILENAME_OUTPUT, FLAG_FILENAME_OUTPUT_SHORT) } @@ -210,7 +210,8 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. } // Create a loader for the SBOM (JSON) document - documentLoader := gojsonschema.NewReferenceLoader(PROTOCOL_PREFIX_FILE + utils.GlobalFlags.InputFile) + inputFile := utils.GlobalFlags.PersistentFlags.InputFile + documentLoader := gojsonschema.NewReferenceLoader(PROTOCOL_PREFIX_FILE + inputFile) schemaName := document.SchemaInfo.File var schemaLoader gojsonschema.JSONLoader @@ -298,7 +299,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. schemaErrors) // Format error results and append to InvalidSBOMError error "details" - format := utils.GlobalFlags.OutputFormat + format := utils.GlobalFlags.PersistentFlags.OutputFormat errInvalid.Details = FormatSchemaErrors(schemaErrors, utils.GlobalFlags.ValidateFlags, format) return INVALID, document, schemaErrors, errInvalid diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 08b3a333..d02453ad 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -50,12 +50,6 @@ type ValidationResultFormat struct { resultMap *orderedmap.OrderedMap ResultError gojsonschema.ResultError Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() - //Type string `json:"type"` // jsonErrorMap["type"] = resultError.Type() - //Field string `json:"field"` // details["field"] = err.Field() - //Description string `json:"description"` // jsonErrorMap["description"] = resultError.Description() - //DescriptionFormat string `json:"descriptionFormat"` // jsonErrorMap["descriptionFormat"] = resultError.DescriptionFormat() - //Value interface{} `json:"value"` // jsonErrorMap["value"] = resultError.Value() - //Details map[string]interface{} `json:"details"` // jsonErrorMap["details"] = resultError.Details() } func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []byte, err error) { @@ -256,7 +250,7 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat // truncate to a reasonable length using an intelligent separator description = resultError.Description() // truncate output unless debug flag is used - if !utils.GlobalFlags.Debug && + if !utils.GlobalFlags.PersistentFlags.Debug && len(description) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { description, _, _ = strings.Cut(description, ":") description = description + " ... (truncated)" @@ -273,7 +267,7 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat failingObject = fmt.Sprintf("\n\tFailing object: [%v]", formattedValue) // truncate output unless debug flag is used - if !utils.GlobalFlags.Debug && + if !utils.GlobalFlags.PersistentFlags.Debug && len(failingObject) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { failingObject = failingObject[:DEFAULT_MAX_ERR_DESCRIPTION_LEN] failingObject = failingObject + " ... (truncated)" diff --git a/cmd/validate_test.go b/cmd/validate_test.go index cbb997f6..12cb5801 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -55,11 +55,11 @@ func innerValidateError(t *testing.T, filename string, variant string, format st defer getLogger().Exit() // Copy the test filename to the command line flags where the code looks for it - utils.GlobalFlags.InputFile = filename + utils.GlobalFlags.PersistentFlags.InputFile = filename + // Set the err result format + utils.GlobalFlags.PersistentFlags.OutputFormat = format // Set the schema variant where the command line flag would utils.GlobalFlags.ValidateFlags.SchemaVariant = variant - // Set the err result format - utils.GlobalFlags.OutputFormat = format // Invoke the actual validate function var isValid bool diff --git a/cmd/vulnerability.go b/cmd/vulnerability.go index c881d80f..b0310063 100644 --- a/cmd/vulnerability.go +++ b/cmd/vulnerability.go @@ -136,7 +136,7 @@ func NewCommandVulnerability() *cobra.Command { command.Use = CMD_USAGE_VULNERABILITY_LIST command.Short = "Report on vulnerabilities found in the BOM input file" command.Long = "Report on vulnerabilities found in the BOM input file" - command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, + command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_VULNERABILITY_OUTPUT_FORMAT_HELP+VULNERABILITY_LIST_SUPPORTED_FORMATS) command.Flags().StringP(FLAG_REPORT_WHERE, "", "", FLAG_REPORT_WHERE_HELP) command.Flags().BoolVarP( @@ -176,7 +176,8 @@ func vulnerabilityCmdImpl(cmd *cobra.Command, args []string) (err error) { defer getLogger().Exit() // Create output writer - outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile) + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer) // use function closure to assure consistent error output based upon error type @@ -184,7 +185,7 @@ func vulnerabilityCmdImpl(cmd *cobra.Command, args []string) (err error) { // always close the output file if outputFile != nil { err = outputFile.Close() - getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile) + getLogger().Infof("Closed output file: `%s`", outputFilename) } }() @@ -195,7 +196,7 @@ func vulnerabilityCmdImpl(cmd *cobra.Command, args []string) (err error) { return } - err = ListVulnerabilities(writer, utils.GlobalFlags.OutputFormat, whereFilters, utils.GlobalFlags.VulnerabilityFlags) + err = ListVulnerabilities(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, whereFilters, utils.GlobalFlags.VulnerabilityFlags) return } diff --git a/cmd/vulnerability_test.go b/cmd/vulnerability_test.go index 78eba553..3bbadf7f 100644 --- a/cmd/vulnerability_test.go +++ b/cmd/vulnerability_test.go @@ -88,7 +88,7 @@ func innerTestVulnList(t *testing.T, testInfo *VulnTestInfo, flags utils.Vulnera } // The command looks for the input filename in global flags struct - utils.GlobalFlags.InputFile = testInfo.InputFile + utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile // invoke list command with a byte buffer outputBuffer, err = innerBufferedTestVulnList(t, testInfo, whereFilters, flags) diff --git a/schema/schema_formats.go b/schema/schema_formats.go index c3175c06..c2355a39 100644 --- a/schema/schema_formats.go +++ b/schema/schema_formats.go @@ -401,7 +401,7 @@ func (sbom *Sbom) UnmarshalCDXSbom() (err error) { return } -func (sbom *Sbom) FindFormatAndSchema() (err error) { +func (sbom *Sbom) FindFormatAndSchema(sbomFilename string) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -422,7 +422,7 @@ func (sbom *Sbom) FindFormatAndSchema() (err error) { } // if we reach here, we did not find the format in our configuration (list) - err = NewUnknownFormatError(utils.GlobalFlags.InputFile) + err = NewUnknownFormatError(sbomFilename) return } diff --git a/utils/flags.go b/utils/flags.go index 0f60c66f..9dc9078f 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -40,12 +40,7 @@ type CommandFlags struct { ConfigLicensePolicyFile string // persistent flags (common to all commands) - Quiet bool // suppresses all non-essential (informational) output from a command. Overrides any other log-level commands. - Trace bool // trace logging - Debug bool // debug logging - InputFile string - OutputFile string // Note: not used by `validate` command, which emits a warning if supplied - OutputSbomFormat string + PersistentFlags PersistentCommandFlags // Diff flags DiffFlags DiffCommandFlags @@ -57,19 +52,26 @@ type CommandFlags struct { VulnerabilityFlags VulnerabilityCommandFlags // Validate (local) flags - ValidateProperties bool ValidateFlags ValidateCommandFlags CustomValidationOptions CustomValidationFlags - // Summary formats (i.e., only valid for summary) - // NOTE: "query" and "list" (raw) commands always returns JSON by default - OutputFormat string // e.g., TXT (default), CSV, markdown (normalized to lowercase) - // Log indent LogOutputIndentCallstack bool } // NOTE: These flags are shared by both the list and policy subcommands +type PersistentCommandFlags struct { + Quiet bool // suppresses all non-essential (informational) output from a command. Overrides any other log-level commands. + Trace bool // trace logging + Debug bool // debug logging + InputFile string + OutputFile string // Note: not used by `validate` command, which emits a warning if supplied + OutputSbomFormat string + // Summary formats (i.e., only valid for summary) + // NOTE: "query" and "list" (raw) commands always returns JSON by default + OutputFormat string // e.g., TXT (default), CSV, markdown (normalized to lowercase) +} + type DiffCommandFlags struct { Colorize bool RevisedFile string From 4ab94f6ef9486b626450d5d5a1116b778ad18301 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 15:20:58 -0500 Subject: [PATCH 16/28] represent array type, index and item as a map in json error results Signed-off-by: Matt Rutkowski --- cmd/validate.go | 20 ++------------------ cmd/validate_format.go | 29 ++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 1a33b377..cace26da 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -27,7 +27,6 @@ import ( "github.com/CycloneDX/sbom-utility/resources" "github.com/CycloneDX/sbom-utility/schema" "github.com/CycloneDX/sbom-utility/utils" - "github.com/iancoleman/orderedmap" "github.com/spf13/cobra" "github.com/xeipuuv/gojsonschema" ) @@ -43,6 +42,8 @@ const ( FLAG_VALIDATE_SCHEMA_VARIANT = "variant" FLAG_VALIDATE_CUSTOM = "custom" // TODO: document when no longer experimental FLAG_VALIDATE_ERR_LIMIT = "error-limit" + FLAG_VALIDATE_ERR_DETAILS = "error-details" + FLAG_VALIDATE_ERR_VALUES = "error-values" MSG_VALIDATE_SCHEMA_FORCE = "force specified schema file for validation; overrides inferred schema" MSG_VALIDATE_SCHEMA_VARIANT = "select named schema variant (e.g., \"strict\"); variant must be declared in configuration file (i.e., \"config.json\")" MSG_VALIDATE_FLAG_CUSTOM = "perform custom validation using custom configuration settings (i.e., \"custom.json\")" @@ -65,23 +66,6 @@ const ( PROTOCOL_PREFIX_FILE = "file://" ) -func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationResultFormat) { - // Prepare values that are optionally output as JSON - validationErrResult = &ValidationResultFormat{ - ResultError: resultError, - } - // Prepare for JSON output by adding all required fields to our ordered map - validationErrResult.resultMap = orderedmap.New() - validationErrResult.resultMap.Set("type", resultError.Type()) - validationErrResult.resultMap.Set("field", resultError.Field()) - if context := resultError.Context(); context != nil { - validationErrResult.resultMap.Set("context", resultError.Context().String()) - } - validationErrResult.resultMap.Set("description", resultError.Description()) - - return -} - func NewCommandValidate() *cobra.Command { // NOTE: `RunE` function takes precedent over `Run` (anonymous) function if both provided var command = new(cobra.Command) diff --git a/cmd/validate_format.go b/cmd/validate_format.go index d02453ad..cd16d6a0 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -32,6 +32,8 @@ const ( ERROR_DETAIL_KEY_VALUE = "value" ERROR_DETAIL_KEY_DATA_TYPE = "type" ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY = "array" + ERROR_DETAIL_KEY_VALUE_INDEX = "index" + ERROR_DETAIL_KEY_VALUE_ITEM = "item" ERROR_DETAIL_ARRAY_ITEM_INDEX_I = "i" ERROR_DETAIL_ARRAY_ITEM_INDEX_J = "j" ) @@ -48,10 +50,28 @@ type ValidationResultFormatter struct { // JsonContext is a linked-list of JSON key strings type ValidationResultFormat struct { resultMap *orderedmap.OrderedMap + valuesMap *orderedmap.OrderedMap ResultError gojsonschema.ResultError Context *gojsonschema.JsonContext `json:"context"` // jsonErrorMap["context"] = resultError.Context() } +func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErrResult *ValidationResultFormat) { + // Prepare values that are optionally output as JSON + validationErrResult = &ValidationResultFormat{ + ResultError: resultError, + } + // Prepare for JSON output by adding all required fields to our ordered map + validationErrResult.resultMap = orderedmap.New() + validationErrResult.resultMap.Set("type", resultError.Type()) + validationErrResult.resultMap.Set("field", resultError.Field()) + if context := resultError.Context(); context != nil { + validationErrResult.resultMap.Set("context", resultError.Context().String()) + } + validationErrResult.resultMap.Set("description", resultError.Description()) + + return +} + func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []byte, err error) { return validationErrResult.resultMap.MarshalJSON() } @@ -97,9 +117,12 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo i, indexValid := index.(int) // verify the claimed item index is within range if arrayValid && indexValid && i < len(array) { - result.resultMap.Set( - fmt.Sprintf("item[%v]", i), - array[i]) + // Add just the first array item to the value key + result.valuesMap = orderedmap.New() + result.valuesMap.Set(ERROR_DETAIL_KEY_DATA_TYPE, valueType) + result.valuesMap.Set(ERROR_DETAIL_KEY_VALUE_INDEX, i) + result.valuesMap.Set(ERROR_DETAIL_KEY_VALUE_ITEM, array[i]) + result.resultMap.Set(ERROR_DETAIL_KEY_VALUE, result.valuesMap) } } } From 5b57e38f4745043b4c4e3ac4280a479239a723ef Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 16:00:06 -0500 Subject: [PATCH 17/28] Support flag true|false on validate command Signed-off-by: Matt Rutkowski --- .github/workflows/release.yml | 2 +- cmd/validate.go | 45 +++++++++++++++++++++++------------ cmd/validate_format.go | 19 +++++++++------ cmd/validate_test.go | 3 ++- utils/flags.go | 16 +++++-------- 5 files changed, 51 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ac72228..3eef386e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} - extra_files: LICENSE config.json license.json custom.json ${{env.SBOM_NAME}} + extra_files: LICENSE README.md config.json license.json custom.json ${{env.SBOM_NAME}} # "auto" will use ZIP for Windows, otherwise default is TAR compress_assets: auto # NOTE: This verbose flag may be removed diff --git a/cmd/validate.go b/cmd/validate.go index cace26da..412828e5 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -37,19 +37,22 @@ const ( ) // validation flags +// TODO: support a `--truncate “ flag (or similar... `err-value-truncate` ) used +// to truncate formatted "value" (details) to bytes. +// This would replace the hardcoded "DEFAULT_MAX_ERR_DESCRIPTION_LEN" value const ( FLAG_VALIDATE_SCHEMA_FORCE = "force" FLAG_VALIDATE_SCHEMA_VARIANT = "variant" FLAG_VALIDATE_CUSTOM = "custom" // TODO: document when no longer experimental FLAG_VALIDATE_ERR_LIMIT = "error-limit" - FLAG_VALIDATE_ERR_DETAILS = "error-details" - FLAG_VALIDATE_ERR_VALUES = "error-values" + FLAG_VALIDATE_ERR_VALUE = "error-value" MSG_VALIDATE_SCHEMA_FORCE = "force specified schema file for validation; overrides inferred schema" MSG_VALIDATE_SCHEMA_VARIANT = "select named schema variant (e.g., \"strict\"); variant must be declared in configuration file (i.e., \"config.json\")" MSG_VALIDATE_FLAG_CUSTOM = "perform custom validation using custom configuration settings (i.e., \"custom.json\")" MSG_VALIDATE_FLAG_ERR_COLORIZE = "Colorize formatted error output (true|false); default true" - MSG_VALIDATE_FLAG_ERR_LIMIT = "Limit number of errors output (integer); default 10" + MSG_VALIDATE_FLAG_ERR_LIMIT = "Limit number of errors output to specified (integer) (default 10)" MSG_VALIDATE_FLAG_ERR_FORMAT = "format error results using the specified format type" + MSG_VALIDATE_FLAG_ERR_VALUE = "include details of failing value in error results (bool) (default: true)" ) var VALIDATE_SUPPORTED_ERROR_FORMATS = MSG_VALIDATE_FLAG_ERR_FORMAT + @@ -75,7 +78,6 @@ func NewCommandValidate() *cobra.Command { command.RunE = validateCmdImpl command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "", MSG_VALIDATE_FLAG_ERR_FORMAT+VALIDATE_SUPPORTED_ERROR_FORMATS) - command.PreRunE = func(cmd *cobra.Command, args []string) error { // This command can be called with this persistent flag, but does not make sense... @@ -86,12 +88,12 @@ func NewCommandValidate() *cobra.Command { return preRunTestForInputFile(cmd, args) } - initCommandValidate(command) + initCommandValidateFlags(command) return command } // Add local flags to validate command -func initCommandValidate(command *cobra.Command) { +func initCommandValidateFlags(command *cobra.Command) { getLogger().Enter() defer getLogger().Exit() @@ -103,17 +105,31 @@ func initCommandValidate(command *cobra.Command) { // Colorize default: true (for historical reasons) command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_VALIDATE_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_VALIDATE_FLAG_ERR_LIMIT) + command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ShowErrorValue, FLAG_VALIDATE_ERR_VALUE, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) } func validateCmdImpl(cmd *cobra.Command, args []string) error { getLogger().Enter() defer getLogger().Exit() + // TODO - support an output file for errors + // Create output writer + // outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + // outputFile, writer, err := createOutputFile(outputFilename) + + // // use function closure to assure consistent error output based upon error type + // defer func() { + // // always close the output file + // if outputFile != nil { + // err = outputFile.Close() + // getLogger().Infof("Closed output file: `%s`", outputFilename) + // } + // }() + // invoke validate and consistently manage exit messages and codes - isValid, _, _, err := Validate() + isValid, _, _, err := Validate(utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) - // Note: all invalid SBOMs (that fail schema validation) SHOULD result in an - // InvalidSBOMError() + // Note: all invalid SBOMs (that fail schema validation) MUST result in an InvalidSBOMError() if err != nil { if IsInvalidSBOMError(err) { os.Exit(ERROR_VALIDATION) @@ -164,7 +180,7 @@ func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) getLogger().Info(message) } -func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema.ResultError, err error) { +func Validate(persistentFlags utils.PersistentCommandFlags, validateFlags utils.ValidateCommandFlags) (valid bool, document *schema.Sbom, schemaErrors []gojsonschema.ResultError, err error) { getLogger().Enter() defer getLogger().Exit() @@ -183,7 +199,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. } // if "custom" flag exists, then assure we support the format - if utils.GlobalFlags.ValidateFlags.CustomValidation && !document.FormatInfo.IsCycloneDx() { + if validateFlags.CustomValidation && !document.FormatInfo.IsCycloneDx() { err = schema.NewUnsupportedFormatError( schema.MSG_FORMAT_UNSUPPORTED_COMMAND, document.GetFilename(), @@ -194,7 +210,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. } // Create a loader for the SBOM (JSON) document - inputFile := utils.GlobalFlags.PersistentFlags.InputFile + inputFile := persistentFlags.InputFile documentLoader := gojsonschema.NewReferenceLoader(PROTOCOL_PREFIX_FILE + inputFile) schemaName := document.SchemaInfo.File @@ -205,7 +221,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. // If caller "forced" a specific schema file (version), load it instead of // any SchemaInfo found in config.json // TODO: support remote schema load (via URL) with a flag (default should always be local file for security) - forcedSchemaFile := utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile + forcedSchemaFile := validateFlags.ForcedJsonSchemaFile if forcedSchemaFile != "" { getLogger().Infof("Validating document using forced schema (i.e., `--force %s`)", forcedSchemaFile) //schemaName = document.SchemaInfo.File @@ -283,8 +299,7 @@ func Validate() (valid bool, document *schema.Sbom, schemaErrors []gojsonschema. schemaErrors) // Format error results and append to InvalidSBOMError error "details" - format := utils.GlobalFlags.PersistentFlags.OutputFormat - errInvalid.Details = FormatSchemaErrors(schemaErrors, utils.GlobalFlags.ValidateFlags, format) + errInvalid.Details = FormatSchemaErrors(schemaErrors, validateFlags, persistentFlags.OutputFormat) return INVALID, document, schemaErrors, errInvalid } diff --git a/cmd/validate_format.go b/cmd/validate_format.go index cd16d6a0..8d3077d5 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -76,12 +76,12 @@ func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []b return validationErrResult.resultMap.MarshalJSON() } -func (result *ValidationResultFormat) Format(showValue bool, flags utils.ValidateCommandFlags) string { +func (result *ValidationResultFormat) Format(flags utils.ValidateCommandFlags) string { var sb strings.Builder // Conditionally, add optional values as requested - if showValue { + if flags.ShowErrorValue { result.resultMap.Set(ERROR_DETAIL_KEY_VALUE, result.ResultError.Value()) } @@ -95,7 +95,7 @@ func (result *ValidationResultFormat) Format(showValue bool, flags utils.Validat return sb.String() } -func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue bool, flags utils.ValidateCommandFlags) string { +func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.ValidateCommandFlags) string { var sb strings.Builder @@ -103,8 +103,8 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(showValue boo // For this error type, we want to reduce the information show to the end user. // Originally, the entire array with duplicate items was show for EVERY occurrence; // attempt to only show the failing item itself once (and only once) - // TODO: deduplication (planned) will also help shrink large error output - if showValue { + // TODO: deduplication (planned) will also help shrink large error output results + if flags.ShowErrorValue { details := result.ResultError.Details() valueType, typeFound := details[ERROR_DETAIL_KEY_DATA_TYPE] // verify the claimed type is an array @@ -154,10 +154,13 @@ func FormatSchemaErrors(schemaErrors []gojsonschema.ResultError, flags utils.Val return } +// Custom formatting based upon possible JSON schema error types func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.ValidateCommandFlags) (formattedResult string) { validationErrorResult := NewValidationErrResult(resultError) + // The cases below represent the complete set of typed errors possible. + // Most are commented out as placeholder for future custom format methods. switch errorType := resultError.(type) { // case *gojsonschema.AdditionalPropertyNotAllowedError: // case *gojsonschema.ArrayContainsError: @@ -178,7 +181,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va // case *gojsonschema.InvalidPropertyPatternError: // case *gojsonschema.InvalidTypeError: case *gojsonschema.ItemsMustBeUniqueError: - formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(true, flags) + formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(flags) // case *gojsonschema.MissingDependencyError: // case *gojsonschema.MultipleOfError: // case *gojsonschema.NumberAllOfError: @@ -194,7 +197,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va // case *gojsonschema.StringLengthLTEError: default: getLogger().Debugf("default formatting: ResultError Type: [%v]", errorType) - formattedResult = validationErrorResult.Format(true, flags) + formattedResult = validationErrorResult.Format(flags) } return @@ -283,6 +286,8 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat // as this slows down processing on SBOMs with large numbers of errors if colorize { formattedValue, _ = log.FormatInterfaceAsColorizedJson(resultError.Value()) + } else { + formattedValue, _ = log.FormatInterfaceAsJson(resultError.Value()) } // Indent error detail output in logs formattedValue = log.AddTabs(formattedValue) diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 12cb5801..0f0f43dc 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -63,7 +63,8 @@ func innerValidateError(t *testing.T, filename string, variant string, format st // Invoke the actual validate function var isValid bool - isValid, document, schemaErrors, actualError = Validate() + //isValid, document, schemaErrors, actualError = Validate() + isValid, document, schemaErrors, actualError = Validate(utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) getLogger().Tracef("document: `%s`, isValid=`%t`, actualError=`%T`", document.GetFilename(), isValid, actualError) diff --git a/utils/flags.go b/utils/flags.go index 9dc9078f..db5db6bf 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -61,15 +61,12 @@ type CommandFlags struct { // NOTE: These flags are shared by both the list and policy subcommands type PersistentCommandFlags struct { - Quiet bool // suppresses all non-essential (informational) output from a command. Overrides any other log-level commands. - Trace bool // trace logging - Debug bool // debug logging - InputFile string - OutputFile string // Note: not used by `validate` command, which emits a warning if supplied - OutputSbomFormat string - // Summary formats (i.e., only valid for summary) - // NOTE: "query" and "list" (raw) commands always returns JSON by default - OutputFormat string // e.g., TXT (default), CSV, markdown (normalized to lowercase) + Quiet bool // suppresses all non-essential (informational) output from a command. Overrides any other log-level commands. + Trace bool // trace logging + Debug bool // debug logging + InputFile string + OutputFile string // TODO: TODO: Note: not used by `validate` command, which emits a warning if supplied + OutputFormat string // e.g., "txt", "csv"", "md" (markdown) (normalized to lowercase) } type DiffCommandFlags struct { @@ -92,7 +89,6 @@ type ValidateCommandFlags struct { MaxErrorDescriptionLength int ColorizeErrorOutput bool ShowErrorValue bool - ShowErrorDetail bool } type VulnerabilityCommandFlags struct { From a630b7e09e0ad54de349127d290c63d4453f1739 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Wed, 21 Jun 2023 17:53:32 -0500 Subject: [PATCH 18/28] Fix even more Sonatype errors that seem to chnage every time I touch an old file Signed-off-by: Matt Rutkowski --- cmd/errors.go | 9 ++++----- cmd/resource.go | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/errors.go b/cmd/errors.go index 1c07c28b..843ab2aa 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -116,11 +116,10 @@ func (err BaseError) Error() string { return formattedMessage } -//nolint:all -func (base BaseError) AppendMessage(addendum string) { - // Ignore (invalid) static linting message: - // "ineffective assignment to field (SA4005)" - base.Message += addendum //nolint:staticcheck +func (err *BaseError) AppendMessage(addendum string) { + if addendum != "" { + err.Message += addendum + } } type UtilityError struct { diff --git a/cmd/resource.go b/cmd/resource.go index 01029c41..f5c52d3e 100644 --- a/cmd/resource.go +++ b/cmd/resource.go @@ -188,7 +188,9 @@ func resourceCmdImpl(cmd *cobra.Command, args []string) (err error) { var resourceType string resourceType, err = retrieveResourceType(cmd) - ListResources(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, resourceType, whereFilters) + if err == nil { + err = ListResources(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, resourceType, whereFilters) + } return } From 16f8ce6077041aa02cc6689297bb7afd2271c177 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Thu, 22 Jun 2023 10:41:48 -0500 Subject: [PATCH 19/28] Adjust help for validate given new formats/flags Signed-off-by: Matt Rutkowski --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 040d3105..979892fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -55,7 +55,7 @@ const ( CMD_USAGE_QUERY = CMD_QUERY + " --input-file [--select * | field1[,fieldN]] [--from [key1[.keyN]] [--where key=regex[,...]]" CMD_USAGE_RESOURCE_LIST = CMD_RESOURCE + " --input-file [--type component|service] [--where key=regex[,...]] [--format txt|csv|md]" CMD_USAGE_SCHEMA_LIST = CMD_SCHEMA + " [--where key=regex[,...]] [--format txt|csv|md]" - CMD_USAGE_VALIDATE = CMD_VALIDATE + " --input-file [--variant ] [--error-limit ] [--colorize=true|false] [--force schema_file]" + CMD_USAGE_VALIDATE = CMD_VALIDATE + " --input-file [--variant ] [--format txt|json] [--force schema_file]" CMD_USAGE_VULNERABILITY_LIST = CMD_VULNERABILITY + " " + SUBCOMMAND_VULNERABILITY_LIST + " --input-file [--summary] [--where key=regex[,...]] [--format json|txt|csv|md]" ) From ff1c2780730ee699675de2ea50cfe93f65522a0f Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Thu, 22 Jun 2023 12:03:06 -0500 Subject: [PATCH 20/28] Update README to show validate JSON output and new flags Signed-off-by: Matt Rutkowski --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99965217..e8512795 100644 --- a/README.md +++ b/README.md @@ -804,11 +804,15 @@ The following flags can be used to improve performance when formatting error out ##### `--error-limit` flag -Use the `--error-limit x` flag to reduce the formatted error result output to the first `x` errors. By default, only the first 10 errors are output with an informational messaging indicating `x/y` errors were shown. +Use the `--error-limit x` (default: `10`) flag to reduce the formatted error result output to the first `x` errors. By default, only the first 10 errors are output with an informational messaging indicating `x/y` errors were shown. + +##### `--error-value` flag + +Use the `--error-value=true|false` (default: `true`)flag to reduce the formatted error result output by not showing the `value` field which shows detailed information about the failing data in the BOM. ##### `--colorize` flag -Use the `--colorize=true|false` flag to add/remove color formatting to error result output. By default, formatted error output is colorized to help with human readability; for automated use, it can be turned off. +Use the `--colorize=true|false` (default: `true`) flag to add/remove color formatting to error result `txt` formatted output. By default, `txt` formatted error output is colorized to help with human readability; for automated use, it can be turned off. #### Validate Examples @@ -911,6 +915,108 @@ The details include the full context of the failing `metadata.properties` object ]] ``` +#### Example: Validate using "JSON" format + +The JSON format will provide an `array` of schema error results that can be post-processed as part of validation toolchain. + +```bash +./sbom-utility validate -i test/validation/cdx-1-4-validate-err-components-unique-items-1.json --format json --quiet +``` + +```json +[ + { + "type": "unique", + "field": "components", + "context": "(root).components", + "description": "array items[1,2] must be unique", + "value": { + "type": "array", + "index": 1, + "item": { + "bom-ref": "pkg:npm/body-parser@1.19.0", + "description": "Node.js body parsing middleware", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "name": "body-parser", + "purl": "pkg:npm/body-parser@1.19.0", + "type": "library", + "version": "1.19.0" + } + } + }, + { + "type": "unique", + "field": "components", + "context": "(root).components", + "description": "array items[2,4] must be unique", + "value": { + "type": "array", + "index": 2, + "item": { + "bom-ref": "pkg:npm/body-parser@1.19.0", + "description": "Node.js body parsing middleware", + "hashes": [ + { + "alg": "SHA-1", + "content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "name": "body-parser", + "purl": "pkg:npm/body-parser@1.19.0", + "type": "library", + "version": "1.19.0" + } + } + } +] +``` + +##### Reducing output size using `error-value=false` flag + +In many cases, BOMs may have many errors and having the `value` information details included can be too verbose and lead to large output files to inspect. In those cases, simply set the `error-value` flag to `false`. + +Rerunning the same command with this flag set to false yields a reduced set of information. + +```bash +./sbom-utility validate -i test/validation/cdx-1-4-validate-err-components-unique-items-1.json --format json --error-value=false --quiet +``` + +```json +[ + { + "type": "unique", + "field": "components", + "context": "(root).components", + "description": "array items[1,2] must be unique" + }, + { + "type": "unique", + "field": "components", + "context": "(root).components", + "description": "array items[2,4] must be unique" + } +] +``` + --- ### Vulnerability From 7258d9e54267aaf091fa34b9417ea1b47a71767d Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Thu, 22 Jun 2023 13:32:58 -0500 Subject: [PATCH 21/28] buffer JSON output for unit tests Signed-off-by: Matt Rutkowski --- cmd/root.go | 2 +- cmd/validate.go | 39 ++++++++++++++++++++++++--------------- cmd/validate_test.go | 24 ++++++++++++++++++++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 979892fd..0909cbfa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -85,7 +85,7 @@ const ( MSG_FLAG_DEBUG = "enable debug logging" MSG_FLAG_INPUT = "input filename (e.g., \"path/sbom.json\")" MSG_FLAG_OUTPUT = "output filename" - MSG_FLAG_LOG_QUIET = "enable quiet logging mode (removes all information messages from console output); overrides other logging commands" + MSG_FLAG_LOG_QUIET = "enable quiet logging mode (removes all informational messages from console output); overrides other logging commands" MSG_FLAG_LOG_INDENT = "enable log indentation of functional callstack" MSG_FLAG_CONFIG_SCHEMA = "provide custom application schema configuration file (i.e., overrides default `config.json`)" MSG_FLAG_CONFIG_LICENSE = "provide custom application license policy configuration file (i.e., overrides default `license.json`)" diff --git a/cmd/validate.go b/cmd/validate.go index 412828e5..0a893ade 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -21,6 +21,7 @@ package cmd import ( "encoding/json" "fmt" + "io" "os" "strings" @@ -112,22 +113,21 @@ func validateCmdImpl(cmd *cobra.Command, args []string) error { getLogger().Enter() defer getLogger().Exit() - // TODO - support an output file for errors // Create output writer - // outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile - // outputFile, writer, err := createOutputFile(outputFilename) - - // // use function closure to assure consistent error output based upon error type - // defer func() { - // // always close the output file - // if outputFile != nil { - // err = outputFile.Close() - // getLogger().Infof("Closed output file: `%s`", outputFilename) - // } - // }() + outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile + outputFile, writer, err := createOutputFile(outputFilename) + + // use function closure to assure consistent error output based upon error type + defer func() { + // always close the output file + if outputFile != nil { + err = outputFile.Close() + getLogger().Infof("Closed output file: `%s`", outputFilename) + } + }() // invoke validate and consistently manage exit messages and codes - isValid, _, _, err := Validate(utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) + isValid, _, _, err := Validate(writer, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) // Note: all invalid SBOMs (that fail schema validation) MUST result in an InvalidSBOMError() if err != nil { @@ -180,7 +180,7 @@ func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) getLogger().Info(message) } -func Validate(persistentFlags utils.PersistentCommandFlags, validateFlags utils.ValidateCommandFlags) (valid bool, document *schema.Sbom, schemaErrors []gojsonschema.ResultError, err error) { +func Validate(output io.Writer, persistentFlags utils.PersistentCommandFlags, validateFlags utils.ValidateCommandFlags) (valid bool, document *schema.Sbom, schemaErrors []gojsonschema.ResultError, err error) { getLogger().Enter() defer getLogger().Exit() @@ -299,7 +299,14 @@ func Validate(persistentFlags utils.PersistentCommandFlags, validateFlags utils. schemaErrors) // Format error results and append to InvalidSBOMError error "details" - errInvalid.Details = FormatSchemaErrors(schemaErrors, validateFlags, persistentFlags.OutputFormat) + formattedErrors := FormatSchemaErrors(schemaErrors, validateFlags, persistentFlags.OutputFormat) + errInvalid.Details = formattedErrors + + // Always produce JSON output (since it is considered non-informational), ignoring the `--quiet` flags + if persistentFlags.Quiet && persistentFlags.OutputFormat == FORMAT_JSON { + // Note: JSON data files MUST ends in a newline s as this is a POSIX standard + fmt.Fprintf(output, "%s\n", formattedErrors) + } return INVALID, document, schemaErrors, errInvalid } @@ -313,6 +320,8 @@ func Validate(persistentFlags utils.PersistentCommandFlags, validateFlags utils. } } + // TODO: Need to perhaps factor in these errors into the JSON output as if they + // were actual schema errors... // Perform additional validation in document composition/structure // and "custom" required data within specified fields if utils.GlobalFlags.ValidateFlags.CustomValidation { diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 0f0f43dc..2946ddaf 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -18,9 +18,12 @@ package cmd import ( + "bufio" + "bytes" "encoding/json" "fmt" "io/fs" + "os" "testing" "github.com/CycloneDX/sbom-utility/schema" @@ -63,8 +66,8 @@ func innerValidateError(t *testing.T, filename string, variant string, format st // Invoke the actual validate function var isValid bool - //isValid, document, schemaErrors, actualError = Validate() - isValid, document, schemaErrors, actualError = Validate(utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) + + isValid, document, schemaErrors, actualError = Validate(os.Stdout, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) getLogger().Tracef("document: `%s`, isValid=`%t`, actualError=`%T`", document.GetFilename(), isValid, actualError) @@ -96,6 +99,19 @@ func innerValidateError(t *testing.T, filename string, variant string, format st return } +func innerValidateErrorBuffered(t *testing.T, filename string, variant string, format string, expectedError error) (schemaErrors []gojsonschema.ResultError, outputBuffer bytes.Buffer, err error) { + // Declare an output outputBuffer/outputWriter to use used during tests + var outputWriter = bufio.NewWriter(&outputBuffer) + // ensure all data is written to buffer before further validation + defer outputWriter.Flush() + + // Invoke the actual command (API) + isValid, document, schemaErrors, actualError := Validate(outputWriter, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) + getLogger().Tracef("document: `%s`, isValid=`%t`, actualError=`%T`", document.GetFilename(), isValid, actualError) + + return +} + // Tests *ErrorInvalidSBOM error types and any (lower-level) errors they "wrapped" func innerValidateInvalidSBOMInnerError(t *testing.T, filename string, variant string, innerError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { getLogger().Enter() @@ -277,7 +293,7 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateError(t, + innerValidateErrorBuffered(t, TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, SCHEMA_VARIANT_NONE, FORMAT_JSON, @@ -286,7 +302,7 @@ func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateError(t, + innerValidateErrorBuffered(t, TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, SCHEMA_VARIANT_NONE, FORMAT_JSON, From 577e945c6ab7bfa32ef67efba586511b2f2ba8b1 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 23 Jun 2023 09:33:12 -0500 Subject: [PATCH 22/28] Update the text format logic to mirror new json formatting Signed-off-by: Matt Rutkowski --- cmd/validate.go | 75 +++++++++++++++++------------ cmd/validate_format.go | 104 +++++++++++++++++++++++------------------ cmd/validate_test.go | 27 +++++------ log/format.go | 10 ++++ 4 files changed, 123 insertions(+), 93 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 0a893ade..c4274578 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -103,8 +103,7 @@ func initCommandValidateFlags(command *cobra.Command) { // Optional schema "variant" of inferred schema (e.g, "strict") command.Flags().StringVarP(&utils.GlobalFlags.ValidateFlags.SchemaVariant, FLAG_VALIDATE_SCHEMA_VARIANT, "", "", MSG_VALIDATE_SCHEMA_VARIANT) command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.CustomValidation, FLAG_VALIDATE_CUSTOM, "", false, MSG_VALIDATE_FLAG_CUSTOM) - // Colorize default: true (for historical reasons) - command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput, FLAG_COLORIZE_OUTPUT, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) + command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput, FLAG_COLORIZE_OUTPUT, "", false, MSG_VALIDATE_FLAG_ERR_COLORIZE) command.Flags().IntVarP(&utils.GlobalFlags.ValidateFlags.MaxNumErrors, FLAG_VALIDATE_ERR_LIMIT, "", DEFAULT_MAX_ERROR_LIMIT, MSG_VALIDATE_FLAG_ERR_LIMIT) command.Flags().BoolVarP(&utils.GlobalFlags.ValidateFlags.ShowErrorValue, FLAG_VALIDATE_ERR_VALUE, "", true, MSG_VALIDATE_FLAG_ERR_COLORIZE) } @@ -166,7 +165,7 @@ func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) // Note: InvalidSBOMError type errors include schema errors which have already // been added to the error type and will shown with the Error() interface if valid { - getLogger().Errorf("invalid state: error (%T) returned, but SBOM valid !!!", t) + getLogger().Errorf("invalid state: error (%T) returned, but SBOM valid!", t) } getLogger().Error(err) default: @@ -298,49 +297,63 @@ func Validate(output io.Writer, persistentFlags utils.PersistentCommandFlags, va nil, schemaErrors) - // Format error results and append to InvalidSBOMError error "details" - formattedErrors := FormatSchemaErrors(schemaErrors, validateFlags, persistentFlags.OutputFormat) - errInvalid.Details = formattedErrors - - // Always produce JSON output (since it is considered non-informational), ignoring the `--quiet` flags - if persistentFlags.Quiet && persistentFlags.OutputFormat == FORMAT_JSON { + // TODO: de-duplicate errors (e.g., array item not "unique"...) + var formattedErrors string + switch persistentFlags.OutputFormat { + case FORMAT_JSON: // Note: JSON data files MUST ends in a newline s as this is a POSIX standard - fmt.Fprintf(output, "%s\n", formattedErrors) + formattedErrors = FormatSchemaErrors(schemaErrors, validateFlags, FORMAT_JSON) + fmt.Fprintf(output, "%s", formattedErrors) + case FORMAT_TEXT: + fallthrough + default: + // Format error results and append to InvalidSBOMError error "details" + formattedErrors = FormatSchemaErrors(schemaErrors, validateFlags, FORMAT_TEXT) + errInvalid.Details = formattedErrors } return INVALID, document, schemaErrors, errInvalid } + // TODO: Need to perhaps factor in these errors into the JSON output as if they + // were actual schema errors... + // Perform additional validation in document composition/structure + // and "custom" required data within specified fields + if validateFlags.CustomValidation { + valid, err = validateCustom(document) + } + + // All validation tests passed; return VALID + return +} + +func validateCustom(document *schema.Sbom) (valid bool, err error) { + // If the validated SBOM is of a known format, we can unmarshal it into // more convenient typed structure for simplified custom validation if document.FormatInfo.IsCycloneDx() { document.CdxBom, err = schema.UnMarshalDocument(document.GetJSONMap()) if err != nil { - return INVALID, document, schemaErrors, err + return INVALID, err } } - // TODO: Need to perhaps factor in these errors into the JSON output as if they - // were actual schema errors... - // Perform additional validation in document composition/structure - // and "custom" required data within specified fields - if utils.GlobalFlags.ValidateFlags.CustomValidation { - // Perform all custom validation - err := validateCustomCDXDocument(document) - if err != nil { - // Wrap any specific validation error in a single invalid SBOM error - if !IsInvalidSBOMError(err) { - err = NewInvalidSBOMError( - document, - err.Error(), - err, - nil) - } - // an error implies it is also invalid (according to custom requirements) - return INVALID, document, schemaErrors, err + // Perform all custom validation + // TODO Implement customValidation as an interface supported by the CDXDocument type + // and later supported by a SPDXDocument type. + err = validateCustomCDXDocument(document) + if err != nil { + // Wrap any specific validation error in a single invalid SBOM error + if !IsInvalidSBOMError(err) { + err = NewInvalidSBOMError( + document, + err.Error(), + err, + nil) } + // an error implies it is also invalid (according to custom requirements) + return INVALID, err } - // All validation tests passed; return VALID - return + return VALID, nil } diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 8d3077d5..6b9b9e85 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -29,18 +29,22 @@ import ( ) const ( - ERROR_DETAIL_KEY_VALUE = "value" - ERROR_DETAIL_KEY_DATA_TYPE = "type" - ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY = "array" - ERROR_DETAIL_KEY_VALUE_INDEX = "index" - ERROR_DETAIL_KEY_VALUE_ITEM = "item" - ERROR_DETAIL_ARRAY_ITEM_INDEX_I = "i" - ERROR_DETAIL_ARRAY_ITEM_INDEX_J = "j" + ERROR_DETAIL_KEY_FIELD = "field" + ERROR_DETAIL_KEY_CONTEXT = "context" + ERROR_DETAIL_KEY_VALUE = "value" + ERROR_DETAIL_KEY_DATA_TYPE = "type" + ERROR_DETAIL_KEY_VALUE_TYPE_ARRAY = "array" + ERROR_DETAIL_KEY_VALUE_INDEX = "index" + ERROR_DETAIL_KEY_VALUE_ITEM = "item" + ERROR_DETAIL_KEY_VALUE_DESCRIPTION = "description" + ERROR_DETAIL_ARRAY_ITEM_INDEX_I = "i" + ERROR_DETAIL_ARRAY_ITEM_INDEX_J = "j" ) const ( - ERROR_DETAIL_JSON_DEFAULT_PREFIX = " " - ERROR_DETAIL_JSON_DEFAULT_INDENT = " " + ERROR_DETAIL_JSON_DEFAULT_PREFIX = " " + ERROR_DETAIL_JSON_DEFAULT_INDENT = " " + ERROR_DETAIL_JSON_CONTEXT_DELIMITER = "." ) type ValidationResultFormatter struct { @@ -62,12 +66,12 @@ func NewValidationErrResult(resultError gojsonschema.ResultError) (validationErr } // Prepare for JSON output by adding all required fields to our ordered map validationErrResult.resultMap = orderedmap.New() - validationErrResult.resultMap.Set("type", resultError.Type()) - validationErrResult.resultMap.Set("field", resultError.Field()) + validationErrResult.resultMap.Set(ERROR_DETAIL_KEY_DATA_TYPE, resultError.Type()) + validationErrResult.resultMap.Set(ERROR_DETAIL_KEY_FIELD, resultError.Field()) if context := resultError.Context(); context != nil { - validationErrResult.resultMap.Set("context", resultError.Context().String()) + validationErrResult.resultMap.Set(ERROR_DETAIL_KEY_CONTEXT, resultError.Context().String()) } - validationErrResult.resultMap.Set("description", resultError.Description()) + validationErrResult.resultMap.Set(ERROR_DETAIL_KEY_VALUE_DESCRIPTION, resultError.Description()) return } @@ -85,8 +89,11 @@ func (result *ValidationResultFormat) Format(flags utils.ValidateCommandFlags) s result.resultMap.Set(ERROR_DETAIL_KEY_VALUE, result.ResultError.Value()) } - // TODO: add a general JSON formatting flag - formattedResult, err := log.FormatIndentedInterfaceAsJson(result.resultMap, ERROR_DETAIL_JSON_DEFAULT_PREFIX, ERROR_DETAIL_JSON_DEFAULT_INDENT) + formattedResult, err := log.FormatIndentedInterfaceAsJson( + result.resultMap, + ERROR_DETAIL_JSON_DEFAULT_PREFIX, + ERROR_DETAIL_JSON_DEFAULT_INDENT, + ) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) } @@ -128,8 +135,12 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.V } } - // TODO: add a general JSON formatting flag - formattedResult, err := log.FormatIndentedInterfaceAsJson(result.resultMap, ERROR_DETAIL_JSON_DEFAULT_PREFIX, ERROR_DETAIL_JSON_DEFAULT_INDENT) + // format information on the failing "value" (details) with proper JSON indenting + formattedResult, err := log.FormatIndentedInterfaceAsJson( + result.resultMap, + ERROR_DETAIL_JSON_DEFAULT_PREFIX, + ERROR_DETAIL_JSON_DEFAULT_INDENT, + ) if err != nil { return fmt.Sprintf("formatting error: %s", err.Error()) } @@ -140,7 +151,7 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.V func FormatSchemaErrors(schemaErrors []gojsonschema.ResultError, flags utils.ValidateCommandFlags, format string) (formattedSchemaErrors string) { - getLogger().Infof("Formatting error results (`%s` format)...\n", format) + getLogger().Infof("Formatting error results (`%s` format)...", format) switch format { case FORMAT_JSON: formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors, utils.GlobalFlags.ValidateFlags) @@ -208,14 +219,13 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat lenErrs := len(errs) if lenErrs > 0 { - sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):\n", lenErrs)) + getLogger().Infof("(%d) schema errors detected.", lenErrs) errLimit := flags.MaxNumErrors // If we have more errors than the (default or user set) limit; notify user if lenErrs > errLimit { // notify users more errors exist - msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) - getLogger().Infof("%s", msg) + getLogger().Infof("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) } if lenErrs > 1 { @@ -241,7 +251,7 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat } if lenErrs > 1 { - sb.WriteString("\n]") + sb.WriteString("\n]\n") } } @@ -282,34 +292,36 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat description = description + " ... (truncated)" } - // TODO: provide flag to allow users to "turn on", by default we do NOT want this - // as this slows down processing on SBOMs with large numbers of errors - if colorize { - formattedValue, _ = log.FormatInterfaceAsColorizedJson(resultError.Value()) - } else { - formattedValue, _ = log.FormatInterfaceAsJson(resultError.Value()) - } - // Indent error detail output in logs - formattedValue = log.AddTabs(formattedValue) - // NOTE: if we do not colorize or indent we could simply do this: - failingObject = fmt.Sprintf("\n\tFailing object: [%v]", formattedValue) - - // truncate output unless debug flag is used - if !utils.GlobalFlags.PersistentFlags.Debug && - len(failingObject) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { - failingObject = failingObject[:DEFAULT_MAX_ERR_DESCRIPTION_LEN] - failingObject = failingObject + " ... (truncated)" - } - // append the numbered schema error - schemaErrorText := fmt.Sprintf("\n\t%d. Type: [%s], Field: [%s], Description: [%s] %s", + schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": [%s], \"%s\": [%s], \"%s\": [%s], \"%s\": [%s]", i+1, - resultError.Type(), - resultError.Field(), - description, - failingObject) + ERROR_DETAIL_KEY_DATA_TYPE, resultError.Type(), + ERROR_DETAIL_KEY_FIELD, resultError.Field(), + ERROR_DETAIL_KEY_CONTEXT, resultError.Context().String(ERROR_DETAIL_JSON_CONTEXT_DELIMITER), + ERROR_DETAIL_KEY_VALUE_DESCRIPTION, description) sb.WriteString(schemaErrorText) + + if flags.ShowErrorValue { + + // TODO: provide flag to allow users to "turn on", by default we do NOT want this + // as this slows down processing on SBOMs with large numbers of errors + if colorize { + formattedValue, _ = log.FormatIndentedInterfaceAsColorizedJson( + resultError.Value(), + len(ERROR_DETAIL_JSON_DEFAULT_INDENT), + ) + } else { + // formattedValue, _ = log.FormatInterfaceAsJson(resultError.Value()) + formattedValue, _ = log.FormatIndentedInterfaceAsJson( + resultError.Value(), + ERROR_DETAIL_JSON_DEFAULT_PREFIX, + ERROR_DETAIL_JSON_DEFAULT_INDENT, + ) + } + failingObject = fmt.Sprintf("\n\t\t\"value\": %v", formattedValue) + sb.WriteString(failingObject) + } } } return sb.String() diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 2946ddaf..b60cb0fe 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -23,7 +23,6 @@ import ( "encoding/json" "fmt" "io/fs" - "os" "testing" "github.com/CycloneDX/sbom-utility/schema" @@ -67,7 +66,12 @@ func innerValidateError(t *testing.T, filename string, variant string, format st // Invoke the actual validate function var isValid bool - isValid, document, schemaErrors, actualError = Validate(os.Stdout, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) + // TODO: support additional tests on output buffer (e.g., format==valid JSON) + isValid, document, schemaErrors, _, actualError = innerValidateErrorBuffered( + t, + utils.GlobalFlags.PersistentFlags, + utils.GlobalFlags.ValidateFlags, + ) getLogger().Tracef("document: `%s`, isValid=`%t`, actualError=`%T`", document.GetFilename(), isValid, actualError) @@ -99,15 +103,15 @@ func innerValidateError(t *testing.T, filename string, variant string, format st return } -func innerValidateErrorBuffered(t *testing.T, filename string, variant string, format string, expectedError error) (schemaErrors []gojsonschema.ResultError, outputBuffer bytes.Buffer, err error) { +func innerValidateErrorBuffered(t *testing.T, persistentFlags utils.PersistentCommandFlags, validationFlags utils.ValidateCommandFlags) (isValid bool, document *schema.Sbom, schemaErrors []gojsonschema.ResultError, outputBuffer bytes.Buffer, err error) { // Declare an output outputBuffer/outputWriter to use used during tests var outputWriter = bufio.NewWriter(&outputBuffer) // ensure all data is written to buffer before further validation defer outputWriter.Flush() // Invoke the actual command (API) - isValid, document, schemaErrors, actualError := Validate(outputWriter, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.ValidateFlags) - getLogger().Tracef("document: `%s`, isValid=`%t`, actualError=`%T`", document.GetFilename(), isValid, actualError) + isValid, document, schemaErrors, err = Validate(outputWriter, persistentFlags, utils.GlobalFlags.ValidateFlags) + getLogger().Tracef("document: `%s`, isValid=`%t`, err=`%T`", document.GetFilename(), isValid, err) return } @@ -284,16 +288,8 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { nil) } -// func TestValidateSyntaxErrorCdx14AdHoc2(t *testing.T) { -// innerValidateError(t, -// "sample_co_May16.json", -// SCHEMA_VARIANT_NONE, -// nil) -// } - func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { - //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateErrorBuffered(t, + innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, SCHEMA_VARIANT_NONE, FORMAT_JSON, @@ -301,8 +297,7 @@ func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { } func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { - //utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateErrorBuffered(t, + innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, SCHEMA_VARIANT_NONE, FORMAT_JSON, diff --git a/log/format.go b/log/format.go index 834fa22d..bca3f433 100644 --- a/log/format.go +++ b/log/format.go @@ -161,6 +161,16 @@ func FormatInterfaceAsColorizedJson(data interface{}) (string, error) { return string(bytes), nil } +func FormatIndentedInterfaceAsColorizedJson(data interface{}, indent int) (string, error) { + formatter := prettyjson.NewFormatter() + formatter.Indent = indent + bytes, err := formatter.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + // TODO: make indent length configurable func FormatIndentedInterfaceAsJson(data interface{}, prefix string, indent string) (string, error) { bytes, err := json.MarshalIndent(data, prefix, indent) From b02cebc2562a888c44839b768ac9e06ca4477d76 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 23 Jun 2023 09:38:31 -0500 Subject: [PATCH 23/28] Update the text format logic to mirror new json formatting Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 6b9b9e85..b68283df 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -293,7 +293,7 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat } // append the numbered schema error - schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": [%s], \"%s\": [%s], \"%s\": [%s], \"%s\": [%s]", + schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": \"%s\", \"%s\": [%s], \"%s\": [%s], \"%s\": [%s]", i+1, ERROR_DETAIL_KEY_DATA_TYPE, resultError.Type(), ERROR_DETAIL_KEY_FIELD, resultError.Field(), From bf722687ca9c966b18456cd65008593d1240d79d Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 23 Jun 2023 10:30:15 -0500 Subject: [PATCH 24/28] Update the text format logic to mirror new json formatting Signed-off-by: Matt Rutkowski --- cmd/validate.go | 2 +- cmd/validate_format.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index c4274578..96f5b3c5 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -165,7 +165,7 @@ func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) // Note: InvalidSBOMError type errors include schema errors which have already // been added to the error type and will shown with the Error() interface if valid { - getLogger().Errorf("invalid state: error (%T) returned, but SBOM valid!", t) + _ = getLogger().Errorf("invalid state: error (%T) returned, but SBOM valid!", t) } getLogger().Error(err) default: diff --git a/cmd/validate_format.go b/cmd/validate_format.go index b68283df..0f6ae375 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -293,7 +293,7 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat } // append the numbered schema error - schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": \"%s\", \"%s\": [%s], \"%s\": [%s], \"%s\": [%s]", + schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": \"%s\",\n\t\t\"%s\": [%s],\n\t\t\"%s\": [%s],\n\t\t\"%s\": [%s]", i+1, ERROR_DETAIL_KEY_DATA_TYPE, resultError.Type(), ERROR_DETAIL_KEY_FIELD, resultError.Field(), From ac503d51b8dad13cf6fea09b18b72659e14ca859 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Fri, 23 Jun 2023 12:12:17 -0500 Subject: [PATCH 25/28] Streamline json and text formatting paths Signed-off-by: Matt Rutkowski --- cmd/validate.go | 15 ++-- cmd/validate_format.go | 182 ++++++++++++++++++----------------------- log/format.go | 3 +- 3 files changed, 90 insertions(+), 110 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 96f5b3c5..3acfcb5e 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -116,6 +116,13 @@ func validateCmdImpl(cmd *cobra.Command, args []string) error { outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile outputFile, writer, err := createOutputFile(outputFilename) + // Note: all invalid SBOMs (that fail schema validation) MUST result in an InvalidSBOMError() + if err != nil { + // TODO: assure this gets normalized + getLogger().Error(err) + os.Exit(ERROR_APPLICATION) + } + // use function closure to assure consistent error output based upon error type defer func() { // always close the output file @@ -141,6 +148,8 @@ func validateCmdImpl(cmd *cobra.Command, args []string) error { // TODO: remove this if we can assure that we ALWAYS return an // IsInvalidSBOMError(err) in these cases from the Validate() method if !isValid { + // TODO: if JSON validation resulted in !valid, turn that into an + // InvalidSBOMError and test to make sure this works in all cases os.Exit(ERROR_VALIDATION) } @@ -151,9 +160,6 @@ func validateCmdImpl(cmd *cobra.Command, args []string) error { // Normalize error/normalizeValidationErrorTypes from the Validate() function func normalizeValidationErrorTypes(document *schema.Sbom, valid bool, err error) { - // TODO: if JSON validation resulted in !valid, turn that into an - // InvalidSBOMError and test to make sure this works in all cases - // Consistently display errors before exiting if err != nil { switch t := err.(type) { @@ -315,8 +321,7 @@ func Validate(output io.Writer, persistentFlags utils.PersistentCommandFlags, va return INVALID, document, schemaErrors, errInvalid } - // TODO: Need to perhaps factor in these errors into the JSON output as if they - // were actual schema errors... + // TODO: Perhaps factor in these errors into the JSON output as if they were actual schema errors... // Perform additional validation in document composition/structure // and "custom" required data within specified fields if validateFlags.CustomValidation { diff --git a/cmd/validate_format.go b/cmd/validate_format.go index 0f6ae375..ec6e18ac 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -19,7 +19,7 @@ package cmd // "github.com/iancoleman/orderedmap" import ( - "fmt" + "strconv" "strings" "github.com/CycloneDX/sbom-utility/log" @@ -47,6 +47,22 @@ const ( ERROR_DETAIL_JSON_CONTEXT_DELIMITER = "." ) +// JSON formatting +const ( + JSON_ARRAY_START = "[\n" + JSON_ARRAY_ITEM_SEP = ",\n" + JSON_ARRAY_END = "\n]\n" +) + +// Recurring / translatable messages +const ( + MSG_INFO_FORMATTING_ERROR_RESULTS = "Formatting error results (`%s` format)..." + MSG_INFO_SCHEMA_ERRORS_DETECTED = "(%d) schema errors detected." + MSG_INFO_TOO_MANY_ERRORS = "Too many errors. Showing (%v/%v) errors." + MSG_ERROR_FORMATTING_ERROR = "formatting error: %s" + MSG_WARN_INVALID_FORMAT = "invalid format. error results not supported for `%s` format; defaulting to `%s` format..." +) + type ValidationResultFormatter struct { Results []ValidationResultFormat } @@ -80,37 +96,20 @@ func (validationErrResult *ValidationResultFormat) MarshalJSON() (marshalled []b return validationErrResult.resultMap.MarshalJSON() } -func (result *ValidationResultFormat) Format(flags utils.ValidateCommandFlags) string { - - var sb strings.Builder - - // Conditionally, add optional values as requested +func (result *ValidationResultFormat) Format(flags utils.ValidateCommandFlags) { + // Conditionally, add optional values as requested (via flags) if flags.ShowErrorValue { result.resultMap.Set(ERROR_DETAIL_KEY_VALUE, result.ResultError.Value()) } - - formattedResult, err := log.FormatIndentedInterfaceAsJson( - result.resultMap, - ERROR_DETAIL_JSON_DEFAULT_PREFIX, - ERROR_DETAIL_JSON_DEFAULT_INDENT, - ) - if err != nil { - return fmt.Sprintf("formatting error: %s", err.Error()) - } - sb.WriteString(formattedResult) - - return sb.String() } -func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.ValidateCommandFlags) string { +func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.ValidateCommandFlags) { - var sb strings.Builder - - // Conditionally, add optional values as requested // For this error type, we want to reduce the information show to the end user. // Originally, the entire array with duplicate items was show for EVERY occurrence; // attempt to only show the failing item itself once (and only once) // TODO: deduplication (planned) will also help shrink large error output results + // Conditionally, add optional values as requested (via flags) if flags.ShowErrorValue { details := result.ResultError.Details() valueType, typeFound := details[ERROR_DETAIL_KEY_DATA_TYPE] @@ -134,38 +133,26 @@ func (result *ValidationResultFormat) FormatItemsMustBeUniqueError(flags utils.V } } } - - // format information on the failing "value" (details) with proper JSON indenting - formattedResult, err := log.FormatIndentedInterfaceAsJson( - result.resultMap, - ERROR_DETAIL_JSON_DEFAULT_PREFIX, - ERROR_DETAIL_JSON_DEFAULT_INDENT, - ) - if err != nil { - return fmt.Sprintf("formatting error: %s", err.Error()) - } - sb.WriteString(formattedResult) - - return sb.String() } func FormatSchemaErrors(schemaErrors []gojsonschema.ResultError, flags utils.ValidateCommandFlags, format string) (formattedSchemaErrors string) { - getLogger().Infof("Formatting error results (`%s` format)...", format) + getLogger().Infof(MSG_INFO_FORMATTING_ERROR_RESULTS, format) switch format { case FORMAT_JSON: formattedSchemaErrors = FormatSchemaErrorsJson(schemaErrors, utils.GlobalFlags.ValidateFlags) case FORMAT_TEXT: formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) default: - getLogger().Warningf("error results not supported for `%s` format; defaulting to `%s` format...", - format, FORMAT_TEXT) + getLogger().Warningf(MSG_WARN_INVALID_FORMAT, format, FORMAT_TEXT) formattedSchemaErrors = FormatSchemaErrorsText(schemaErrors, utils.GlobalFlags.ValidateFlags) } return } // Custom formatting based upon possible JSON schema error types +// the custom formatting handlers SHOULD adjust the fields/keys and their values within the `resultMap` +// for the respective errorResult being operated on. func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.ValidateCommandFlags) (formattedResult string) { validationErrorResult := NewValidationErrResult(resultError) @@ -192,7 +179,7 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va // case *gojsonschema.InvalidPropertyPatternError: // case *gojsonschema.InvalidTypeError: case *gojsonschema.ItemsMustBeUniqueError: - formattedResult = validationErrorResult.FormatItemsMustBeUniqueError(flags) + validationErrorResult.FormatItemsMustBeUniqueError(flags) // case *gojsonschema.MissingDependencyError: // case *gojsonschema.MultipleOfError: // case *gojsonschema.NumberAllOfError: @@ -208,10 +195,38 @@ func formatSchemaErrorTypes(resultError gojsonschema.ResultError, flags utils.Va // case *gojsonschema.StringLengthLTEError: default: getLogger().Debugf("default formatting: ResultError Type: [%v]", errorType) - formattedResult = validationErrorResult.Format(flags) + validationErrorResult.Format(flags) } - return + return validationErrorResult.formatResultMap(flags) +} + +func (result *ValidationResultFormat) formatResultMap(flags utils.ValidateCommandFlags) string { + // format information on the failing "value" (details) with proper JSON indenting + var formattedResult string + var errFormatting error + if flags.ColorizeErrorOutput { + formattedResult, errFormatting = log.FormatIndentedInterfaceAsColorizedJson( + result.resultMap, + len(ERROR_DETAIL_JSON_DEFAULT_INDENT), + "\n", + ) + } else { + formattedResult, errFormatting = log.FormatIndentedInterfaceAsJson( + result.resultMap, + ERROR_DETAIL_JSON_DEFAULT_PREFIX, + ERROR_DETAIL_JSON_DEFAULT_INDENT, + ) + + // NOTE: we must add the prefix (indent) ourselves + // see issue: https://github.com/golang/go/issues/49261 + formattedResult = ERROR_DETAIL_JSON_DEFAULT_PREFIX + formattedResult + } + if errFormatting != nil { + return getLogger().Errorf(MSG_ERROR_FORMATTING_ERROR, errFormatting.Error()).Error() + } + + return formattedResult } func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) string { @@ -219,40 +234,35 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat lenErrs := len(errs) if lenErrs > 0 { - getLogger().Infof("(%d) schema errors detected.", lenErrs) + getLogger().Infof(MSG_INFO_SCHEMA_ERRORS_DETECTED, lenErrs) errLimit := flags.MaxNumErrors // If we have more errors than the (default or user set) limit; notify user if lenErrs > errLimit { // notify users more errors exist - getLogger().Infof("Too many errors. Showing (%v/%v) errors.", errLimit, len(errs)) + getLogger().Infof(MSG_INFO_TOO_MANY_ERRORS, errLimit, len(errs)) } - if lenErrs > 1 { - sb.WriteString("[\n") - } + // begin/open JSON array + sb.WriteString(JSON_ARRAY_START) for i, resultError := range errs { // short-circuit if too many errors (i.e., using the error limit flag value) - if i > errLimit { + if i == errLimit { break } // add to the result errors schemaErrorText := formatSchemaErrorTypes(resultError, flags) - // NOTE: we must add the prefix (indent) ourselves - // see issue: https://github.com/golang/go/issues/49261 - sb.WriteString(ERROR_DETAIL_JSON_DEFAULT_PREFIX) sb.WriteString(schemaErrorText) - if i < (lenErrs-1) && i < (errLimit) { - sb.WriteString(",\n") + if i < (lenErrs-1) && i < (errLimit-1) { + sb.WriteString(JSON_ARRAY_ITEM_SEP) } } - if lenErrs > 1 { - sb.WriteString("\n]\n") - } + // end/close JSON array + sb.WriteString(JSON_ARRAY_END) } return sb.String() @@ -263,65 +273,29 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat lenErrs := len(errs) if lenErrs > 0 { + getLogger().Infof(MSG_INFO_SCHEMA_ERRORS_DETECTED, lenErrs) errLimit := utils.GlobalFlags.ValidateFlags.MaxNumErrors - colorize := utils.GlobalFlags.ValidateFlags.ColorizeErrorOutput - var formattedValue string - var description string - var failingObject string + var errorIndex string + + // If we have more errors than the (default or user set) limit; notify user + if lenErrs > errLimit { + // notify users more errors exist + getLogger().Infof(MSG_INFO_TOO_MANY_ERRORS, errLimit, len(errs)) + } - sb.WriteString(fmt.Sprintf("\n(%d) Schema errors detected (use `--debug` for more details):", lenErrs)) for i, resultError := range errs { - // short-circuit if we have too many errors + // short-circuit if too many errors (i.e., using the error limit flag value) if i == errLimit { - // notify users more errors exist - msg := fmt.Sprintf("Too many errors. Showing (%v/%v) errors.", i, len(errs)) - getLogger().Infof("%s", msg) - // always include limit message in discrete output (i.e., not turned off by --quiet flag) - sb.WriteString("\n" + msg) break } - // Some descriptions include very long enums; in those cases, - // truncate to a reasonable length using an intelligent separator - description = resultError.Description() - // truncate output unless debug flag is used - if !utils.GlobalFlags.PersistentFlags.Debug && - len(description) > DEFAULT_MAX_ERR_DESCRIPTION_LEN { - description, _, _ = strings.Cut(description, ":") - description = description + " ... (truncated)" - } - // append the numbered schema error - schemaErrorText := fmt.Sprintf("\n\t%d. \"%s\": \"%s\",\n\t\t\"%s\": [%s],\n\t\t\"%s\": [%s],\n\t\t\"%s\": [%s]", - i+1, - ERROR_DETAIL_KEY_DATA_TYPE, resultError.Type(), - ERROR_DETAIL_KEY_FIELD, resultError.Field(), - ERROR_DETAIL_KEY_CONTEXT, resultError.Context().String(ERROR_DETAIL_JSON_CONTEXT_DELIMITER), - ERROR_DETAIL_KEY_VALUE_DESCRIPTION, description) - - sb.WriteString(schemaErrorText) + errorIndex = strconv.Itoa(i + 1) - if flags.ShowErrorValue { - - // TODO: provide flag to allow users to "turn on", by default we do NOT want this - // as this slows down processing on SBOMs with large numbers of errors - if colorize { - formattedValue, _ = log.FormatIndentedInterfaceAsColorizedJson( - resultError.Value(), - len(ERROR_DETAIL_JSON_DEFAULT_INDENT), - ) - } else { - // formattedValue, _ = log.FormatInterfaceAsJson(resultError.Value()) - formattedValue, _ = log.FormatIndentedInterfaceAsJson( - resultError.Value(), - ERROR_DETAIL_JSON_DEFAULT_PREFIX, - ERROR_DETAIL_JSON_DEFAULT_INDENT, - ) - } - failingObject = fmt.Sprintf("\n\t\t\"value\": %v", formattedValue) - sb.WriteString(failingObject) - } + // emit formatted error result + formattedResult := formatSchemaErrorTypes(resultError, utils.GlobalFlags.ValidateFlags) + sb.WriteString("\n" + errorIndex + ". " + formattedResult) } } return sb.String() diff --git a/log/format.go b/log/format.go index bca3f433..0938dcbd 100644 --- a/log/format.go +++ b/log/format.go @@ -161,9 +161,10 @@ func FormatInterfaceAsColorizedJson(data interface{}) (string, error) { return string(bytes), nil } -func FormatIndentedInterfaceAsColorizedJson(data interface{}, indent int) (string, error) { +func FormatIndentedInterfaceAsColorizedJson(data interface{}, indent int, newline string) (string, error) { formatter := prettyjson.NewFormatter() formatter.Indent = indent + formatter.Newline = newline bytes, err := formatter.Marshal(data) if err != nil { return "", err From d646e3e83874c5b3064e6ebc910dbc5fd4a2d9f8 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 26 Jun 2023 10:55:51 -0500 Subject: [PATCH 26/28] Adjust colorized indent to match normal indent Signed-off-by: Matt Rutkowski --- cmd/validate_format.go | 18 +++++++++++------- cmd/validate_test.go | 20 ++++++++++++++++++++ log/format.go | 3 ++- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/cmd/validate_format.go b/cmd/validate_format.go index ec6e18ac..781262b6 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -19,6 +19,7 @@ package cmd // "github.com/iancoleman/orderedmap" import ( + "fmt" "strconv" "strings" @@ -45,6 +46,7 @@ const ( ERROR_DETAIL_JSON_DEFAULT_PREFIX = " " ERROR_DETAIL_JSON_DEFAULT_INDENT = " " ERROR_DETAIL_JSON_CONTEXT_DELIMITER = "." + ERROR_DETAIL_JSON_NEWLINE_INDENT = "\n" + ERROR_DETAIL_JSON_DEFAULT_PREFIX ) // JSON formatting @@ -209,7 +211,7 @@ func (result *ValidationResultFormat) formatResultMap(flags utils.ValidateComman formattedResult, errFormatting = log.FormatIndentedInterfaceAsColorizedJson( result.resultMap, len(ERROR_DETAIL_JSON_DEFAULT_INDENT), - "\n", + ERROR_DETAIL_JSON_NEWLINE_INDENT, ) } else { formattedResult, errFormatting = log.FormatIndentedInterfaceAsJson( @@ -217,10 +219,6 @@ func (result *ValidationResultFormat) formatResultMap(flags utils.ValidateComman ERROR_DETAIL_JSON_DEFAULT_PREFIX, ERROR_DETAIL_JSON_DEFAULT_INDENT, ) - - // NOTE: we must add the prefix (indent) ourselves - // see issue: https://github.com/golang/go/issues/49261 - formattedResult = ERROR_DETAIL_JSON_DEFAULT_PREFIX + formattedResult } if errFormatting != nil { return getLogger().Errorf(MSG_ERROR_FORMATTING_ERROR, errFormatting.Error()).Error() @@ -254,6 +252,9 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat // add to the result errors schemaErrorText := formatSchemaErrorTypes(resultError, flags) + // NOTE: we must add the prefix (indent) ourselves + // see issue: https://github.com/golang/go/issues/49261 + sb.WriteString(ERROR_DETAIL_JSON_DEFAULT_PREFIX) sb.WriteString(schemaErrorText) if i < (lenErrs-1) && i < (errLimit-1) { @@ -270,7 +271,7 @@ func FormatSchemaErrorsJson(errs []gojsonschema.ResultError, flags utils.Validat func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) string { var sb strings.Builder - + var lineOutput string lenErrs := len(errs) if lenErrs > 0 { getLogger().Infof(MSG_INFO_SCHEMA_ERRORS_DETECTED, lenErrs) @@ -295,7 +296,10 @@ func FormatSchemaErrorsText(errs []gojsonschema.ResultError, flags utils.Validat // emit formatted error result formattedResult := formatSchemaErrorTypes(resultError, utils.GlobalFlags.ValidateFlags) - sb.WriteString("\n" + errorIndex + ". " + formattedResult) + // NOTE: we must add the prefix (indent) ourselves + // see issue: https://github.com/golang/go/issues/49261 + lineOutput = fmt.Sprintf("\n%v. %s", errorIndex, formattedResult) + sb.WriteString(lineOutput) } } return sb.String() diff --git a/cmd/validate_test.go b/cmd/validate_test.go index b60cb0fe..9021695e 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -288,6 +288,25 @@ func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { nil) } +// TODO: add additional checks on the buffered output +func TestValidateCdx14ErrorResultsUniqueComponentsText(t *testing.T) { + innerValidateError(t, + TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, + SCHEMA_VARIANT_NONE, + FORMAT_TEXT, + &InvalidSBOMError{}) +} + +// TODO: add additional checks on the buffered output +func TestValidateCdx14ErrorResultsFormatIriReferencesText(t *testing.T) { + innerValidateError(t, + TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, + SCHEMA_VARIANT_NONE, + FORMAT_TEXT, + &InvalidSBOMError{}) +} + +// TODO: add additional checks on the buffered output func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, @@ -296,6 +315,7 @@ func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { &InvalidSBOMError{}) } +// TODO: add additional checks on the buffered output func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, diff --git a/log/format.go b/log/format.go index 0938dcbd..446aea13 100644 --- a/log/format.go +++ b/log/format.go @@ -172,7 +172,6 @@ func FormatIndentedInterfaceAsColorizedJson(data interface{}, indent int, newlin return string(bytes), nil } -// TODO: make indent length configurable func FormatIndentedInterfaceAsJson(data interface{}, prefix string, indent string) (string, error) { bytes, err := json.MarshalIndent(data, prefix, indent) if err != nil { @@ -181,6 +180,8 @@ func FormatIndentedInterfaceAsJson(data interface{}, prefix string, indent strin return string(bytes), nil } +// NOTE: hardcodes indent length +// TODO: make configurable as a formatter field/value func FormatInterfaceAsJson(data interface{}) (string, error) { bytes, err := json.MarshalIndent(data, "", " ") if err != nil { From 62deaed253226ad9b76bf061c1c576eca9b114d0 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 26 Jun 2023 13:40:19 -0500 Subject: [PATCH 27/28] Add additional test assertions to validate # errs and error conext Signed-off-by: Matt Rutkowski --- README.md | 4 ++-- cmd/validate_test.go | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e8512795..96b868eb 100644 --- a/README.md +++ b/README.md @@ -808,11 +808,11 @@ Use the `--error-limit x` (default: `10`) flag to reduce the formatted error res ##### `--error-value` flag -Use the `--error-value=true|false` (default: `true`)flag to reduce the formatted error result output by not showing the `value` field which shows detailed information about the failing data in the BOM. +Use the `--error-value=true|false` (default: `true`) flag to reduce the formatted error result output by not showing the `value` field which shows detailed information about the failing data in the BOM. ##### `--colorize` flag -Use the `--colorize=true|false` (default: `true`) flag to add/remove color formatting to error result `txt` formatted output. By default, `txt` formatted error output is colorized to help with human readability; for automated use, it can be turned off. +Use the `--colorize=true|false` (default: `false`) flag to add/remove color formatting to error result `txt` formatted output. By default, `txt` formatted error output is colorized to help with human readability; for automated use, it can be turned off. #### Validate Examples diff --git a/cmd/validate_test.go b/cmd/validate_test.go index 9021695e..fc2ac712 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -308,18 +308,38 @@ func TestValidateCdx14ErrorResultsFormatIriReferencesText(t *testing.T) { // TODO: add additional checks on the buffered output func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { - innerValidateError(t, + var EXPECTED_ERROR_NUM = 2 + var EXPECTED_ERROR_CONTEXT = "(root).components" + _, schemaErrors, _ := innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_COMPONENTS_UNIQUE, SCHEMA_VARIANT_NONE, FORMAT_JSON, &InvalidSBOMError{}) + + if len(schemaErrors) != EXPECTED_ERROR_NUM { + t.Errorf("invalid error count: expected `%v` schema errors; actual errors: `%v`)", EXPECTED_ERROR_NUM, len(schemaErrors)) + } + + if schemaErrors[0].Context().String() != EXPECTED_ERROR_CONTEXT { + t.Errorf("invalid schema error context: expected `%v`; actual: `%v`)", EXPECTED_ERROR_CONTEXT, schemaErrors[0].Context().String()) + } } // TODO: add additional checks on the buffered output func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { - innerValidateError(t, + var EXPECTED_ERROR_NUM = 1 + var EXPECTED_ERROR_CONTEXT = "(root).components.2.externalReferences.0.url" + _, schemaErrors, _ := innerValidateError(t, TEST_CDX_1_4_VALIDATE_ERR_FORMAT_IRI_REFERENCE, SCHEMA_VARIANT_NONE, FORMAT_JSON, &InvalidSBOMError{}) + + if len(schemaErrors) != EXPECTED_ERROR_NUM { + t.Errorf("invalid schema error count: expected `%v`; actual: `%v`)", EXPECTED_ERROR_NUM, len(schemaErrors)) + } + + if schemaErrors[0].Context().String() != EXPECTED_ERROR_CONTEXT { + t.Errorf("invalid schema error context: expected `%v`; actual: `%v`)", EXPECTED_ERROR_CONTEXT, schemaErrors[0].Context().String()) + } } From 64af463a974ec48d8648040bc2edfde4413c50d8 Mon Sep 17 00:00:00 2001 From: Matt Rutkowski Date: Mon, 26 Jun 2023 14:28:38 -0500 Subject: [PATCH 28/28] Assure forced schema file tests reset to default schema Signed-off-by: Matt Rutkowski --- cmd/validate.go | 1 + cmd/validate_test.go | 34 ++++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cmd/validate.go b/cmd/validate.go index 3acfcb5e..dce748ed 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -309,6 +309,7 @@ func Validate(output io.Writer, persistentFlags utils.PersistentCommandFlags, va case FORMAT_JSON: // Note: JSON data files MUST ends in a newline s as this is a POSIX standard formattedErrors = FormatSchemaErrors(schemaErrors, validateFlags, FORMAT_JSON) + // getLogger().Debugf("%s", formattedErrors) fmt.Fprintf(output, "%s", formattedErrors) case FORMAT_TEXT: fallthrough diff --git a/cmd/validate_test.go b/cmd/validate_test.go index fc2ac712..b981cf16 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -116,6 +116,18 @@ func innerValidateErrorBuffered(t *testing.T, persistentFlags utils.PersistentCo return } +func innerValidateForcedSchema(t *testing.T, filename string, forcedSchema string, format string, expectedError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { + getLogger().Enter() + defer getLogger().Exit() + + utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = forcedSchema + innerValidateError(t, filename, SCHEMA_VARIANT_NONE, format, expectedError) + // !!!Important!!! Must reset this global flag + utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = "" + + return +} + // Tests *ErrorInvalidSBOM error types and any (lower-level) errors they "wrapped" func innerValidateInvalidSBOMInnerError(t *testing.T, filename string, variant string, innerError error) (document *schema.Sbom, schemaErrors []gojsonschema.ResultError, actualError error) { getLogger().Enter() @@ -260,30 +272,27 @@ func TestValidateSyntaxErrorCdx13Test2(t *testing.T) { // Force validation against a "custom" schema with compatible format (CDX) and version (1.3) func TestValidateForceCustomSchemaCdx13(t *testing.T) { - utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateError(t, + innerValidateForcedSchema(t, TEST_CDX_1_3_MATURITY_EXAMPLE_1_BASE, - SCHEMA_VARIANT_NONE, + TEST_SCHEMA_CDX_1_3_CUSTOM, FORMAT_TEXT, nil) } // Force validation against a "custom" schema with compatible format (CDX) and version (1.4) func TestValidateForceCustomSchemaCdx14(t *testing.T) { - utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_4_CUSTOM - innerValidateError(t, + innerValidateForcedSchema(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, - SCHEMA_VARIANT_NONE, + TEST_SCHEMA_CDX_1_4_CUSTOM, FORMAT_TEXT, nil) } // Force validation using schema with compatible format, but older version than the SBOM version func TestValidateForceCustomSchemaCdxSchemaOlder(t *testing.T) { - utils.GlobalFlags.ValidateFlags.ForcedJsonSchemaFile = TEST_SCHEMA_CDX_1_3_CUSTOM - innerValidateError(t, + innerValidateForcedSchema(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, - SCHEMA_VARIANT_NONE, + TEST_SCHEMA_CDX_1_3_CUSTOM, FORMAT_TEXT, nil) } @@ -315,9 +324,11 @@ func TestValidateCdx14ErrorResultsUniqueComponentsJson(t *testing.T) { SCHEMA_VARIANT_NONE, FORMAT_JSON, &InvalidSBOMError{}) + //output, _ := log.FormatIndentedInterfaceAsJson(schemaErrors, " ", " ") if len(schemaErrors) != EXPECTED_ERROR_NUM { - t.Errorf("invalid error count: expected `%v` schema errors; actual errors: `%v`)", EXPECTED_ERROR_NUM, len(schemaErrors)) + t.Errorf("invalid schema error count: expected `%v`; actual: `%v`)", EXPECTED_ERROR_NUM, len(schemaErrors)) + //fmt.Printf("schemaErrors:\n %s", output) } if schemaErrors[0].Context().String() != EXPECTED_ERROR_CONTEXT { @@ -335,8 +346,11 @@ func TestValidateCdx14ErrorResultsFormatIriReferencesJson(t *testing.T) { FORMAT_JSON, &InvalidSBOMError{}) + //output, _ := log.FormatIndentedInterfaceAsJson(schemaErrors, " ", " ") + if len(schemaErrors) != EXPECTED_ERROR_NUM { t.Errorf("invalid schema error count: expected `%v`; actual: `%v`)", EXPECTED_ERROR_NUM, len(schemaErrors)) + //fmt.Printf("schemaErrors:\n %s", output) } if schemaErrors[0].Context().String() != EXPECTED_ERROR_CONTEXT {