Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(validation)!: #266 validation.ValidationCommand no longer returns error if validation was run #267

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/cmd/validate/validate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package validate

import (
"encoding/json"
"fmt"
"log"

"github.com/defenseunicorns/go-oscal/src/pkg/validation"
Expand All @@ -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 != "" {
Expand All @@ -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
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
45 changes: 29 additions & 16 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,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
Expand Down
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