From 3037b9376997a83f5e4301585402490dbe90648d Mon Sep 17 00:00:00 2001 From: hugo-syn Date: Sun, 27 Oct 2024 21:58:44 +0100 Subject: [PATCH] feat: add support for sarif format --- cmd/scan.go | 32 ++++++++++++++--- common/assets/sarif.template | 66 ++++++++++++++++++++++++++++++++++++ common/helpers.go | 14 ++++++++ core/linter.go | 30 +++++++++++++++- 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 common/assets/sarif.template diff --git a/cmd/scan.go b/cmd/scan.go index a6991d5..708d386 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -25,8 +25,8 @@ Options: -v, --version -d, --debug --verbose - --json JSON output - --oneline Use one line per one error. Useful for reading error messages from programs + --format Output format, json, sarif or custom template to format error messages in Go template syntax. See https://github.com/rhysd/actionlint/tree/main/docs/usage.md#format + --oneline Use one line per one error. Useful for reading error messages from programs Args: Target File or directory to scan @@ -89,6 +89,25 @@ func runScanner(args docopt.Opts) int { return common.ExitStatusFailure } + if args["--format"] != nil { + switch format := args["--format"].(string); format { + case "json": + opts.Format = "{{json .}}" + case "sarif": + opts.Format = string(common.SarifTemplate) + default: + opts.Format = format + } + // Now we can use our own formatter on all the errors. + err = core.DisplayErrors(os.Stdout, opts.Format, errs) + } + + if err != nil { + common.Log.Error(err) + + return common.ExitStatusFailure + } + if len(errs) > 0 { return common.ExitStatusSuccessProblemFound // Linter found some issues, yay! } @@ -157,8 +176,13 @@ func setScannerArgs(args docopt.Opts) actionlint.LinterOptions { opts.Oneline = true } - if v, _ := args.Bool("--json"); v { - opts.Format = "{{json .}}" + /* + This is a hacky trick to disable the formatter of actionlint otherwise it will use + a new formatter for each repo and this not what we want. We want to use the same + formatter for all the repo. It's far from being perfect but I don't know how to do it. + */ + if args["--format"] != nil { + opts.Format = "{{\"\"}}" } setCoreParameter(args) diff --git a/common/assets/sarif.template b/common/assets/sarif.template new file mode 100644 index 0000000..828db71 --- /dev/null +++ b/common/assets/sarif.template @@ -0,0 +1,66 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "octoscan", + "version": {{ getVersion | json }}, + "informationUri": "https://github.com/synacktiv/octoscan", + "rules": [ + {{$first := true}} + {{range $ := allKinds }} + {{if $first}}{{$first = false}}{{else}},{{end}} + { + "id": {{json $.Name}}, + "name": {{$.Name | toPascalCase | json}}, + "defaultConfiguration": { + "level": "error" + }, + "properties": { + "description": {{json $.Description}}, + "queryURI": "https://github.com/synacktiv/octoscan?tab=readme-ov-file#rules" + }, + "fullDescription": { + "text": {{json $.Description}} + }, + "helpUri": "https://github.com/synacktiv/octoscan?tab=readme-ov-file#rules" + } + {{end}} + ] + } + }, + "results": [ + {{$first := true}} + {{range $ := .}} + {{if $first}}{{$first = false}}{{else}},{{end}} + { + "ruleId": {{json $.Kind}}, + "message": { + "text": {{json $.Message}} + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": {{json $.Filepath}}, + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": {{$.Line}}, + "startColumn": {{$.Column}}, + "endColumn": {{$.EndColumn}}, + "snippet": { + "text": {{json $.Snippet}} + } + } + } + } + ] + } + {{end}} + ] + } + ] +} diff --git a/common/helpers.go b/common/helpers.go index fe857eb..7c58467 100644 --- a/common/helpers.go +++ b/common/helpers.go @@ -1,6 +1,7 @@ package common import ( + _ "embed" "net/http" "os" "regexp" @@ -49,6 +50,16 @@ var SyntaxCheckErrors = []string{ "sequence node but mapping node is expected", "please remove this section if it's unnecessary", "is only available for a reusable workflow call with", + "previously defined at line", + "could not parse as YAML", + "it must be one of", + "event must be one of", + "this step is for running shell command since it contains", + "job is scalar node but mapping node is expected", + "section should have", + "but found plain text node", + "but found mapping node with", + "section must be mapping", } var TriggerWithExternalData = []string{ @@ -97,6 +108,9 @@ var AllTriggers = []string{ "workflow_run", } +//go:embed assets/sarif.template +var SarifTemplate []byte + func IsDirectory(path string) bool { fileInfo, err := os.Stat(path) if err != nil { diff --git a/core/linter.go b/core/linter.go index 962052a..1c478c1 100644 --- a/core/linter.go +++ b/core/linter.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "io" "io/fs" "os" "path/filepath" @@ -130,7 +131,7 @@ func offlineRules() []actionlint.Rule { } if RulesSwitch["debug-artefacts"] { - res = append(res, rules.NewRuleRuleDebugArtefacts()) + res = append(res, rules.NewRuleRuleDebugArtefacts(FilterTriggers)) } if RulesSwitch["debug-js-exec"] { @@ -174,3 +175,30 @@ func (l *OctoLinter) LintRepositoryRecurse(dir string) ([]*actionlint.Error, err } return lintErrors, nil } + +/* +Everything is stolen from here: https://github.com/rhysd/actionlint/blob/main/docs/usage.md#format +Again thanks @rhysd for the great work. +*/ +func DisplayErrors(writer io.Writer, format string, errs []*actionlint.Error) error { + formatter, err := actionlint.NewErrorFormatter(format) + rules := &[]actionlint.Rule{} + availableRules := OnRulesCreated(*rules) + + for _, rule := range availableRules { + formatter.RegisterRule(rule) + } + + if err != nil { + return err + } + + temp := make([]*actionlint.ErrorTemplateFields, 0, len(errs)) + + for _, octoscanErr := range errs { + src, _ := os.ReadFile(octoscanErr.Filepath) + temp = append(temp, octoscanErr.GetTemplateFields(src)) + } + + return formatter.Print(writer, temp) +}