From e519eeaa19793003b36a631efdd25fecd62b5a76 Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Thu, 7 Sep 2023 15:14:18 -0600 Subject: [PATCH] Finished YAML validation pre-commit hook - Streamlined the YAML validation process by consolidating it into a shared function. - Improved error messaging for enhanced clarity. - Established a new function, loadSchema, to facilitate schema loading and unmarshalling from a YAML file. - Enhanced ValidateYAML to accommodate schema path input and extended support for directory-wide YAML file validation. - Revised the validateAllYAML function to furnish comprehensive error messages and enhanced logging. - Elevated error handling for YAML validation by providing informative error messages. - Augmented the inspectAndValidate function to adeptly manage nested SubTTPSteps and validate referenced files. - Implemented custom validation to ascertain the presence of the "name" property when "ttp" is declared in a step. This commit amplifies the effectiveness of YAML file validation, ensuring strict adherence to the schema. Introduces a pre-commit hook mechanism to preemptively thwart the commitment of non-conforming YAML files. --- .hooks/validate-yaml.sh | 36 +++--- docs/ttpforge-spec.yaml | 265 ++++++++++++++++++---------------------- magefiles/go.mod | 10 +- magefiles/go.sum | 27 +--- magefiles/magefile.go | 134 +++++++++++++++++--- 5 files changed, 263 insertions(+), 209 deletions(-) diff --git a/.hooks/validate-yaml.sh b/.hooks/validate-yaml.sh index 073bfbc..83696c2 100755 --- a/.hooks/validate-yaml.sh +++ b/.hooks/validate-yaml.sh @@ -1,5 +1,12 @@ #!/bin/bash -set -ex +# This script is a pre-commit hook that checks if the mage command is +# installed and if not, prompts the user to install it. If mage is +# installed, the script changes to the repository root and runs the +# `mage validateyaml` command for each staged YAML file. This command +# validates all committed YAML files against a predefined schema. If any +# validation fails, the commit is stopped and an error message is shown. + +set -e # Change to the repository root cd "$(git rev-parse --show-toplevel)" @@ -20,24 +27,23 @@ fi # Get the list of staged files ending with .yaml staged_files=$(git diff --cached --name-only --diff-filter=AM | grep '\.yaml$') -# Variable to track whether validation failed -validation_failed=false +if [[ -z "$staged_files" ]]; then + echo "No YAML files to validate." + exit 0 +fi # Iterate over each staged file and validate it for file in $staged_files; do - "${mage_bin}" validateallyamlfiles docs/ttpforge-spec.yaml "$file" + # Run the mage validateyaml command for the staged YAML file + "${mage_bin}" validateyaml docs/ttpforge-spec.yaml "$file" - # Capture the exit code of the last validation - exit_code=$? + # Catch the exit code of the last command + exit_status=$? - # If the exit code is not zero, set the flag to true - if [ $exit_code -ne 0 ]; then - validation_failed=true + # If the exit code is not zero (i.e., the command failed), + # then stop the commit and show an error message + if [ $exit_status -ne 0 ]; then + echo "Failed to validate YAML file '$file' against the schema." + exit 1 fi done - -# If any validation failed, exit with an error code -if [ "$validation_failed" = true ]; then - echo "Validation failed for one or more YAML files." - exit 1 -fi diff --git a/docs/ttpforge-spec.yaml b/docs/ttpforge-spec.yaml index 3afa5af..a17099d 100644 --- a/docs/ttpforge-spec.yaml +++ b/docs/ttpforge-spec.yaml @@ -1,161 +1,130 @@ --- -TTP: - type: object - properties: - name: - type: string - description: - type: string - required: true - mitre: - $ref: "#/definitions/Mitre" - env: - type: object - additionalProperties: - type: string - steps: - type: array - items: - type: object - oneOf: - - $ref: "#/definitions/FileStep" - - $ref: "#/definitions/BasicStep" - - $ref: "#/definitions/SubTTPStep" - - $ref: "#/definitions/EditStep" - args: - type: array - items: - $ref: "#/definitions/Spec" +definitions: + TTP: + type: object + properties: + name: + type: string + description: + type: string + mitre: + $ref: "#/definitions/Mitre" + steps: + type: array + items: + oneOf: + - $ref: "#/definitions/SubTTPStep" + - $ref: "#/definitions/BasicStep" + - $ref: "#/definitions/EditStep" + args: + type: array + items: + $ref: "#/definitions/Spec" + required: + - name + - description + - steps -Act: - type: object - properties: - if: - type: string - env: - type: object - additionalProperties: - type: string - name: - type: string - required: true - outputs: - type: object - additionalProperties: - $ref: "#/definitions/Spec" + Mitre: + type: object + properties: + tactics: + type: array + items: + type: string + techniques: + type: array + items: + type: string + subtechniques: + type: array + items: + type: string -Mitre: - type: object - properties: - tactics: - type: array - items: - type: string - techniques: - type: array - items: + BasicStep: + type: object + properties: + name: type: string - subtechniques: - type: array - items: + inline: type: string + cleanup: + $ref: "#/definitions/CleanupAct" + args: + type: array + items: + type: string + required: + - name + - inline -FileStep: - type: object - properties: - act: - $ref: "#/definitions/Act" - file: - type: string - executor: - type: string - cleanup: - $ref: "#/definitions/CleanupAct" - args: - type: array - items: + CleanupAct: + type: object + properties: + inline: type: string + required: + - inline -BasicStep: - type: object - properties: - act: - $ref: "#/definitions/Act" - executor: - type: string - inline: - type: string - args: - type: array - items: - type: string - cleanup: - type: string - -SubTTPStep: - type: object - properties: - act: - $ref: "#/definitions/Act" - ttp: - type: string - args: - type: object - additionalProperties: + SubTTPStep: + type: object + properties: + name: type: string + ttp: + type: string + args: + type: object + additionalProperties: + type: string + required: + - name + - ttp -Edit: - type: object - properties: - old: - type: string - new: - type: string - regexp: - type: boolean - -EditStep: - type: object - properties: - act: - $ref: "#/definitions/Act" - edit_file: - type: string - edits: - type: array - items: - $ref: "#/definitions/Edit" - backup_file: - type: string - -CleanupAct: - type: object - properties: - if: - type: string - env: - type: object - additionalProperties: - type: string - name: - type: string - required: true - outputs: - type: object - additionalProperties: + EditStep: + type: object + properties: + name: + type: string + edit_file: + type: string + backup_file: + type: string + edits: type: array items: - $ref: "#/definitions/Spec" + $ref: "#/definitions/Edit" + required: + - name + - edit_file + - backup_file + - edits -Spec: - type: object - properties: - name: - type: string - required: true - type: - type: string - default: - type: string - description: - type: string + Edit: + type: object + properties: + old: + type: string + new: + type: string + regexp: + type: string + required: + - old + - new + - regexp + + Spec: + type: object + properties: + name: + type: string + type: + type: string + default: + type: string + description: + type: string + required: + - name + - type diff --git a/magefiles/go.mod b/magefiles/go.mod index 75a746f..0e9f08e 100755 --- a/magefiles/go.mod +++ b/magefiles/go.mod @@ -5,10 +5,10 @@ go 1.21 toolchain go1.21.1 require ( - github.com/facebookincubator/ttpforge v1.0.6 github.com/go-playground/validator/v10 v10.15.3 github.com/l50/goutils/v2 v2.1.0 github.com/spf13/afero v1.9.5 + github.com/xeipuuv/gojsonschema v1.2.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -40,13 +40,9 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.2.0 // indirect - github.com/tidwall/gjson v1.16.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.24.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect diff --git a/magefiles/go.sum b/magefiles/go.sum index fa14cfa..cd049e1 100755 --- a/magefiles/go.sum +++ b/magefiles/go.sum @@ -51,8 +51,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bitfield/script v0.22.0 h1:LA7QHuEsXMPD52YLtxWrlqCCy+9FOpzNYfsRHC5Gsrc= github.com/bitfield/script v0.22.0/go.mod h1:ms4w+9B8f2/W0mbsgWDVTtl7K94bYuZc3AunnJC4Ebs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -81,8 +79,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/facebookincubator/ttpforge v1.0.6 h1:WPHn7MPB8T4TpZrn7MXws8rzWp47QQHiYZ85B8FeW2E= -github.com/facebookincubator/ttpforge v1.0.6/go.mod h1:AIilkLrCgcdyjWglSffm6U+7l+tSwd9GBj7GQmDYKIM= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -250,15 +246,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= -github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -270,15 +265,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -615,7 +601,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 5f2fdb8..421c0c5 100755 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -27,7 +27,6 @@ import ( "path/filepath" "strings" - "github.com/facebookincubator/ttpforge/pkg/blocks" "github.com/go-playground/validator/v10" "github.com/l50/goutils/v2/dev/lint" mageutils "github.com/l50/goutils/v2/dev/mage" @@ -35,6 +34,7 @@ import ( "github.com/l50/goutils/v2/git" "github.com/l50/goutils/v2/sys" "github.com/spf13/afero" + "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" ) @@ -104,56 +104,154 @@ func RunPreCommit() error { return nil } -// ValidateAllYAMLFiles checks if YAML in the repo is compliant with the schema. -func ValidateAllYAMLFiles(schemaPath, searchDir string) error { - // Load the schema first +// loadSchema loads and unmarshals the schema from a YAML file. +func loadSchema(schemaPath string) (map[string]interface{}, error) { schemaContent, err := os.ReadFile(schemaPath) if err != nil { - return fmt.Errorf("error reading schema file: %v", err) + return nil, fmt.Errorf("error reading schema file: %v", err) } - var schema blocks.TTP + var schema map[string]interface{} err = yaml.Unmarshal(schemaContent, &schema) if err != nil { - return fmt.Errorf("error unmarshalling schema: %v", err) + return nil, fmt.Errorf("error unmarshalling schema: %v", err) + } + + return schema, nil +} + +// ValidateYAML checks if a YAML file is compliant with the schema. +func ValidateYAML(schemaPath, filePath string) error { + // Load the YAML schema + yamlSchema, err := loadSchema(schemaPath) + if err != nil { + return err } validate := validator.New() + if filePath == "ttps" { + err := validateAllYAML(schemaPath, yamlSchema, filePath, validate) + if err != nil { + return err + } + } else { + err := inspectAndValidate(filePath, yamlSchema, validate, 0) // Added depth as 0 for initial call + if err != nil { + return err + } + } + + return nil +} + +// validateAllYAML checks if YAML in the repo is compliant with the schema. +func validateAllYAML(schemaPath string, schema map[string]interface{}, searchDir string, validate *validator.Validate) error { // Start directory walk to validate each YAML file - return filepath.WalkDir(searchDir, func(path string, d os.DirEntry, err error) error { + return filepath.WalkDir(searchDir, func(filePath string, d os.DirEntry, err error) error { if err != nil { return err } // Only consider .yaml files if strings.HasSuffix(strings.ToLower(d.Name()), ".yaml") { - fmt.Printf("Checking: %s\n", path) - return inspectAndValidate(path, schema, validate) + fmt.Printf("Checking: %s\n", filePath) + fmt.Printf("Validating: %s against the TTPForge schema (%s)\n", filePath, schemaPath) + + return inspectAndValidate(filePath, schema, validate, 0) // Added the depth parameter here } return nil }) } -func inspectAndValidate(filePath string, schema blocks.TTP, validate *validator.Validate) error { +func inspectAndValidate(filePath string, schema map[string]interface{}, validate *validator.Validate, depth int) error { + // Check if the file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return fmt.Errorf("file %s does not exist", filePath) + } + fileContent, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("error reading file %s: %v", filePath, err) } - var ttp blocks.TTP - err = yaml.Unmarshal(fileContent, &ttp) + var yamlData map[string]interface{} + err = yaml.Unmarshal(fileContent, &yamlData) if err != nil { return fmt.Errorf("error unmarshalling file %s: %v", filePath, err) } - if err = validate.Struct(ttp); err != nil { - for _, err := range err.(validator.ValidationErrors) { - fmt.Printf("Error in %s - Field: %s, Tag: %s, ActualTag: %s, Value: %v\n", - filePath, err.Field(), err.Tag(), err.ActualTag(), err.Value()) + // Validate the YAML data against the YAML schema + err = validateYAMLAgainstSchema(yamlData, schema) + if err != nil { + return err + } + + repoRoot, err := git.RepoRoot() + if err != nil { + return fmt.Errorf("failed to get repo root: %v", err) + } + + // Check for SubTTPSteps and validate referenced files + if steps, ok := yamlData["steps"].([]interface{}); ok { + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + if ttpPath, ok := stepMap["ttp"].(string); ok { + // This is a SubTTPStep, so validate the referenced file + absTtpPath := filepath.Join(repoRoot, "ttps", ttpPath) + + // Check if the referenced TTP file exists + if _, err := os.Stat(absTtpPath); os.IsNotExist(err) { + return fmt.Errorf("referenced TTP file %s does not exist", absTtpPath) + } + + // This is a SubTTPStep, so validate the referenced file + err = inspectAndValidate(absTtpPath, schema, validate, depth+1) + if err != nil { + return err + } + } + } + } + } + + // Only print the success message for the top-level invocation + if depth == 0 { + fmt.Println("YAML is valid according to the schema.") + } + + return nil +} + +func validateYAMLAgainstSchema(yamlData map[string]interface{}, schema map[string]interface{}) error { + schemaLoader := gojsonschema.NewGoLoader(schema) + documentLoader := gojsonschema.NewGoLoader(yamlData) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return err + } + + if !result.Valid() { + var errors []string + for _, desc := range result.Errors() { + // Append each error to the errors slice + errors = append(errors, desc.String()) + } + return fmt.Errorf("this YAML does not match the schema: %s", strings.Join(errors, "; ")) + } + + // Custom validation: Check for "name" property when "ttp" is defined + if steps, ok := yamlData["steps"].([]interface{}); ok { + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + // Check for the presence of "name" property when "ttp" is defined + if _, nameExists := stepMap["name"]; !nameExists { + return fmt.Errorf("the \"name\" property is missing when \"ttp\" is defined in a step") + } + } } - return fmt.Errorf("file %s does not conform to the schema", filePath) } return nil