diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index eab69e15..438a10fb 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -1,6 +1,8 @@ package validate import ( + "encoding/json" + "fmt" "log" "github.com/defenseunicorns/go-oscal/src/pkg/validation" @@ -16,7 +18,11 @@ var ValidateCmd = &cobra.Command{ Long: "Validate an OSCAL document against the OSCAL schema version specified in the document.", RunE: func(cmd *cobra.Command, args []string) error { // Run the validation - validationResponse, validationErr := validation.ValidationCommand(inputfile) + validationResponse, err := validation.ValidationCommand(inputfile) + // Return any non-validation errors if they exist + if err != nil { + return err + } // Write validation result if it was specified and exists before returning ValidateCommand error if validationResultFile != "" { @@ -25,15 +31,19 @@ var ValidateCmd = &cobra.Command{ log.Printf("Failed to write validation result to %s: %s\n", validationResultFile, err) } } - // Return the error from the validation if there was one - if validationErr != nil { - return validationErr + + // Log any go-oscal related warnings (ie version warnings) + for _, warning := range validationResponse.Warnings { + log.Print(warning) } - if len(validationResponse.Warnings) > 0 { - for _, warning := range validationResponse.Warnings { - log.Print(warning) + // Return any validation errors + if validationResponse.JsonSchemaError != nil { + jsonResult, err := json.MarshalIndent(validationResponse.Result, "", " ") + if err != nil { + return fmt.Errorf("failed to format validation result as JSON: %v", err) } + return fmt.Errorf("invalid OSCAL document, results: %s", string(jsonResult)) } // No errors, log success diff --git a/src/pkg/validation/validationError.go b/src/pkg/validation/validationError.go index eda29c01..d5342805 100644 --- a/src/pkg/validation/validationError.go +++ b/src/pkg/validation/validationError.go @@ -10,11 +10,16 @@ import ( // Extension of the jsonschema.BasicError struct to include the failed value // if the failed value is a map or slice, it will be omitted type ValidatorError struct { - KeywordLocation string `json:"keywordLocation" yaml:"keywordLocation"` - AbsoluteKeywordLocation string `json:"absoluteKeywordLocation" yaml:"absoluteKeywordLocation"` - InstanceLocation string `json:"instanceLocation" yaml:"instanceLocation"` - Error string `json:"error" yaml:"error"` - FailedValue interface{} `json:"failedValue,omitempty" yaml:"failedValue,omitempty"` + // KeywordLocation is the location of the keyword in the schema for failing value + KeywordLocation string `json:"keywordLocation" yaml:"keywordLocation"` + // AbsoluteKeywordLocation is the absolute location of the keyword in the schema for failing value + AbsoluteKeywordLocation string `json:"absoluteKeywordLocation" yaml:"absoluteKeywordLocation"` + // InstanceLocation is the location of the instance in the document + InstanceLocation string `json:"instanceLocation" yaml:"instanceLocation"` + // Error is the error message + Error string `json:"error" yaml:"error"` + // FailedValue is the value of the key that failed validation + FailedValue interface{} `json:"failedValue,omitempty" yaml:"failedValue,omitempty"` } // Creates a []ValidatorError from a jsonschema.Basic diff --git a/src/pkg/validation/validationResult.go b/src/pkg/validation/validationResult.go index ab5026c6..142346a2 100644 --- a/src/pkg/validation/validationResult.go +++ b/src/pkg/validation/validationResult.go @@ -9,17 +9,25 @@ import ( ) type ValidationResult struct { - Valid bool `json:"valid" yaml:"valid"` - TimeStamp time.Time `json:"timeStamp" yaml:"timeStamp"` - Errors []ValidatorError `json:"errors,omitempty" yaml:"errors,omitempty"` - Metadata ValidationResultMetadata `json:"metadata" yaml:"metadata"` + // Valid is true if the validation result is valid + Valid bool `json:"valid" yaml:"valid"` + // TimeStamp is the time the validation result was created + TimeStamp time.Time `json:"timeStamp" yaml:"timeStamp"` + // Errors is a slice of ValidatorErrors + Errors []ValidatorError `json:"errors,omitempty" yaml:"errors,omitempty"` + // Metadata is the metadata of the validation result + Metadata ValidationResultMetadata `json:"metadata" yaml:"metadata"` } type ValidationResultMetadata struct { - DocumentPath string `json:"documentPath,omitempty" yaml:"documentPath,omitempty"` - DocumentType string `json:"documentType,omitempty" yaml:"documentType,omitempty"` + // DocumentPath is the path to the document + DocumentPath string `json:"documentPath,omitempty" yaml:"documentPath,omitempty"` + // DocumentType is the type of the document + DocumentType string `json:"documentType,omitempty" yaml:"documentType,omitempty"` + // DocumentVersion is the version of the document DocumentVersion string `json:"documentVersion,omitempty" yaml:"documentVersion,omitempty"` - SchemaVersion string `json:"schemaVersion,omitempty" yaml:"schemaVersion,omitempty"` + // SchemaVersion is the version of the schema + SchemaVersion string `json:"schemaVersion,omitempty" yaml:"schemaVersion,omitempty"` } // NewValidationResult creates a new ValidationResult from a Validator and a slice of ValidatorErrors @@ -50,6 +58,7 @@ func WriteValidationResult(validationResult ValidationResult, outputFile string) return files.WriteOutput(validationResultBytes, outputFile) } +// WriteValidationResults writes a slice of ValidationResults to a file func WriteValidationResults(validationResults []ValidationResult, outputFile string) (err error) { resultMap := map[string][]ValidationResult{ "results": validationResults, diff --git a/src/pkg/validation/validation_command.go b/src/pkg/validation/validation_command.go index 5d76cd11..f4987ba2 100644 --- a/src/pkg/validation/validation_command.go +++ b/src/pkg/validation/validation_command.go @@ -7,12 +7,17 @@ import ( "strings" "github.com/defenseunicorns/go-oscal/src/pkg/versioning" + "github.com/santhosh-tekuri/jsonschema/v5" ) type ValidationResponse struct { Validator Validator - Result ValidationResult - Warnings []string + // Parsed validation result + Result ValidationResult + // Non-failing go-oscal warnings (ie: deprecated fields, newer schema versions, etc) + Warnings []string + // Unparsed Failing validation errors from the jsonschema library + JsonSchemaError *jsonschema.ValidationError } // ValidationCommand validates an OSCAL document @@ -36,26 +41,34 @@ func ValidationCommand(inputFile string) (validationResponse ValidationResponse, } validationResponse.Validator = validator - // Get and set version warnings - version := validator.GetSchemaVersion() - err = versioning.VersionWarning(version) - if err != nil { - validationResponse.Warnings = append(validationResponse.Warnings, err.Error()) - } - // Set the document path validator.SetDocumentPath(inputFile) // Run the validation - validationError := validator.Validate() + err = validator.Validate() + if err != nil { + validationError, ok := err.(*jsonschema.ValidationError) + // If the error is not a validation error, return the error + if !ok { + return validationResponse, err + } + // Set the jsonschema error in the validation response + validationResponse.JsonSchemaError = validationError + } - // Write validation result if it was specified and exists before returning ValidateCommand error - validationResult, _ := validator.GetValidationResult() - validationResponse.Result = validationResult + // Get and set version warnings if upgrade available + version := validator.GetSchemaVersion() + err = versioning.VersionWarning(version) + if err != nil { + validationResponse.Warnings = append(validationResponse.Warnings, err.Error()) + } - // Handle the validation error - if validationError != nil { - return validationResponse, fmt.Errorf("failed to validate %s version %s: %s", validator.GetModelType(), validator.GetSchemaVersion(), err) + // Get the validation result + validationResponse.Result, err = validator.GetValidationResult() + // If there is an error, return it, but there shouldn't be an error + // Referenced in [#268](https://github.com/defenseunicorns/go-oscal/issues/268) + if err != nil { + return validationResponse, fmt.Errorf("shouldn't error,check the GetValidationResult method for more information: %s", err) } return validationResponse, nil diff --git a/src/pkg/validation/validation_command_test.go b/src/pkg/validation/validation_command_test.go index e36e86dd..2594727c 100644 --- a/src/pkg/validation/validation_command_test.go +++ b/src/pkg/validation/validation_command_test.go @@ -53,10 +53,10 @@ func TestValidationCommand(t *testing.T) { } }) - t.Run("returns an error and validation result if the input file fails validation", func(t *testing.T) { + t.Run("returns no error and validation result if the input file fails validation", func(t *testing.T) { validationResponse, err := validation.ValidationCommand(gooscaltest.InvalidCatalogPath) - if err == nil { - t.Error("expected an error, got nil") + if err != nil { + t.Errorf("expected no error, got %s", err) } if validationResponse.Result.Valid != false { diff --git a/src/pkg/validation/validator.go b/src/pkg/validation/validator.go index 6e546897..a1d0a0b0 100644 --- a/src/pkg/validation/validator.go +++ b/src/pkg/validation/validator.go @@ -1,7 +1,6 @@ package validation import ( - "encoding/json" "errors" "strings" @@ -131,18 +130,17 @@ func (v *Validator) Validate() error { err = sch.Validate(v.jsonMap) if err != nil { - // If the error is not a validation error, return the error + // If the error is not a `ValidationError`, return the error validationErr, ok := err.(*jsonschema.ValidationError) if !ok { return err } // Extract the specific errors from the schema error - // Return the errors as a string basicErrors := ExtractErrors(v.jsonMap, validationErr.BasicOutput()) + // Set the validation result v.validationResult = NewValidationResult(v, basicErrors) - formattedErrors, _ := json.MarshalIndent(basicErrors, "", " ") - return errors.New(string(formattedErrors)) + return err } v.validationResult = NewValidationResult(v, []ValidatorError{})