From 268d1e8e233cb242bbd609fc5f64b380d7fe1362 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Thu, 30 Jun 2022 13:30:21 +0100 Subject: [PATCH] (feat) add javascript scanner --- .vscode/launch.json | 2 +- go.mod | 8 +- go.sum | 15 ++- plugin/plugin.go | 10 +- scanner/golang/golang.go | 4 +- scanner/javascript/javascript.go | 163 +++++++++++++++++++++++++++++++ scanner/javascript/option.go | 29 ++++++ scanner/util.go | 31 +++++- 8 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 scanner/javascript/javascript.go create mode 100644 scanner/javascript/option.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f7c5d1..e254920 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "mode": "auto", "program": "${workspaceFolder}", "env": { - "PLUGIN_WORKING_DIRECTORY": "/home/tp/workspace/drone", + "PLUGIN_WORKING_DIRECTORY": "/home/tp/workspace/drone-ui", }, }, ] diff --git a/go.mod b/go.mod index ee16c93..83b80f4 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,10 @@ module github.com/tphoney/best_practice go 1.18 require ( + github.com/Masterminds/semver v1.5.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/sirupsen/logrus v1.4.2 + github.com/sirupsen/logrus v1.8.1 golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d ) -require ( - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect - golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect -) +require golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect diff --git a/go.sum b/go.sum index 55ac12a..e8fef05 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,17 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/plugin/plugin.go b/plugin/plugin.go index c1d791a..04b885e 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -14,6 +14,7 @@ import ( "github.com/tphoney/best_practice/outputter/dronebuild" "github.com/tphoney/best_practice/scanner" "github.com/tphoney/best_practice/scanner/golang" + "github.com/tphoney/best_practice/scanner/javascript" "github.com/tphoney/best_practice/types" ) @@ -42,7 +43,7 @@ func Exec(ctx context.Context, args *Args) error { fmt.Println("working directory:", args.WorkingDirectory) // setup requested scanners if len(args.RequestedScanners) == 0 { - args.RequestedScanners = []string{golang.Name} + args.RequestedScanners = []string{javascript.Name, golang.Name} } scanners := make([]types.Scanner, 0) for _, scannerName := range args.RequestedScanners { @@ -54,6 +55,13 @@ func Exec(ctx context.Context, args *Args) error { return err } scanners = append(scanners, g) + case javascript.Name: + // create golang scanner + g, err := javascript.New(javascript.WithWorkingDirectory(args.WorkingDirectory)) + if err != nil { + return err + } + scanners = append(scanners, g) default: fmt.Printf("unknown scanner: %s\n", scannerName) } diff --git a/scanner/golang/golang.go b/scanner/golang/golang.go index 7069af7..8658e54 100644 --- a/scanner/golang/golang.go +++ b/scanner/golang/golang.go @@ -163,7 +163,7 @@ func (sc *scannerConfig) lintCheck() (match bool, outputResults []types.Scanlet) } func (sc *scannerConfig) mainCheck() (match bool, outputResults []types.Scanlet) { - matches, err := scanner.WalkMatch(sc.workingDirectory, "main.go") + matches, err := scanner.FindMatchingFiles(sc.workingDirectory, "main.go") if err == nil && len(matches) > 0 { // we use the first one found mainLocation := strings.TrimPrefix(matches[0], sc.workingDirectory) @@ -195,7 +195,7 @@ func (sc *scannerConfig) mainCheck() (match bool, outputResults []types.Scanlet) } func (sc *scannerConfig) unitTestCheck() (match bool, outputResults []types.Scanlet) { - matches, err := scanner.WalkMatch(sc.workingDirectory, "*_test.go") + matches, err := scanner.FindMatchingFiles(sc.workingDirectory, "*_test.go") if err == nil && len(matches) > 0 { droneBuildResult := types.Scanlet{ Name: UnitTestCheck, diff --git a/scanner/javascript/javascript.go b/scanner/javascript/javascript.go new file mode 100644 index 0000000..9850699 --- /dev/null +++ b/scanner/javascript/javascript.go @@ -0,0 +1,163 @@ +package javascript + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/tphoney/best_practice/outputter/dronebuild" + "github.com/tphoney/best_practice/scanner" + "github.com/tphoney/best_practice/types" + "golang.org/x/exp/slices" +) + +type scannerConfig struct { + name string + description string + workingDirectory string + checksToRun []string + runAll bool +} + +const ( + packageLocation = "package.json" + Name = "javascript" + BuildCheck = "build_check" + TestCheck = "test_check" + LintCheck = "lint_check" +) + +func New(opts ...Option) (types.Scanner, error) { + sc := new(scannerConfig) + sc.name = Name + sc.description = "checks for various javascript related best practices" + sc.runAll = true + // apply options + for _, opt := range opts { + opt(sc) + } + + return sc, nil +} + +func (sc *scannerConfig) Name() string { + return sc.name +} + +func (sc *scannerConfig) Description() string { + return sc.description +} + +func (sc *scannerConfig) AvailableChecks() []string { + return []string{BuildCheck} +} + +func (sc *scannerConfig) Scan(ctx context.Context, requestedChecks []string) (returnVal []types.Scanlet, err error) { + // lets look for a package file in the directory + _, err = os.Stat(filepath.Join(sc.workingDirectory, packageLocation)) + if err != nil { + // nothing to see here, lets leave + return returnVal, nil + } + var scriptMap map[string]interface{} + var dependencyMap map[string]interface{} + var reactVersion string + packageStruct, err := scanner.ReadJSONFile(filepath.Join(sc.workingDirectory, packageLocation)) + if err == nil { + // look for declared scripts + if packageStruct["scripts"] != nil { + scriptMap = packageStruct["scripts"].(map[string]interface{}) + } + if packageStruct["dependencies"] != nil { + dependencyMap = packageStruct["dependencies"].(map[string]interface{}) + rawReactVersion := dependencyMap["react"].(string) + v, versionErr := scanner.ReturnVersionObject(rawReactVersion) + if versionErr != nil { + fmt.Printf("error parsing react version: %s\n", versionErr.Error()) + } + reactVersion = fmt.Sprint(v.Major()) + } + } else { + return returnVal, err + } + // check for build + if sc.runAll || slices.Contains(requestedChecks, BuildCheck) { + match, outputResults := sc.buildCheck(scriptMap, reactVersion) + if match { + returnVal = append(returnVal, outputResults...) + } + } + if sc.runAll || slices.Contains(requestedChecks, LintCheck) { + match, outputResults := sc.lintCheck(scriptMap, reactVersion) + if match { + returnVal = append(returnVal, outputResults...) + } + } + if sc.runAll || slices.Contains(requestedChecks, TestCheck) { + match, outputResults := sc.testCheck(scriptMap, reactVersion) + if match { + returnVal = append(returnVal, outputResults...) + } + } + + return returnVal, nil +} + +func (sc *scannerConfig) buildCheck(scriptMap map[string]interface{}, reactVersion string) (match bool, outputResults []types.Scanlet) { + if scriptMap["build"] != "" { + droneBuildResult := types.Scanlet{ + Name: BuildCheck, + ScannerFamily: Name, + Description: "run npm build", + OutputRenderer: dronebuild.Name, + Spec: dronebuild.OutputFields{ + RawYaml: fmt.Sprintf(` - name: run npm build + image: node:%s-alpine + commands: + - npm run build`, reactVersion)}, + } + outputResults = append(outputResults, droneBuildResult) + return true, outputResults + } + return false, outputResults +} + +func (sc *scannerConfig) lintCheck(scriptMap map[string]interface{}, reactVersion string) (match bool, outputResults []types.Scanlet) { + if scriptMap["lint"] != "" { + droneBuildResult := types.Scanlet{ + Name: BuildCheck, + ScannerFamily: Name, + Description: "run npm lint", + OutputRenderer: dronebuild.Name, + Spec: dronebuild.OutputFields{ + RawYaml: fmt.Sprintf(` - name: run npm lint + image: node:%s-alpine + commands: + - npm run lint`, reactVersion)}, + } + outputResults = append(outputResults, droneBuildResult) + return true, outputResults + } + return false, outputResults +} + +func (sc *scannerConfig) testCheck(scriptMap map[string]interface{}, reactVersion string) (match bool, outputResults []types.Scanlet) { + if scriptMap["test"] != "" { + droneBuildResult := types.Scanlet{ + Name: BuildCheck, + ScannerFamily: Name, + Description: "run npm test", + OutputRenderer: dronebuild.Name, + Spec: dronebuild.OutputFields{ + RawYaml: fmt.Sprintf(` - name: run npm test + image: node:%s-alpine + commands: + - npm run test`, reactVersion)}, + } + outputResults = append(outputResults, droneBuildResult) + return true, outputResults + } + + return false, outputResults +} diff --git a/scanner/javascript/option.go b/scanner/javascript/option.go new file mode 100644 index 0000000..12ab1d3 --- /dev/null +++ b/scanner/javascript/option.go @@ -0,0 +1,29 @@ +package javascript + +import "golang.org/x/exp/slices" + +type Option func(*scannerConfig) + +func WithChecksToRun(i []string) Option { + return func(p *scannerConfig) { + if len(i) > 0 { + validChecks := []string{} + // only add valid checks + for _, check := range i { + if slices.Contains(p.AvailableChecks(), check) { + validChecks = append(validChecks, check) + } + } + p.runAll = false + p.checksToRun = validChecks + } else { + p.runAll = true + } + } +} + +func WithWorkingDirectory(i string) Option { + return func(p *scannerConfig) { + p.workingDirectory = i + } +} diff --git a/scanner/util.go b/scanner/util.go index 0c253d7..4ee56bd 100644 --- a/scanner/util.go +++ b/scanner/util.go @@ -1,11 +1,15 @@ package scanner import ( + "encoding/json" "os" "path/filepath" + "strings" + + "github.com/Masterminds/semver" ) -func WalkMatch(workingDir, pattern string) ([]string, error) { +func FindMatchingFiles(workingDir, pattern string) ([]string, error) { var matches []string err := filepath.Walk(workingDir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -26,3 +30,28 @@ func WalkMatch(workingDir, pattern string) ([]string, error) { } return matches, nil } + +func ReadJSONFile(filePath string) (map[string]interface{}, error) { + file, fileErr := os.Open(filePath) + if fileErr != nil { + return nil, fileErr + } + defer file.Close() + myMap := map[string]interface{}{} + decoder := json.NewDecoder(file) + jsonErr := decoder.Decode(&myMap) + if jsonErr != nil { + return nil, jsonErr + } + return myMap, nil +} + +func ReturnVersionObject(version string) (*semver.Version, error) { + // strip weird stuff from the version slow for now + version = strings.ReplaceAll(version, "^", "") + v, err := semver.NewVersion(version) + if err != nil { + return nil, err + } + return v, nil +}