diff --git a/analyzers/generic.go b/analyzers/generic.go index 49fd8b4..18a5496 100644 --- a/analyzers/generic.go +++ b/analyzers/generic.go @@ -15,13 +15,9 @@ package analyzers import ( - "io/ioutil" - "log" - "github.com/praetorian-inc/gokart/util" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/buildssa" - "gopkg.in/yaml.v3" ) // Creates generic taint analyzer based on Sources and Sinks defined in analyzers.yaml file @@ -66,53 +62,20 @@ func genericFunctionRun(pass *analysis.Pass, vulnPathFuncs map[string][]string, // LoadGenericAnalyzers creates generic taint anlalyzers from custom Sources and Sinks defined in analyzers.yaml // converts all variables to SSA form to construct a call graph and performs // recursive taint analysis to search for input sources of user-controllable data +func LoadGenericAnalyzers() []*analysis.Analyzer { + var analyzers []*analysis.Analyzer -func LoadGenericAnalyzers(yaml_path string) []*analysis.Analyzer { - yfile, err := ioutil.ReadFile(yaml_path) - if err != nil { - log.Fatal(err) - } - - data := make(map[interface{}]map[interface{}]map[interface{}]interface{}) - err = yaml.Unmarshal(yfile, &data) - if err != nil { - log.Fatal(err) - } - - // Load analyzers from the interface - analyzers := []*analysis.Analyzer{} - m := data["analyzers"] - for analyzerName, analyzerDict := range m { - // Get the vulnerability message - message := "" - if analyzerDict["message"] != nil { - message = analyzerDict["message"].(string) - } - - // Load the map of vulnerable functions - vulnCalls := make(map[string][]string) - yamlCallsMap := analyzerDict["vuln_calls"].(map[string]interface{}) - for pkgName, packageVulnFuncs := range yamlCallsMap { - var newList []string - vulnCalls[pkgName] = newList - packageVulnFuncsList := packageVulnFuncs.([]interface{}) - for _, val := range packageVulnFuncsList { - vulnCalls[pkgName] = append(vulnCalls[pkgName], val.(string)) - } - } - - // Wrap generic_function_run with a function that the analyze package can use + for analyzerName, analyzerDict := range util.ScanConfig.Analyzers { analyzerFunc := func(pass *analysis.Pass) (interface{}, error) { - return genericFunctionRun(pass, vulnCalls, analyzerName.(string), message) + return genericFunctionRun(pass, analyzerDict.VulnCalls, analyzerName, analyzerDict.Message) } - - // Form the analyzer object and append to the analyzer list - analysisRun := new(analysis.Analyzer) - analysisRun.Name = "path_traversal" - analysisRun.Doc = analyzerDict["doc"].(string) - analysisRun.Run = analyzerFunc - analysisRun.Requires = []*analysis.Analyzer{buildssa.Analyzer} - analyzers = append(analyzers, analysisRun) + analysisRun := analysis.Analyzer{ + Name: analyzerName, + Doc: analyzerDict.Doc, + Run: analyzerFunc, + Requires: []*analysis.Analyzer{buildssa.Analyzer}, + } + analyzers = append(analyzers, &analysisRun) } return analyzers diff --git a/analyzers/scan.go b/analyzers/scan.go index c71472d..6e39bd7 100644 --- a/analyzers/scan.go +++ b/analyzers/scan.go @@ -58,7 +58,7 @@ func Scan(args []string) { } // Fix up the path to make sure it is pointed at a directory (even if given a file) - fileInfo, err := os.Stat(strings.TrimRight(target_path, "...")) + fileInfo, err := os.Stat(strings.TrimRight(target_path, ".")) if err != nil { log.Fatal(err) } @@ -76,7 +76,7 @@ func Scan(args []string) { } } - err = os.Chdir(strings.TrimRight(target_path, "...")) + err = os.Chdir(strings.TrimRight(target_path, ".")) if err != nil { log.Fatal(err) } @@ -86,7 +86,7 @@ func Scan(args []string) { } } - generic_analyzers := LoadGenericAnalyzers(util.Config.YMLPath) + generic_analyzers := LoadGenericAnalyzers() Analyzers = append(Analyzers, generic_analyzers[:]...) // Begin timer diff --git a/util/analyzers.yml b/util/analyzers.yml index b5be05d..080f2d3 100644 --- a/util/analyzers.yml +++ b/util/analyzers.yml @@ -33,66 +33,66 @@ # - "Printf" + +# Each entry specifies a source that should be considered untrusted +# If the package already exists in the sources section, add the variable/function/type underneath +# Each package can contain multiple vulnerable sources. sources: - # Each entry specifies a source that should be considered untrusted - # If the package already exists in the sources section, add the variable/function/type underneath - # Each package can contain multiple vulnerable sources. - sources: - # Sources that are defined in Go documentation as a "variable" go here (note: these variables will have an SSA type of "Global"). - variables: - "os": - - "Args" - # Sources that are defined in Go documentation as a "function" go here. - functions: - "flag": - - "Arg" - - "Args" - "os": - - "Environ" - - "File" - - "FileInfo" - - "FileMode" - - "Readdir" - - "Readdirnames" - - "OpenFile" - "crypto/tls": - - "LoadX509KeyPair" - - "X509KeyPair" - "os/user": - - "Lookup" - - "LookupId" - - "Current" - "crypto/x509": - - "Subjects" - "io": - - "ReadAtLeast" - - "ReadFull" - "database/sql": - - "Query" - - "QueryRow" - "bytes": - - "String" - - "ReadBytes" - - "ReadByte" - "bufio": - - "Text" - - "Bytes" - - "ReadString" - - "ReadSlice" - - "ReadRune" - - "ReadLine" - - "ReadBytes" - - "ReadByte" - "archive/tar": - - "Next" - - "FileInfo" - - "Header" - "net/url": - - "ParseQuery" - - "ParseUriRequest" - - "Parse" - - "Query" - # Sources that are defined in Go documentation as a "type" go here (note: adding types will consider all functions that use that type to be tainted). - types: - "net/http": - - "Request" + # Sources that are defined in Go documentation as a "variable" go here (note: these variables will have an SSA type of "Global"). + variables: + "os": + - "Args" + # Sources that are defined in Go documentation as a "function" go here. + functions: + "flag": + - "Arg" + - "Args" + "os": + - "Environ" + - "File" + - "FileInfo" + - "FileMode" + - "Readdir" + - "Readdirnames" + - "OpenFile" + "crypto/tls": + - "LoadX509KeyPair" + - "X509KeyPair" + "os/user": + - "Lookup" + - "LookupId" + - "Current" + "crypto/x509": + - "Subjects" + "io": + - "ReadAtLeast" + - "ReadFull" + "database/sql": + - "Query" + - "QueryRow" + "bytes": + - "String" + - "ReadBytes" + - "ReadByte" + "bufio": + - "Text" + - "Bytes" + - "ReadString" + - "ReadSlice" + - "ReadRune" + - "ReadLine" + - "ReadBytes" + - "ReadByte" + "archive/tar": + - "Next" + - "FileInfo" + - "Header" + "net/url": + - "ParseQuery" + - "ParseUriRequest" + - "Parse" + - "Query" + # Sources that are defined in Go documentation as a "type" go here (note: adding types will consider all functions that use that type to be tainted). + types: + "net/http": + - "Request" diff --git a/util/config.go b/util/config.go index bd69d75..e567ad6 100644 --- a/util/config.go +++ b/util/config.go @@ -16,13 +16,12 @@ package util import ( _ "embed" - "flag" "fmt" "io/ioutil" "log" "os" - "path" "path/filepath" + "sync" "gopkg.in/yaml.v3" ) @@ -36,6 +35,28 @@ type ConfigType struct { YMLPath string } +// ConfigFile stores the values parsed from the configuration file +type ConfigFile struct { + Analyzers map[string]Analyzer `yaml:"analyzers"` + Sources Sources `yaml:"sources"` +} + +// Analyzer stores an analyzer parsed from the configuration file +type Analyzer struct { + Doc string `yaml:"doc"` + Message string `yaml:"message"` + VulnCalls map[string][]string `yaml:"vuln_calls"` +} + +// Sources stores the untrusted sources parsed from the configuration file +type Sources struct { + Variables map[string][]string `yaml:"variables"` + Functions map[string][]string `yaml:"functions"` + Types map[string][]string `yaml:"types"` + // For compatibility with older analyzer.yml format + OldSrcs *Sources `yaml:"sources"` +} + var ( FilesFound = 0 VulnGlobalVars map[string][]string @@ -45,115 +66,104 @@ var ( DefaultAnalyzersContent []byte ) -var Config ConfigType +var ( + configDir string + once sync.Once +) -func LoadVulnerableSources() { - // Load YAML - yamlPath := Config.YMLPath - // If not found in the working directory, use the one in the executable's directory - if _, err := os.Stat(yamlPath); os.IsNotExist(err) { - execPath, err := os.Executable() - if err != nil { - log.Fatal(err) - } - yamlPath = path.Join(path.Dir(execPath), yamlPath) - } - yfile, err := ioutil.ReadFile(yamlPath) +var ( + Config ConfigType + ScanConfig ConfigFile +) + +func LoadScanConfig() { + configBytes, err := ioutil.ReadFile(Config.YMLPath) if err != nil { log.Fatal(err) } - - data := make(map[interface{}]map[interface{}]map[interface{}]interface{}) - err = yaml.Unmarshal(yfile, &data) - if err != nil { + if err := yaml.Unmarshal(configBytes, &ScanConfig); err != nil { log.Fatal(err) } - skeys := data["sources"] - if Config.Debug { - log.Println("Beginning list of sources defined in yml:") + // If OldSrcs isn't nil, then the config file is in the old format and we unnest the values + if ScanConfig.Sources.OldSrcs != nil { + ScanConfig.Sources.Functions = ScanConfig.Sources.OldSrcs.Functions + ScanConfig.Sources.Variables = ScanConfig.Sources.OldSrcs.Variables + ScanConfig.Sources.Types = ScanConfig.Sources.OldSrcs.Types + // Set OldSrcs to nil to let the garbage collector clean it up + ScanConfig.Sources.OldSrcs = nil } - for _, sdict := range skeys { - for stype, sTypeDict := range sdict { - callsmap := sTypeDict.(map[string]interface{}) - vulnmap := make(map[string][]string) - - for package_name, package_vuln_funcs := range callsmap { - // Initialize an empty array if the map key does not exist - if _, ok := vulnmap[package_name]; !ok { - var empty_array []string - vulnmap[package_name] = empty_array - } - package_vuln_funcs_arr := package_vuln_funcs.([]interface{}) - for i, val := range package_vuln_funcs_arr { - if Config.Debug { - log.Println("Function", package_vuln_funcs_arr[i], "in package", package_name) - } - vulnmap[package_name] = append(vulnmap[package_name], val.(string)) - } - } - // Set the map of vulnerable sources of this type - if stype == "variables" { - VulnGlobalVars = vulnmap - } else if stype == "functions" { - VulnGlobalFuncs = vulnmap - } else if stype == "types" { - VulnTypes = vulnmap + if Config.Debug { + log.Println("Beginning list of default sources defined in yml:") + for pkg, fn := range ScanConfig.Sources.Functions { + log.Printf("Functions %s in package %s\n", fn, pkg) + } + if len(ScanConfig.Analyzers) > 0 { + log.Println("\nBeginning list of analyzers defined in yml:") + for name, values := range ScanConfig.Analyzers { + log.Printf("Name: %s\n", name) + log.Printf("Doc: %s\n", values.Doc) + log.Printf("Message: %s\n", values.Message) + log.Println("Vuln Calls:") + for pkg, fn := range values.VulnCalls { + log.Printf("Functions %s in package %s\n", fn, pkg) + } } } + log.Printf("\n\n") } - if Config.Debug { - log.Println("List of sources complete") - } + VulnGlobalVars = ScanConfig.Sources.Variables + VulnGlobalFuncs = ScanConfig.Sources.Functions + VulnTypes = ScanConfig.Sources.Types } // InitConfig() parses the flags and sets the corresponding Config variables func InitConfig(globals bool, sarif bool, verbose bool, debug bool, yml string) { - - flag.Parse() - - // If the YAML path provided is a relative path, convert it to absolute - if yml != "" && !filepath.IsAbs(yml) { - yml, _ = filepath.Abs(yml) - } - - // If the YAML path provided is empty or doesn't exist, then load from the default of ~/.gokart/analyzers.yml - if _, err := os.Stat(yml); os.IsNotExist(err) { - if yml != "" { - fmt.Printf("Custom analyzers config file not found at %q. ", yml) - } - fmt.Println("Using default analyzers config found at \"~/.gokart/analyzers.yml\".") - - // Load YAML - config_path := os.ExpandEnv("$HOME/.gokart") - yaml_path := path.Join(config_path, "analyzers.yml") - yml = yaml_path - - // Create our config directory if it doesn't already exist - if _, err := os.Stat(config_path); os.IsNotExist(err) { - err = os.Mkdir(config_path, 0744) - if err != nil { - log.Fatal(err) - } - } - - // If not found in the working directory, use the one in the executable's directory - if _, err := os.Stat(yaml_path); os.IsNotExist(err) { - // default_analyzers_content is populated using the go:embed directive above - err := ioutil.WriteFile(yaml_path, DefaultAnalyzersContent, 0744) - if err != nil { - log.Fatal(err) - } - fmt.Println("No existing analyzers.yml file found - writing default to ~/.gokart/analyzers.yml") - } + if yml == "" { + yml = getDefaultConfigPath() + } else if _, err := os.Stat(yml); err != nil { + log.Fatalf("failed to find the provided config file at %s: %v", yml, err) } + fmt.Printf("Using config found at %s\n", yml) Config.GlobalsSafe = !globals Config.OutputSarif = sarif Config.Debug = debug Config.Verbose = verbose Config.YMLPath = yml - LoadVulnerableSources() + LoadScanConfig() +} + +// getDefaultConfigPath gets the path to the default configuration file and creates it if it doesn't yet exist. +func getDefaultConfigPath() string { + setConfigDir() + yamlPath := filepath.Join(configDir, "analyzers.yml") + + // If ~/.gokart/analyzers.yml doesn't exist, create it with the default config + if _, err := os.Stat(yamlPath); os.IsNotExist(err) { + fmt.Printf("Initializing default config at %s\n", yamlPath) + if err := ioutil.WriteFile(yamlPath, DefaultAnalyzersContent, 0o744); err != nil { + log.Fatalf("failed to write default config to %s: %v", yamlPath, err) + } + } else if err != nil { + // If the error returned by os.Stat is not ErrNotExist + log.Fatalf("failed to initialize default config: %v", err) + } + return yamlPath +} + +// setConfigDir initializes the configDir variable upon its first invocation, does nothing otherwise. +func setConfigDir() { + once.Do(func() { + userHomeDir, err := os.UserHomeDir() + if err != nil { + log.Fatalf("failed to get home directory: %v", err) + } + configDir = filepath.Join(userHomeDir, ".gokart") + if err = os.MkdirAll(configDir, 0o744); err != nil { + log.Fatalf("failed to create config directory %s: %v", configDir, err) + } + }) } diff --git a/util/finding.go b/util/finding.go index e7adc63..3269c98 100644 --- a/util/finding.go +++ b/util/finding.go @@ -50,8 +50,11 @@ func StripArguments(parentFunction string) string { // prints out a finding; returns true if the finding was valid and false if the finding had the same source and sink func OutputFinding(finding Finding) bool { - // if the source and sink are the same, return false and do not print out the finding + if len(finding.Untrusted_Source) == 0 { + return false + } if finding.Vulnerable_Function.SourceCode == finding.Untrusted_Source[0].SourceCode { + // if the source and sink are the same, return false and do not print out the finding return false } if Config.OutputSarif {