Skip to content

Commit

Permalink
fix(validation)!: #266 validation.ValidationCommand no longer returns…
Browse files Browse the repository at this point in the history
… an error when the validation has been successfully run

fix(validator)!: validator.Validate() no longer returns the parsed basic output as an error and instead returns the jsonschema.ValidationError
fix(cmd): validate cmd updated to handle ValidationCommand & validator.Validate changes
  • Loading branch information
mike-winberry committed Jun 18, 2024
1 parent 1921e9f commit ea0d4e4
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 45 deletions.
21 changes: 13 additions & 8 deletions src/cmd/validate/validate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validate

import (
"fmt"
"log"

"github.com/defenseunicorns/go-oscal/src/pkg/validation"
Expand All @@ -16,7 +17,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 != "" {
Expand All @@ -25,15 +30,15 @@ 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 {
return fmt.Errorf("invalid OSCAL document: %s", validationResponse.JsonSchemaError)
}

// No errors, log success
Expand Down
15 changes: 10 additions & 5 deletions src/pkg/validation/validationError.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 16 additions & 7 deletions src/pkg/validation/validationResult.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
42 changes: 25 additions & 17 deletions src/pkg/validation/validation_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,27 +41,30 @@ 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()

// Write validation result if it was specified and exists before returning ValidateCommand error
validationResult, _ := validator.GetValidationResult()
validationResponse.Result = validationResult
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
}

// Handle the validation error
if validationError != nil {
return validationResponse, fmt.Errorf("failed to validate %s version %s: %s", validator.GetModelType(), validator.GetSchemaVersion(), err)
// 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())
}

// Set the response result, ignore error since we know validate has been run
validationResponse.Result, _ = validator.GetValidationResult()

return validationResponse, nil
}
6 changes: 3 additions & 3 deletions src/pkg/validation/validation_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 3 additions & 5 deletions src/pkg/validation/validator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package validation

import (
"encoding/json"
"errors"
"strings"

Expand Down Expand Up @@ -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{})
Expand Down

0 comments on commit ea0d4e4

Please sign in to comment.