From 47bbdccb685b47135aedce3075e3ddfcb9849988 Mon Sep 17 00:00:00 2001
From: Matt Rutkowski <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
Date: Wed, 21 Jun 2023 04:27:28 -0500
Subject: [PATCH 10/28] Format value for unique item error

Signed-off-by: Matt Rutkowski <mrutkows@us.ibm.com>
---
 .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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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
-//    <key.1>, <key.2>, ... // "firstname, lastname, email" || * (default)
+//
+//	<key.1>, <key.2>, ... // "firstname, lastname, email" || * (default)
+//
 // FROM
-//    <key path>            // "product.customers"
+//
+//	<key path>            // "product.customers"
+//
 // WHERE
-//    <key.X> == <value>    // "country='Germany'"
+//
+//	<key.X> == <value>    // "country='Germany'"
+//
 // ORDER BY
-//    <key.N>               // "lastname"
+//
+//	<key.N>               // "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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 .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 <int>“ flag (or similar... `err-value-truncate` <int>) used
+// to truncate formatted "value" (details) to <int> 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <input_file> [--select * | field1[,fieldN]] [--from [key1[.keyN]] [--where key=regex[,...]]"
 	CMD_USAGE_RESOURCE_LIST      = CMD_RESOURCE + " --input-file <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 <input_file> [--variant <variant_name>] [--error-limit <integer>] [--colorize=true|false] [--force schema_file]"
+	CMD_USAGE_VALIDATE           = CMD_VALIDATE + " --input-file <input_file> [--variant <variant_name>] [--format txt|json] [--force schema_file]"
 	CMD_USAGE_VULNERABILITY_LIST = CMD_VULNERABILITY + " " + SUBCOMMAND_VULNERABILITY_LIST + " --input-file <input_file> [--summary] [--where key=regex[,...]] [--format json|txt|csv|md]"
 )
 

From ff1c2780730ee699675de2ea50cfe93f65522a0f Mon Sep 17 00:00:00 2001
From: Matt Rutkowski <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
Date: Thu, 22 Jun 2023 13:32:58 -0500
Subject: [PATCH 21/28] buffer JSON output for unit tests

Signed-off-by: Matt Rutkowski <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
Date: Fri, 23 Jun 2023 12:12:17 -0500
Subject: [PATCH 25/28] Streamline json and text formatting paths

Signed-off-by: Matt Rutkowski <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 <mrutkows@us.ibm.com>
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 <mrutkows@us.ibm.com>
---
 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 {