Skip to content
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
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/
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/specterops/chow

go 1.25.3
go 1.26.2

require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
Expand Down
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