Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Want to add the OpenGraph schema to your JSON document?

```json
{
"$schema": "https://raw.githubusercontent.com/SpecterOps/chow/refs/heads/main/pkg/validator/jsonschema/opengraph.json"
"$schema": "https://raw.githubusercontent.com/SpecterOps/chow/refs/heads/main/pkg/validator/jsonschema/payload-schema.json"
}
```

Most editors will ask you to trust the schema's source. Be sure to add the following URL to your trusted domains

```text
https://raw.githubusercontent.com/SpecterOps/chow/refs/heads/main/pkg/validator/jsonschema/
```
74 changes: 61 additions & 13 deletions pkg/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ func (v *Validator) buildValidationReport() ValidationReport {
}
}

// result() is a helper for returning the current parsed data, validation report, and provided error.
func (v *Validator) result(err error) (ParsedData, ValidationReport, error) {
return v.buildParsedData(), v.buildValidationReport(), err
}

// Error Helper functions -------------------------------------------------------------------------

// reportCriticalError() is a helper function for adding a critical error
Expand Down Expand Up @@ -245,29 +250,57 @@ func (v *Validator) finalFileConfigCheck() error {
func (v *Validator) ParseAndValidate() (ParsedData, ValidationReport, error) {
if err := v.enterObject(); err != nil {
v.reportCriticalError("failed to enter json object", err)
return v.buildParsedData(), v.buildValidationReport(), err
return v.result(err)
}

valLoopErr := v.validationLoop()

if err := v.readToEnd(valLoopErr); err != nil {
return v.result(err)
}

return v.result(v.finalizeParse())
}

// readToEnd() checks for trailing input if validation succeeded, then consumes all remaining bytes from the decoder
// buffer and reader while preserving any existing loop error.
func (v *Validator) readToEnd(loopErr error) error {
errToReturn := loopErr
if errToReturn == nil {
if err := v.expectEOF(); err != nil {
v.reportCriticalError("expected to hit the end of the file", err)
errToReturn = err
}
}

// This multireader ensures that bytes included in the json decoder's buffer. This guarantees that ALL bytes are read from the io.Reader
_, readToEndErr := io.Copy(io.Discard, io.MultiReader(v.decoder.Buffered(), v.reader))
if valLoopErr != nil && readToEndErr != nil {
if readToEndErr != nil {
v.reportCriticalError("failed to read file to end", readToEndErr)
return v.buildParsedData(), v.buildValidationReport(), errors.Join(valLoopErr, readToEndErr)
} else if valLoopErr == nil && readToEndErr != nil {
v.reportCriticalError("failed to read file to end", readToEndErr)
return v.buildParsedData(), v.buildValidationReport(), readToEndErr
} else if valLoopErr != nil {
return v.buildParsedData(), v.buildValidationReport(), valLoopErr
}

if errToReturn != nil && readToEndErr != nil {
return errors.Join(errToReturn, readToEndErr)
}

if readToEndErr != nil {
return readToEndErr
}

return errToReturn
}

// finalizeParse() performs the final post-parse validation checks and collapses validation errors into a single error.
func (v *Validator) finalizeParse() error {
if err := v.finalFileConfigCheck(); err != nil {
return v.buildParsedData(), v.buildValidationReport(), err
} else if len(v.validationErrors) > 0 {
return v.buildParsedData(), v.buildValidationReport(), ErrValidationErrors
} else {
return v.buildParsedData(), v.buildValidationReport(), nil
return err
}

if len(v.validationErrors) > 0 {
return ErrValidationErrors
}

return nil
}

// Validation Loop functions ----------------------------------------------------------------------
Expand Down Expand Up @@ -642,3 +675,18 @@ func (v *Validator) nextToken() (json.Token, error) {

return tok, nil
}

// expectEOF() reads the next JSON token and expects to hit the end of the file. Returns an error otherwise
func (v *Validator) expectEOF() error {
tok, err := v.nextToken()

if err == io.EOF {
return nil
}

if err != nil {
return err
}

return fmt.Errorf("expected EOF, instead got token: %v", tok)
}
11 changes: 11 additions & 0 deletions pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,17 @@ func Test_ParseAndValidate(t *testing.T) {
assert.ElementsMatch(t, report.CriticalErrors, []validator.CriticalError{{Message: "unrecognized top level tag: pants", Error: validator.ErrInvalidFileConfiguration}})
},
},
{
name: "unsuccessful payload, trailing data after object",
payload: `{"graph":{"nodes":[]}}{}`,
expectedParsedData: validator.ParsedData{PayloadType: ingest.DataTypeOpenGraph},
errValidationFunc: func(t *testing.T, report validator.ValidationReport, err error) {
assert.ErrorContains(t, err, "expected EOF, instead got token: {")
require.Len(t, report.CriticalErrors, 1)
assert.Equal(t, "expected to hit the end of the file", report.CriticalErrors[0].Message)
assert.ErrorContains(t, report.CriticalErrors[0].Error, "expected EOF, instead got token: {")
},
},
}

schema, err := validator.LoadIngestSchema()
Expand Down
Loading