github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/internal/linter/spectral/linter.go (about) 1 package spectral 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 10 "github.com/ghodss/yaml" 11 12 "github.com/w3security/vervet/v5/config" 13 "github.com/w3security/vervet/v5/internal/files" 14 "github.com/w3security/vervet/v5/internal/linter" 15 ) 16 17 // Spectral runs spectral on collections of files with a set of rules. 18 type Spectral struct { 19 rules []string 20 extraArgs []string 21 22 spectralPath string 23 rulesPath string 24 } 25 26 // New returns a new Spectral instance. 27 func New(ctx context.Context, cfg *config.SpectralLinter) (*Spectral, error) { 28 rules, extraArgs := cfg.Rules, cfg.ExtraArgs 29 if len(rules) == 0 { 30 return nil, fmt.Errorf("missing spectral rules") 31 } 32 spectralPath := cfg.Script 33 ok := (cfg.Script != "") 34 if !ok { 35 spectralPath, ok = findSpectralAdjacent() 36 } 37 if !ok { 38 spectralPath, ok = findSpectralFromPath() 39 } 40 if !ok { 41 return nil, fmt.Errorf("cannot find spectral linter: `npm install -g spectral-cli` and try again?") 42 } 43 44 var rulesPath string 45 rulesFile, err := os.CreateTemp("", "*.yaml") 46 if err != nil { 47 return nil, fmt.Errorf("failed to create temp rules file: %w", err) 48 } 49 defer rulesFile.Close() 50 resolvedRules := make([]string, len(rules)) 51 for i := range rules { 52 resolvedRules[i], err = filepath.Abs(rules[i]) 53 if err != nil { 54 return nil, err 55 } 56 } 57 rulesDoc := map[string]interface{}{ 58 "extends": resolvedRules, 59 } 60 rulesBuf, err := yaml.Marshal(&rulesDoc) 61 if err != nil { 62 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 63 } 64 _, err = rulesFile.Write(rulesBuf) 65 if err != nil { 66 return nil, fmt.Errorf("failed to marshal temp rules file: %w", err) 67 } 68 rulesPath = rulesFile.Name() 69 go func() { 70 <-ctx.Done() 71 os.Remove(rulesPath) 72 }() 73 return &Spectral{ 74 rules: resolvedRules, 75 spectralPath: spectralPath, 76 rulesPath: rulesPath, 77 extraArgs: extraArgs, 78 }, nil 79 } 80 81 // Match implements linter.Linter. 82 func (s *Spectral) Match(rcConfig *config.ResourceSet) ([]string, error) { 83 return files.LocalFSSource{}.Match(rcConfig) 84 } 85 86 // WithOverride implements linter.Linter. 87 func (s *Spectral) WithOverride(ctx context.Context, override *config.Linter) (linter.Linter, error) { 88 if override.Spectral == nil { 89 return nil, fmt.Errorf("invalid linter override") 90 } 91 merged := *override.Spectral 92 merged.Rules = append(s.rules, merged.Rules...) 93 return New(ctx, &merged) 94 } 95 96 // Run runs spectral on the given paths. Linting output is written to standard 97 // output by spectral. Returns an error when lint fails configured rules. 98 func (s *Spectral) Run(ctx context.Context, _ string, paths ...string) error { 99 cmd := exec.CommandContext( 100 ctx, 101 s.spectralPath, 102 append(append([]string{"lint", "-r", s.rulesPath}, s.extraArgs...), paths...)...) 103 cmd.Stdin = os.Stdin 104 cmd.Stdout = os.Stdout 105 cmd.Stderr = os.Stderr 106 return cmd.Run() 107 } 108 109 func findSpectralAdjacent() (string, bool) { 110 if len(os.Args) < 1 { 111 // hmmm 112 return "", false 113 } 114 binDir := filepath.Dir(os.Args[0]) 115 binFile := filepath.Join(binDir, "spectral") 116 st, err := os.Stat(binFile) 117 return binFile, err == nil && !st.IsDir() && st.Mode()&0111 != 0 118 } 119 120 func findSpectralFromPath() (string, bool) { 121 path, err := exec.LookPath("spectral") 122 return path, err == nil 123 }