diff --git a/README.md b/README.md index f63c839..f0949ff 100644 --- a/README.md +++ b/README.md @@ -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/ +``` diff --git a/go.mod b/go.mod index 6430e78..df22e5a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/validator/jsonschema/opengraph.json b/pkg/validator/jsonschema/payload-schema.json similarity index 100% rename from pkg/validator/jsonschema/opengraph.json rename to pkg/validator/jsonschema/payload-schema.json diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index e4eedb9..c98bbd1 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -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 @@ -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 ---------------------------------------------------------------------- @@ -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) +} diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index acaf591..f352373 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -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()