1// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"encoding/json"
19	"flag"
20	"fmt"
21	"io/ioutil"
22	"log"
23	"os"
24	"path/filepath"
25	"sort"
26	"strings"
27
28	gas "github.com/GoASTScanner/gas/core"
29	"github.com/GoASTScanner/gas/output"
30)
31
32type recursion bool
33
34const (
35	recurse   recursion = true
36	noRecurse recursion = false
37)
38
39var (
40	// #nosec flag
41	flagIgnoreNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set")
42
43	// format output
44	flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv, html, or text")
45
46	// output file
47	flagOutput = flag.String("out", "", "Set output file for results")
48
49	// config file
50	flagConfig = flag.String("conf", "", "Path to optional config file")
51
52	// quiet
53	flagQuiet = flag.Bool("quiet", false, "Only show output when errors are found")
54
55	usageText = `
56GAS - Go AST Scanner
57
58Gas analyzes Go source code to look for common programming mistakes that
59can lead to security problems.
60
61USAGE:
62
63	# Check a single Go file
64	$ gas example.go
65
66	# Check all files under the current directory and save results in
67	# json format.
68	$ gas -fmt=json -out=results.json ./...
69
70	# Run a specific set of rules (by default all rules will be run):
71	$ gas -include=G101,G203,G401 ./...
72
73	# Run all rules except the provided
74	$ gas -exclude=G101 ./...
75
76`
77
78	logger *log.Logger
79)
80
81func extendConfList(conf map[string]interface{}, name string, inputStr string) {
82	if inputStr == "" {
83		conf[name] = []string{}
84	} else {
85		input := strings.Split(inputStr, ",")
86		if val, ok := conf[name]; ok {
87			if data, ok := val.(*[]string); ok {
88				conf[name] = append(*data, input...)
89			} else {
90				logger.Fatal("Config item must be a string list: ", name)
91			}
92		} else {
93			conf[name] = input
94		}
95	}
96}
97
98func buildConfig(incRules string, excRules string) map[string]interface{} {
99	config := make(map[string]interface{})
100	if flagConfig != nil && *flagConfig != "" { // parse config if we have one
101		if data, err := ioutil.ReadFile(*flagConfig); err == nil {
102			if err := json.Unmarshal(data, &(config)); err != nil {
103				logger.Fatal("Could not parse JSON config: ", *flagConfig, ": ", err)
104			}
105		} else {
106			logger.Fatal("Could not read config file: ", *flagConfig)
107		}
108	}
109
110	// add in CLI include and exclude data
111	extendConfList(config, "include", incRules)
112	extendConfList(config, "exclude", excRules)
113
114	// override ignoreNosec if given on CLI
115	if flagIgnoreNoSec != nil {
116		config["ignoreNosec"] = *flagIgnoreNoSec
117	} else {
118		val, ok := config["ignoreNosec"]
119		if !ok {
120			config["ignoreNosec"] = false
121		} else if _, ok := val.(bool); !ok {
122			logger.Fatal("Config value must be a bool: 'ignoreNosec'")
123		}
124	}
125
126	return config
127}
128
129// #nosec
130func usage() {
131
132	fmt.Fprintln(os.Stderr, usageText)
133	fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
134	flag.PrintDefaults()
135	fmt.Fprint(os.Stderr, "\n\nRULES:\n\n")
136
137	// sorted rule list for eas of reading
138	rl := GetFullRuleList()
139	keys := make([]string, 0, len(rl))
140	for key := range rl {
141		keys = append(keys, key)
142	}
143	sort.Strings(keys)
144	for _, k := range keys {
145		v := rl[k]
146		fmt.Fprintf(os.Stderr, "\t%s: %s\n", k, v.description)
147	}
148	fmt.Fprint(os.Stderr, "\n")
149}
150
151func main() {
152
153	// Setup usage description
154	flag.Usage = usage
155
156	//  Exclude files
157	excluded := newFileList("*_test.go")
158	flag.Var(excluded, "skip", "File pattern to exclude from scan. Uses simple * globs and requires full or partial match")
159
160	incRules := ""
161	flag.StringVar(&incRules, "include", "", "Comma separated list of rules IDs to include. (see rule list)")
162
163	excRules := ""
164	flag.StringVar(&excRules, "exclude", "", "Comma separated list of rules IDs to exclude. (see rule list)")
165
166	// Custom commands / utilities to run instead of default analyzer
167	tools := newUtils()
168	flag.Var(tools, "tool", "GAS utilities to assist with rule development")
169
170	// Setup logging
171	logger = log.New(os.Stderr, "[gas] ", log.LstdFlags)
172
173	// Parse command line arguments
174	flag.Parse()
175
176	// Ensure at least one file was specified
177	if flag.NArg() == 0 {
178
179		fmt.Fprintf(os.Stderr, "\nError: FILE [FILE...] or './...' expected\n")
180		flag.Usage()
181		os.Exit(1)
182	}
183
184	// Run utils instead of analysis
185	if len(tools.call) > 0 {
186		tools.run(flag.Args()...)
187		os.Exit(0)
188	}
189
190	// Setup analyzer
191	config := buildConfig(incRules, excRules)
192	analyzer := gas.NewAnalyzer(config, logger)
193	AddRules(&analyzer, config)
194
195	toAnalyze := getFilesToAnalyze(flag.Args(), excluded)
196
197	for _, file := range toAnalyze {
198		logger.Printf(`Processing "%s"...`, file)
199		if err := analyzer.Process(file); err != nil {
200			logger.Printf(`Failed to process: "%s"`, file)
201			logger.Println(err)
202			logger.Fatalf(`Halting execution.`)
203		}
204	}
205
206	issuesFound := len(analyzer.Issues) > 0
207	// Exit quietly if nothing was found
208	if !issuesFound && *flagQuiet {
209		os.Exit(0)
210	}
211
212	// Create output report
213	if *flagOutput != "" {
214		outfile, err := os.Create(*flagOutput)
215		if err != nil {
216			logger.Fatalf("Couldn't open: %s for writing. Reason - %s", *flagOutput, err)
217		}
218		defer outfile.Close()
219		output.CreateReport(outfile, *flagFormat, &analyzer)
220	} else {
221		output.CreateReport(os.Stdout, *flagFormat, &analyzer)
222	}
223
224	// Do we have an issue? If so exit 1
225	if issuesFound {
226		os.Exit(1)
227	}
228}
229
230// getFilesToAnalyze lists all files
231func getFilesToAnalyze(paths []string, excluded *fileList) []string {
232	//log.Println("getFilesToAnalyze: start")
233	var toAnalyze []string
234	for _, relativePath := range paths {
235		//log.Printf("getFilesToAnalyze: processing \"%s\"\n", path)
236		// get the absolute path before doing anything else
237		path, err := filepath.Abs(relativePath)
238		if err != nil {
239			log.Fatal(err)
240		}
241		if filepath.Base(relativePath) == "..." {
242			toAnalyze = append(
243				toAnalyze,
244				listFiles(filepath.Dir(path), recurse, excluded)...,
245			)
246		} else {
247			var (
248				finfo os.FileInfo
249				err   error
250			)
251			if finfo, err = os.Stat(path); err != nil {
252				logger.Fatal(err)
253			}
254			if !finfo.IsDir() {
255				if shouldInclude(path, excluded) {
256					toAnalyze = append(toAnalyze, path)
257				}
258			} else {
259				toAnalyze = listFiles(path, noRecurse, excluded)
260			}
261		}
262	}
263	//log.Println("getFilesToAnalyze: end")
264	return toAnalyze
265}
266
267// listFiles returns a list of all files found that pass the shouldInclude check.
268// If doRecursiveWalk it true, it will walk the tree rooted at absPath, otherwise it
269// will only include files directly within the dir referenced by absPath.
270func listFiles(absPath string, doRecursiveWalk recursion, excluded *fileList) []string {
271	var files []string
272
273	walk := func(path string, info os.FileInfo, err error) error {
274		if info.IsDir() && doRecursiveWalk == noRecurse {
275			return filepath.SkipDir
276		}
277		if shouldInclude(path, excluded) {
278			files = append(files, path)
279		}
280		return nil
281	}
282
283	if err := filepath.Walk(absPath, walk); err != nil {
284		log.Fatal(err)
285	}
286	return files
287}
288
289// shouldInclude checks if a specific path which is expected to reference
290// a regular file should be included
291func shouldInclude(path string, excluded *fileList) bool {
292	return filepath.Ext(path) == ".go" && !excluded.Contains(path)
293}
294