├── .gitignore ├── LICENSE ├── README.md ├── bind.go ├── closer.go ├── error.go ├── exec.go ├── hardcoded.go ├── insecureCrypto.go ├── insecureRand.go ├── intToStr.go ├── main.go ├── readAll.go ├── sql.go ├── sql2.go ├── testdata ├── bind.go ├── constTest.go ├── errors.go ├── exec.go ├── hardcoded.go ├── importFail.go ├── insecureCrypto.go ├── insecureTLS.go ├── int2strCon.go ├── ioutilReadAll.go ├── noClose.go ├── rand.go ├── sql.go ├── textTemp.go └── unsafe.go ├── textTemp.go ├── tlsConfig.go ├── unsafe.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | ./Go-AST-Analysis-Tool 4 | Go-AST-Analysis-Tool 5 | Glasgo 6 | ./Glasgo 7 | *.swp 8 | todo 9 | done 10 | squib.go 11 | 12 | # local testing 13 | sandbox 14 | sandbox/* 15 | 16 | # random junk 17 | .DS_Store 18 | printAST.txt 19 | printAST.go 20 | Output.txt 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Terence Tarvis. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of NCC Group Ltd. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glasgo Static Analysis Tool 2 | 3 | A static analysis tool intended to check for potential security issues. New tests will be added soon. Special thanks to NCC Group Plc. 4 | 5 | ## Project 6 | 7 | This is a static analysis tool written in Go for Go code. It will find security and some correctness issues that may have a 8 | security implication. The tool will attempt to complete as many tests as possible even if incomplete or unresolved source is scanned. 9 | 10 | ## Compiling 11 | 12 | To compile the tool, be sure to have the Go compiler first. 13 | You will need to install dependencies for the time being. Consider downloading a binary release instead. 14 | 15 | 1. Use `Go build` for a local binary 16 | 2. Use `Go install` to compile and install in Go Path 17 | 18 | ## Using the tool 19 | 20 | For now, all tests are run. 21 | 22 | ``` 23 | Glasgo directory1, directory2 24 | ``` 25 | 26 | or 27 | 28 | ``` 29 | Glasgo file1.go, file2.go 30 | ``` 31 | 32 | or, when source files are outside of the Go path or the tool can't find them: 33 | 34 | ``` 35 | Glasgo -source directory1 36 | ``` 37 | 38 | `verbose` flag prints all warnings and error messages 39 | 40 | ``` 41 | Glasgo -verbose directory1 42 | ``` 43 | 44 | Test files, those with suffix _test.go, are not checked by default by GlasGo. To include them use the `test` flag. 45 | 46 | ``` 47 | Glasgo -test directory1 48 | ``` 49 | 50 | `Note:` The tool does not run on both directories and individual files 51 | 52 | ## Architecture 53 | 54 | tbd 55 | 56 | ## Tests 57 | 58 | * `error` - errors ignored 59 | * `closer` - no file.Close() method called in function with file.Open() 60 | * `insecureCrypto` - insecure cryptographic primitives 61 | * `insecureRand` - insecurely generated random numbers 62 | * `intToStr` - integer to string conversion without calling strconv 63 | * `readAll` - ioutil.ReadAll called 64 | * `textTemp` - checks if HTTP methods and template/text are in use 65 | * `hardcoded` - looks for hardcoded credentials 66 | * `bind` - checks if listener bound to all interfaces 67 | * `TLSConfig` - checks for insecure TLS configuration 68 | * `exec` - checks for use of os/exec package 69 | * `unsafe` - checks for use of unsafe package 70 | * `sql` - checks for non constant strings used in database query methods. 71 | 72 | ## Design Choices 73 | 74 | see the wiki 75 | 76 | ## Updates 77 | 78 | Initial wave of tests have been uploaded and checked on test data 79 | 80 | More tests to come 81 | 82 | ## to do 83 | 84 | * add tests 85 | * document tests 86 | * document design choices 87 | 88 | -------------------------------------------------------------------------------- /bind.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "strings" 9 | ) 10 | 11 | func init() { 12 | register("bind", 13 | "this test checks for network listeners bound to all interfaces", 14 | bindCheck, 15 | callExpr) 16 | } 17 | 18 | func bindCheck(f *File, node ast.Node) { 19 | var names []string 20 | var callName string 21 | if call, ok := node.(*ast.CallExpr); ok { 22 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 23 | // SelectorExpr has two fields 24 | // X and Sel 25 | // X (through reflection) was found to be an Ident 26 | // Ident's have a field Name also. 27 | if id, ok := (fun.X).(*ast.Ident); ok { 28 | names = append(names, id.Name); 29 | names = append(names, fun.Sel.Name); 30 | callName = strings.Join(names, "/"); 31 | if(callName == "net/Listen" || callName == "tls/Listen") { 32 | if len(call.Args) > 1 { // just a check to be sure 33 | if basicLit, ok := call.Args[1].(*ast.BasicLit); ok { 34 | if(strings.Contains(basicLit.Value, "0.0.0.0")) { 35 | callStr := f.ASTString(call); 36 | f.Reportf(node.Pos(), "audit binding network listener to all interfaces: %s", callStr); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | return; 46 | } 47 | -------------------------------------------------------------------------------- /closer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "go/ast" 7 | ) 8 | 9 | func init() { 10 | register("closeCheck", 11 | "this tests if things with .Close() method have .Close() actually called on them", 12 | closeCheck, 13 | funcDecl) 14 | } 15 | 16 | func opensFile(f *File, x ast.Expr) bool { 17 | /* 18 | if(f.pkg.info.TypeOf(x) == nil) { 19 | // should probably print something out here to notify the user 20 | return false; 21 | } 22 | */ 23 | /* 24 | if(f.pkg.info.TypeOf(x).String() == "(*os.File, error)") { 25 | return true 26 | } 27 | */ 28 | if typeValue := f.pkg.info.TypeOf(x); typeValue != nil { 29 | if typeValue.String() == "(*os.File, error)" { 30 | return true; 31 | } 32 | } 33 | return false; 34 | } 35 | 36 | // closesFile checks the remaining statements in a function body for a .Close() method 37 | func closesFile(f *File, stmts []ast.Stmt) bool { 38 | for _, stmt := range stmts { 39 | switch expr := stmt.(type) { 40 | case *ast.AssignStmt: 41 | rhs := expr.Rhs; 42 | for _, x := range rhs { 43 | name, err := getFullFuncName(x); 44 | if err != nil { 45 | warnf("issue, %v", err); 46 | } 47 | if(name == "file/Close") { 48 | return true 49 | } 50 | } 51 | case *ast.ExprStmt: 52 | name, err := getFullFuncName(expr.X); 53 | if err != nil { 54 | warnf("issue, %v", err); 55 | } 56 | if(name == "file/Close") { 57 | return true 58 | } 59 | } 60 | } 61 | return false 62 | } 63 | 64 | // for the time being this just checks a function to see if an opened file is closed 65 | // http.MaxBytesReader should also be checked for a close 66 | func closeCheck(f *File, node ast.Node) { 67 | var formatString string = "Audit for Close() method called on opened file, %s" 68 | // loop through block 69 | // look for file open 70 | // look for file close 71 | // if no file close, report 72 | // consider checking if the opened file is returned 73 | // consider checking if an open file is an input and no file is returned 74 | // it turns out you can open a file and not use it. What then? 75 | // ugh I really hate this 76 | // is walking the statements a better option? 77 | if fun, ok := node.(*ast.FuncDecl); ok { 78 | // todo: check that this prevents crashes 79 | if (fun.Body == nil || fun.Body.List == nil) { return; } 80 | for i, stmts := range fun.Body.List { 81 | switch stmt := stmts.(type) { 82 | case *ast.AssignStmt: 83 | rhs := stmt.Rhs; 84 | for _, x := range rhs { 85 | if(opensFile(f, x)) { 86 | if(!closesFile(f, fun.Body.List[i:])) { 87 | f.Reportf(stmt.Pos(), formatString,f.ASTString(x)) 88 | } 89 | } 90 | } 91 | case *ast.ExprStmt: 92 | if(opensFile(f, stmt.X)) { 93 | if(!closesFile(f, fun.Body.List[i:])) { 94 | f.Reportf(stmt.Pos(), formatString, f.ASTString(stmt.X)) 95 | } 96 | } 97 | case *ast.IfStmt: 98 | if s, ok := stmt.Init.(*ast.AssignStmt); ok { 99 | rhs := s.Rhs; 100 | for _, x := range rhs { 101 | if(opensFile(f, x )) { 102 | if(!closesFile(f, fun.Body.List[i:])) { 103 | f.Reportf(stmt.Pos(), formatString, f.ASTString(x)) 104 | } 105 | } 106 | } 107 | } 108 | default: 109 | // do nothing for time being 110 | } 111 | } 112 | } 113 | return; 114 | } 115 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "go/ast" 7 | "go/types" 8 | "strings" 9 | ) 10 | 11 | func init() { 12 | register("error", 13 | "this tests to see if any errors were ignored", 14 | errorCheck, 15 | assignStmt, 16 | exprStmt) 17 | } 18 | 19 | // isPrint checks to see if the call is a print statement 20 | // this is used because people normally don't care about 21 | // print statement errors 22 | // todo: this could be more rigorous. What if users invent 23 | // their own print statement that could be damaging if errors 24 | // are ignored? Consider also getting the full name and checking 25 | // if the print statement is from the fmt package. 26 | // Or maybe do exact matches for print statement methods in fmt. 27 | // i.e. Println; 28 | func isPrint(call *ast.CallExpr) bool { 29 | name := getFuncName(call); 30 | name = strings.ToLower(name); 31 | 32 | return strings.Contains(name, "print"); 33 | } 34 | 35 | func returnsError(f *File, call *ast.CallExpr) int { 36 | if typeValue := f.pkg.info.TypeOf(call); typeValue != nil { 37 | switch t := typeValue.(type) { 38 | case *types.Tuple: 39 | for i := 0; i < t.Len(); i++ { 40 | variable := t.At(i) 41 | if variable != nil && variable.Type().String() == "error" { 42 | return i; 43 | } 44 | } 45 | case *types.Named: 46 | if t.String() == "error"{ 47 | return 0; 48 | } 49 | } 50 | } 51 | return -1; 52 | } 53 | 54 | // Possibly check if anything returns an error before running the test 55 | // however, this may take roughly the same amount of effort as 56 | // just running the test in the first place. 57 | // 58 | func errorCheck(f *File, node ast.Node) { 59 | switch stmt := node.(type) { 60 | case *ast.AssignStmt: 61 | for _, rhs := range stmt.Rhs { 62 | if call, ok := rhs.(*ast.CallExpr); ok { 63 | index := returnsError(f, call) 64 | if index < 0 { 65 | continue 66 | } 67 | // ignore print calls unless verbose 68 | if isPrint(call) { 69 | if(!(*verbose)) { 70 | continue; 71 | } 72 | } 73 | lhs := stmt.Lhs[index] 74 | if id, ok := lhs.(*ast.Ident); ok && id.Name == "_" { 75 | // todo real reporting 76 | re := f.ASTString(rhs); 77 | le := f.ASTString(lhs); 78 | f.Reportf(stmt.Pos(), "error ignored %s %s", le, re); 79 | } 80 | } 81 | } 82 | case *ast.ExprStmt: 83 | if expr, ok := stmt.X.(*ast.CallExpr); ok { 84 | pos := returnsError(f, expr); 85 | if pos >= 0 { 86 | // todo: real reporting 87 | // ignore print statements unless verbose 88 | if isPrint(expr) { 89 | if(!(*verbose)) { 90 | return; 91 | } 92 | } 93 | x := f.ASTString(expr); 94 | f.Reportf(stmt.Pos(), "error ignored %s", x); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | ) 9 | 10 | func init() { 11 | register("exec", 12 | "this checks for use of exec Command", 13 | execCheck, 14 | callExpr) 15 | } 16 | 17 | func execCheck(f *File, node ast.Node) { 18 | // todo: dry this out. The function name extraction needs to be moved 19 | // as similar code appears elsewhere; 20 | if call, ok := node.(*ast.CallExpr); ok { 21 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 22 | // SelectorExpr has two fields 23 | // X and Sel 24 | // X (through reflection) was found to be an Ident 25 | // Idents have a field Name also 26 | if id, ok := (fun.X).(*ast.Ident); ok { 27 | if(id.Name == "exec") && (fun.Sel.Name == "Command") { 28 | callStr := f.ASTString(call); 29 | f.Reportf(node.Pos(), "audit use of os/exec package: %s", callStr); 30 | } 31 | } 32 | } 33 | } 34 | return; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /hardcoded.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "go/token" 9 | "encoding/hex" 10 | "encoding/base64" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | func init() { 16 | register("hardcoded", 17 | "this is a test to look for suspected hardcoded credentials", 18 | hardcodedCheck, 19 | assignStmt, genDecl) 20 | } 21 | 22 | 23 | // isHighEntropy checks for string encoding to 24 | // properly decode it and then measures its entropy 25 | func isHighEntropy(s *string) bool { 26 | // entropyN is normalised entropy 27 | var entropy, entropyN float64; 28 | 29 | // if there is an error decoding, it could 30 | // be that the string matched the regular expression 31 | // for that encoding type but does not properly decode. 32 | // So, these reverse the normal situation. 33 | // if there isn't an error, it won't fallthrough; 34 | switch { 35 | case isHex(*s): 36 | buf, err := hex.DecodeString(*s); 37 | if err == nil { 38 | entropy, entropyN = H(buf); 39 | break; 40 | } 41 | fallthrough; 42 | case isBase64(*s): 43 | buf, err := base64.StdEncoding.DecodeString(*s); 44 | if err == nil { 45 | entropy, entropyN = H(buf); 46 | break; 47 | } 48 | fallthrough; 49 | default: 50 | buf := []byte(*s); 51 | entropy, entropyN = H(buf); 52 | } 53 | // these values, 2.5 and .98 are set through experiment 54 | // consider changing these or being more rigorous here 55 | if((entropy > 2.5) && (entropyN > .98)) { 56 | return true; 57 | } 58 | return false; 59 | } 60 | 61 | // isCommonCred checks for commonly used credentials 62 | // This check doesn't try to be exhaustive. 63 | func isCommonCred(s *string) bool { 64 | credPatterns := []string{"password", "p4ssword", "123456", "letmein", "admin", "abc123", "passw0rd", "pwd"}; 65 | val := strings.ToLower(*s); 66 | 67 | for _, str := range credPatterns { 68 | re := regexp.MustCompile(str); 69 | if matches := re.MatchString(val); matches { 70 | return true; 71 | } 72 | } 73 | return false; 74 | } 75 | 76 | // checkSuspectVal runs essentially the same checks on a suspect value 77 | // to see if it may be a credential. 78 | func checkSuspectVal(f *File, basicLit *ast.BasicLit) { 79 | // strip quotes from input 80 | suspectVal := basicLit.Value; 81 | // todo: is this the best way to handle this? 82 | // under certain conditions, the below slice assignment contains 83 | // bounds that are out of range 84 | if(string(suspectVal[0]) == `"`) { 85 | suspectVal = suspectVal[1: len(suspectVal) -1]; 86 | } 87 | // now check suspectVal 88 | if isCommonCred(&suspectVal) { 89 | f.Reportf(basicLit.Pos(), "Possible credential found: %s", suspectVal); 90 | return; 91 | } 92 | if isHighEntropy(&suspectVal) { 93 | f.Reportf(basicLit.Pos(), "Possible credential found: %s", suspectVal); 94 | return; 95 | } 96 | return; 97 | } 98 | 99 | // checkAssignStmt starts with an AssignStmt, checks if 100 | func checkAssignStmt(f *File, node *ast.AssignStmt) { 101 | for _, expr := range node.Rhs { 102 | if basicLit, ok := expr.(*ast.BasicLit); ok { 103 | checkSuspectVal(f, basicLit); 104 | } 105 | } 106 | } 107 | 108 | // checkGenDecl starts with a GenDecl, checks if it is a const or a var 109 | // then goes through its specs, then converts them to ValueSpecs 110 | // then takes the Exprs from the ValueSpec and converts those to 111 | // BasicLits and then finally takes the actual string values for testing 112 | // todo: could add check on the variable names first but this is debatable 113 | func checkGenDecl(f *File, node *ast.GenDecl) { 114 | if (node.Tok == token.CONST || node.Tok == token.VAR) { 115 | for _, spec := range node.Specs { 116 | if valSpec, ok := spec.(*ast.ValueSpec); ok { 117 | for _, expr := range valSpec.Values { 118 | if basicLit, ok := expr.(*ast.BasicLit); ok { 119 | checkSuspectVal(f, basicLit); 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | func hardcodedCheck(f *File, node ast.Node) { 128 | switch t := node.(type) { 129 | case *ast.AssignStmt: 130 | checkAssignStmt(f, t); 131 | case *ast.GenDecl: 132 | checkGenDecl(f, t); 133 | } 134 | return; 135 | } 136 | 137 | -------------------------------------------------------------------------------- /insecureCrypto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "strings" 9 | ) 10 | 11 | func init() { 12 | register("insecureCrypto", 13 | "this test checks for insecure cryptography primitives", 14 | cryptoCheck, 15 | fileNode) 16 | } 17 | 18 | func insecureCalls() map[string]bool { 19 | calls := make(map[string]bool) 20 | calls["crypto/des"] = true 21 | calls["crypto/md5"] = true 22 | calls["crypto/rc4"] = true 23 | calls["crypto/sha1"] = true 24 | 25 | return calls; 26 | } 27 | 28 | // getImports returns all the imports for a full file AST node 29 | // todo: consider moving this out somewhere to a helper function to extract imports 30 | func getImports(fn *ast.File) []string { 31 | var imported []string 32 | for _, pkg := range fn.Imports { 33 | // trim parantheses 34 | pkgName := strings.Trim(pkg.Path.Value, "\""); 35 | imported = append(imported, pkgName); 36 | } 37 | return imported; 38 | } 39 | 40 | func cryptoCheck(f *File, node ast.Node) { 41 | var imported []string; 42 | insecure := insecureCalls(); 43 | 44 | if fileNode, ok := node.(*ast.File); ok { 45 | imported = getImports(fileNode); 46 | } 47 | for _, call := range imported { 48 | if _, ok := insecure[call]; ok { 49 | f.Reportf(node.Pos(), "insecure cryptographic import: %s", call); 50 | } 51 | } 52 | return; 53 | } 54 | -------------------------------------------------------------------------------- /insecureRand.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | ) 9 | 10 | func init() { 11 | register("insecureRand", 12 | "this is test to check if random nums generated insecurely", 13 | randCheck, 14 | fileNode) 15 | } 16 | 17 | func randCheck(f *File, node ast.Node) { 18 | var imported []string 19 | 20 | if fileNode, ok := node.(*ast.File); ok { 21 | imported = getImports(fileNode); 22 | } 23 | 24 | for _, pkg := range imported { 25 | if(pkg == "math/rand") { 26 | f.Reportf(node.Pos(), "audit the use of insecure random number generator: import: %s", pkg); 27 | } 28 | } 29 | return; 30 | } 31 | -------------------------------------------------------------------------------- /intToStr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "go/token" 9 | ) 10 | 11 | func init() { 12 | register("intToStr", 13 | "check if integers are being converted to strings using string()", 14 | intToStrCheck, 15 | callExpr) 16 | } 17 | 18 | func intToStrCheck(f *File, node ast.Node) { 19 | formatString := "integer possibly converted improperly: %s"; 20 | if stmt, ok := node.(*ast.CallExpr); ok { 21 | // technically, string() is not a function but a type conversion 22 | name := getFuncName(stmt); 23 | if(name == "string") { 24 | // length of args to string() is only 1 25 | if(len(stmt.Args) == 1) { 26 | switch arg := stmt.Args[0].(type) { 27 | case *ast.Ident: 28 | if t := f.pkg.info.TypeOf(arg); t != nil { 29 | // is this really the best way to check? 30 | if(t.String() == "int") { 31 | str := f.ASTString(stmt); 32 | f.Reportf(stmt.Pos(), formatString, str); 33 | } 34 | } 35 | case *ast.BasicLit: 36 | if(arg.Kind == token.INT) { 37 | str := f.ASTString(stmt); 38 | f.Reportf(stmt.Pos(), formatString, str); 39 | } 40 | case *ast.CallExpr: 41 | if t := f.pkg.info.TypeOf(arg); t != nil { 42 | if(t.String() == "int") { 43 | str := f.ASTString(stmt); 44 | f.Reportf(stmt.Pos(), formatString, str); 45 | } 46 | } 47 | default: 48 | // todo: figure out what the other cases here are 49 | // ast.CompositeLit, ast.SliceExpr, ast.IndexExpr 50 | // ast.StartExpr, ast.SelectorExpr 51 | // ast.TypeAssertExpr, ast.BinaryExpr 52 | } 53 | } 54 | } 55 | } else { 56 | warnf("something strange happened at %s, please report", f.loc(stmt.Pos()) ); 57 | } 58 | return; 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "flag" 6 | "go/ast" 7 | "go/build" 8 | "go/token" 9 | "go/parser" 10 | "go/printer" 11 | "go/types" 12 | "go/importer" 13 | "bytes" 14 | "strings" 15 | "os" 16 | "path/filepath" 17 | 18 | "golang.org/x/tools/go/callgraph" 19 | "golang.org/x/tools/go/loader" 20 | "golang.org/x/tools/go/pointer" 21 | "golang.org/x/tools/go/ssa" 22 | "golang.org/x/tools/go/ssa/ssautil" 23 | ) 24 | 25 | var stdImporter types.Importer 26 | 27 | var ( 28 | source = flag.Bool("source", false, "import from source instead of compiled object files") 29 | verbose = flag.Bool("verbose", false, "verbose logging and warnings") 30 | test = flag.Bool("test", false, "run checker on test files") 31 | ) 32 | 33 | // a global variable for the exit code. 34 | var exitCode = 0; 35 | 36 | var report = make(map[string]bool); 37 | 38 | var ( 39 | // shortens type names 40 | // These are the relevant AST node types to check 41 | // with corresponding cases 42 | assignStmt *ast.AssignStmt 43 | binaryExpr *ast.BinaryExpr 44 | callExpr *ast.CallExpr 45 | compositeLit *ast.CompositeLit 46 | exprStmt *ast.ExprStmt 47 | fileNode *ast.File 48 | forStmt *ast.ForStmt 49 | funcDecl *ast.FuncDecl 50 | funcLit *ast.FuncLit 51 | genDecl *ast.GenDecl 52 | interfaceType *ast.InterfaceType 53 | rangeStmt *ast.RangeStmt 54 | returnStmt *ast.ReturnStmt 55 | structType *ast.StructType 56 | ) 57 | 58 | var ( 59 | // checkers is a map to a map 60 | // the map maps AST types to maps of checker names to checker functions 61 | // this is to first get the functions needed for a certain type 62 | // and second to take just the functions we want to run. 63 | // refactor this to map to struct 64 | checkers = make(map[ast.Node]map[string]func(*File, ast.Node)) 65 | 66 | ) 67 | 68 | // A map 69 | // File is a visitor type for the parse tree. 70 | // it also contains the corresponding AST to a parsed file 71 | // pkg contains data on the entire package that was parsed 72 | // this includes things like type info so you can spot 73 | // an expression, like a func call, and look up it's type 74 | type File struct { 75 | pkg *Package 76 | fset *token.FileSet 77 | name string 78 | file *ast.File 79 | 80 | b bytes.Buffer // used for logging and printing results 81 | 82 | // a map of all registered checkers to run for each node 83 | checkers map[ast.Node][]func(*File, ast.Node); 84 | } 85 | 86 | // Reportf reports issues to a log for each file for later printing 87 | func (f *File) Reportf(pos token.Pos, format string, args ...interface{}) { 88 | // update this to use a logger 89 | fmt.Fprintf(os.Stderr, "\t* %v %s \n", f.loc(pos), fmt.Sprintf(format, args...)); 90 | } 91 | 92 | // loc (line of code) returns a formatted string of file and a file position 93 | func (f *File) loc(pos token.Pos) string { 94 | if pos == token.NoPos { 95 | return "" 96 | } 97 | // we won't print column, just line 98 | posn := f.fset.Position(pos) 99 | return fmt.Sprintf("%s:%d", posn.Filename, posn.Line); 100 | } 101 | 102 | // warnf is a formatted error printer that does not exit 103 | // but it does set an exit code. 104 | func warnf(format string, args ...interface{}) { 105 | fmt.Fprintf(os.Stderr, "Glasgo: "+format+"\n", args...); 106 | exitCode = 1; 107 | } 108 | 109 | // register registers the named checker function 110 | // to be called with AST nodes of the given types. 111 | func register(name, usage string, fn func(*File, ast.Node), types ...ast.Node) { 112 | report[name] = true; 113 | for _, typ := range types { 114 | m, ok := checkers[typ]; 115 | if !ok { 116 | m = make(map[string]func(*File, ast.Node)); 117 | checkers[typ] = m; 118 | } 119 | m[name] = fn; 120 | } 121 | } 122 | 123 | // Visit implements the visitor interface we need to walk the tree 124 | // ast.Walk calls v.Visit(node) 125 | func (f *File) Visit(node ast.Node) ast.Visitor { 126 | var key ast.Node 127 | switch node.(type) { 128 | case *ast.AssignStmt: 129 | key = assignStmt 130 | case *ast.BinaryExpr: 131 | key = binaryExpr 132 | case *ast.CallExpr: 133 | key = callExpr 134 | case *ast.CompositeLit: 135 | key = compositeLit 136 | case *ast.ExprStmt: 137 | key = exprStmt 138 | case *ast.File: 139 | key = fileNode 140 | case *ast.ForStmt: 141 | key = forStmt 142 | case *ast.FuncDecl: 143 | key = funcDecl 144 | case *ast.FuncLit: 145 | key = funcLit 146 | case *ast.GenDecl: 147 | key = genDecl 148 | case *ast.InterfaceType: 149 | key = interfaceType 150 | case *ast.RangeStmt: 151 | key = rangeStmt 152 | case *ast.ReturnStmt: 153 | key = returnStmt 154 | case *ast.StructType: 155 | key = structType 156 | } 157 | // runs checkers below 158 | for _, fn := range f.checkers[key] { 159 | fn(f, node) 160 | } 161 | return f; 162 | } 163 | 164 | type Package struct { 165 | path string 166 | types map[ast.Expr]types.TypeAndValue; 167 | typePkg *types.Package 168 | info *types.Info 169 | 170 | lp *loader.Program 171 | ssaProg *ssa.Program 172 | ssaPkg []*ssa.Package // SSA packages 173 | cGraph *callgraph.Graph 174 | } 175 | 176 | func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) error { 177 | if stdImporter == nil { 178 | if *source { 179 | stdImporter = importer.For("source", nil) 180 | } else { 181 | stdImporter = importer.Default(); 182 | } 183 | } 184 | pkg.types = make(map[ast.Expr]types.TypeAndValue); 185 | 186 | conf := types.Config{ 187 | Importer: stdImporter, 188 | Error: func(err error) { 189 | // todo refactor this 190 | if *verbose { 191 | fmt.Printf("\tWarning: during type checking, %v\n", err) 192 | } 193 | }, 194 | } 195 | 196 | info := types.Info{ 197 | Types: pkg.types, 198 | } 199 | 200 | // Type-Check the package. 201 | typePkg, err := conf.Check(pkg.path, fs, astFiles, &info); 202 | pkg.typePkg = typePkg 203 | pkg.info = &info; 204 | return err; 205 | 206 | } 207 | 208 | // checkPackageDir extracts the go files from a directory and passes them to 209 | // checkPackage for analysis 210 | func checkPackageDir(directory string) { 211 | context := build.Default 212 | // gets build tags if any exist in order to preserve them through the coming import 213 | /* 214 | these are commented out until proof is made of being necessary 215 | if len(context.BuildTags) != 0 { 216 | warnf("build tags already set: %s," context.BuildTags); 217 | } 218 | context.BuildTags = append(tagList, context.BuildTags...); 219 | */ 220 | 221 | pkg, err := context.ImportDir(directory, 0); // 0 means no ImportMode is set i.e. default 222 | if err != nil { 223 | // no go source files 224 | if _, noGoSource := err.(*build.NoGoError); noGoSource { 225 | return; 226 | } 227 | // not considered fatal because we are recursively walking directories 228 | if *verbose { 229 | warnf("error processing directory %s, %s", directory, err); 230 | } 231 | return; 232 | } 233 | var names []string 234 | names = append(names, pkg.GoFiles...); 235 | names = append(names, pkg.CgoFiles...); 236 | // don't check test files by default 237 | if *test { 238 | names = append(names, pkg.TestGoFiles...); 239 | } 240 | /* there are other types include binary files that can be added */ 241 | 242 | /* prefix each file with the directory name 243 | * could use a refactor 244 | */ 245 | if directory != "." { 246 | for i, name := range names{ 247 | names[i] = filepath.Join(directory, name); 248 | } 249 | } 250 | checkPackage(names); 251 | } 252 | 253 | // checkPackage runs analysis on all named files in a package. 254 | // It parses and then runs the analysis. 255 | // It returns the parsed package or nil. 256 | func checkPackage(names []string) { 257 | var files []*File; 258 | var astFiles []*ast.File; 259 | fset := token.NewFileSet(); 260 | var err error; 261 | for _, name := range names { 262 | // skipping using ioutil to read the file data 263 | // and just going to parse files directly. 264 | var parsedFile *ast.File; 265 | if strings.HasSuffix(name, ".go") { 266 | parsedFile, err = parser.ParseFile(fset, name, nil, parser.ParseComments) 267 | if err != nil { 268 | // warn but continue 269 | if *verbose { 270 | warnf("error: %s: %s", name, err); 271 | } 272 | return; 273 | } 274 | astFiles = append(astFiles, parsedFile); 275 | } 276 | file := &File{ 277 | fset: fset, 278 | name: name, 279 | file: parsedFile, 280 | } 281 | files = append(files, file); 282 | } 283 | if len(astFiles) == 0 { 284 | return; 285 | } 286 | pkg := new(Package); 287 | 288 | // Type check package and 289 | // generate information about it 290 | // Check. 291 | err = pkg.check(fset, astFiles); 292 | if err != nil { 293 | // probably should just keep going 294 | // fmt.Printf("exited, %v", err); 295 | //os.Exit(0); 296 | // errors being caught in different location. 297 | } 298 | 299 | // Attempt to load program 300 | // todo: add errors and handle them 301 | //loadSSA(fset, astFiles, pkg); 302 | prog, _ := loadProgram(fset, astFiles); 303 | if prog != nil { 304 | // check for errors 305 | pkg.lp = prog; 306 | pkg.ssaProg = ssautil.CreateProgram(prog, 0); 307 | pkg.ssaProg.Build(); 308 | pkg.ssaPkg = getMainFns(prog, pkg.ssaProg); 309 | } 310 | 311 | if len(pkg.ssaPkg) != 0 { 312 | res, err := pointer.Analyze(&pointer.Config{ 313 | Mains: pkg.ssaPkg, 314 | BuildCallGraph: true, 315 | }); 316 | if err != nil { 317 | // print a warning? 318 | warnf("in pointer analysis, %v", err); 319 | } 320 | if res != nil { 321 | pkg.cGraph = res.CallGraph; 322 | } 323 | } 324 | 325 | for _, file := range files { 326 | file.pkg = pkg; 327 | } 328 | 329 | 330 | chk := make(map[ast.Node][]func(*File, ast.Node)); 331 | for typ, set := range checkers { 332 | for name, fn := range set { 333 | // check to see if named function will be run and reported 334 | _, ok := report[name]; 335 | if ok { 336 | chk[typ] = append(chk[typ], fn); 337 | } 338 | } 339 | } 340 | for _, file := range files { 341 | file.checkers = chk 342 | if file.file != nil { 343 | // Should this go in to a new function to make it more readable? 344 | // file.walkFile(file.name, file.file) as a method? 345 | if(*verbose) { 346 | fmt.Printf("Checking %s\n", file.name); 347 | } 348 | ast.Walk(file, file.file); 349 | } 350 | } 351 | } 352 | 353 | // loadProgram loads a program from the parsed files 354 | func loadProgram(fset *token.FileSet, astFiles []*ast.File) (*loader.Program, error) { 355 | var conf loader.Config; 356 | 357 | conf.AllowErrors = true; 358 | conf.Fset = fset; 359 | conf.CreateFromFiles(".", astFiles...); 360 | 361 | conf.TypeChecker.Error = func(error) { 362 | // do nothing 363 | // repeated above 364 | // todo: merge type checkers (can this not be done twice?) 365 | // todo: consider printing something (although it will be repeated) 366 | } 367 | 368 | // errors are not returned because AllowErrors set to true above 369 | // todo: get all the errors from "PackageInfo" and print them 370 | // here or elsewhere 371 | // see documentation to see how to do this 372 | prog, _ := conf.Load(); 373 | 374 | return prog, nil; 375 | } 376 | 377 | func getMainFns(prog *loader.Program, ssaProg *ssa.Program) []*ssa.Package { 378 | ips := prog.InitialPackages(); 379 | mains := make([]*ssa.Package, 0, len(ips)); 380 | for _, info := range ips { 381 | ssaPkg := ssaProg.Package(info.Pkg); 382 | if ssaPkg != nil && ssaPkg.Func("main") != nil { 383 | mains = append(mains, ssaPkg); 384 | } 385 | } 386 | return mains; 387 | } 388 | 389 | // visit is for walking input directory roots 390 | func visit(path string, info os.FileInfo, err error) error { 391 | if err != nil { 392 | warnf("directory walk error: %s", err); 393 | return err; 394 | } 395 | // make sure we are only dealing with directories here 396 | if !info.IsDir() { 397 | return nil 398 | } 399 | checkPackageDir(path); 400 | return nil; 401 | } 402 | 403 | // ASTString returns a string representation of the AST for reporting 404 | func (f *File) ASTString(x ast.Expr) string { 405 | var b bytes.Buffer 406 | printer.Fprint(&b, f.fset, x); 407 | return b.String() 408 | } 409 | 410 | // getFuncName returns just function name i.e. not ioutil.ReadAll but just ReadAll 411 | // not returning errors, 412 | func getFuncName(node ast.Node) string { 413 | if call, ok := node.(*ast.CallExpr); ok { 414 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 415 | if(fun.Sel.Name != "") { 416 | return fun.Sel.Name; 417 | } 418 | } 419 | if fun, ok := call.Fun.(*ast.Ident); ok { 420 | if(fun.Name != "") { 421 | return fun.Name; 422 | } 423 | } 424 | } 425 | return "" 426 | } 427 | 428 | // getFullFuncName extracts a full function name path i.e ioutil.ReadAll 429 | func getFullFuncName(node ast.Node) (string, error) { 430 | var names []string 431 | var callName string 432 | if call, ok := node.(*ast.CallExpr); ok { 433 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 434 | // fmt.Println(fun.X); 435 | // I think the above can be removed 436 | // SelectorExpr has two fields 437 | // X and Sel 438 | // X (through reflection) was found to be an Ident 439 | // Sel has field Name 440 | // Ident's have a field Name also. 441 | if id, ok := (fun.X).(*ast.Ident); ok { 442 | names = append(names, id.Name); 443 | names = append(names, fun.Sel.Name); 444 | callName = strings.Join(names, "/") 445 | return callName, nil 446 | } 447 | } 448 | } 449 | return "", fmt.Errorf("type conversion of CallExpr failed, no name extracted, %v", node); 450 | } 451 | 452 | func main() { 453 | var runOnDirs, runOnFiles bool; 454 | flag.Parse(); 455 | 456 | for _, name := range flag.Args() { 457 | // check to see if cl argument is a directory 458 | f, err := os.Stat(name); 459 | if err != nil { 460 | warnf("error: %s", err); 461 | continue; 462 | } 463 | if f.IsDir() { 464 | runOnDirs = true; 465 | } else { 466 | runOnFiles = true; 467 | } 468 | } 469 | if runOnDirs && runOnFiles { 470 | // print an error 471 | fmt.Println("error: input arguments must not be both directories and files"); 472 | exitCode = 1; 473 | os.Exit(exitCode); 474 | } 475 | if runOnDirs { 476 | // I want to do each directory in order 477 | // so I am going to loop through these regardless 478 | // root is a name of a directory, at the root, to be walked 479 | for _, root := range flag.Args() { 480 | filepath.Walk(root, visit); 481 | } 482 | os.Exit(exitCode); 483 | } 484 | // else they are just file names 485 | fileNames := flag.Args(); 486 | checkPackage(fileNames); 487 | return; 488 | } 489 | 490 | -------------------------------------------------------------------------------- /readAll.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "strings" 9 | ) 10 | 11 | func init() { 12 | register("readAll", 13 | "this tests checks of use of ioutil.ReadAll needs to be audited", 14 | readAllCheck, 15 | callExpr) 16 | } 17 | 18 | // this checks for the bad function 19 | // but maybe abstract this and create a function name extractor 20 | // then throw all the bad functions together or check for them 21 | // using it 22 | func readAllCheck(f *File, node ast.Node) { 23 | // the names of the called functions 24 | // new function getFullFuncName does all this work. 25 | // todo: replace 26 | var names []string 27 | var callName string 28 | if call, ok := node.(*ast.CallExpr); ok { 29 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 30 | // SelectorExpr has two fields 31 | // X and Sel 32 | // X (through reflection) was found to be an Ident 33 | // Sel has field Name 34 | // Ident's have a field Name also. 35 | if id, ok := (fun.X).(*ast.Ident); ok { 36 | names = append(names, id.Name); 37 | names = append(names, fun.Sel.Name); 38 | callName = strings.Join(names, "/") 39 | if(callName == "ioutil/ReadAll") { 40 | callStr := f.ASTString(call); 41 | f.Reportf(node.Pos(), "audit use of ioutil.ReadAll %s", callStr); 42 | } 43 | } 44 | } 45 | } 46 | return; 47 | } 48 | -------------------------------------------------------------------------------- /sql.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "go/types" 9 | 10 | "golang.org/x/tools/go/loader" 11 | "golang.org/x/tools/go/callgraph" 12 | "golang.org/x/tools/go/ssa" 13 | ) 14 | 15 | func init() { 16 | register("sql", 17 | "this test checks for non constant sql query strings", 18 | sqlCheck, 19 | fileNode) 20 | } 21 | 22 | type sqlPackage struct { 23 | packageName string 24 | argNames []string 25 | enabled bool 26 | pkg *types.Package 27 | } 28 | 29 | type SQLQuery struct { 30 | Func *types.Func 31 | SSA *ssa.Function 32 | ArgCount int 33 | Param int 34 | } 35 | 36 | var isCheckedSQL bool 37 | var SQLCheckFailed bool 38 | 39 | var sqlPackages = []sqlPackage{ 40 | { 41 | packageName: "database/sql", 42 | argNames: []string{"query"}, 43 | }, 44 | } 45 | 46 | // this may be called more than once, never do the same thing twice 47 | // todo: dry, this will likely be used in other places 48 | func getPkgImports(lp *loader.Program) map[string]*types.Package { 49 | pkgs := make(map[string]*types.Package); 50 | for _, pkg := range lp.AllPackages { 51 | if pkg.Importable { 52 | pkgs[pkg.Pkg.Path()] = pkg.Pkg; 53 | } 54 | } 55 | return pkgs; 56 | } 57 | 58 | func GetQueries(sqlPackages sqlPackage, sqlPkg *types.Package, ssa *ssa.Program) []*SQLQuery { 59 | methods := make([]*SQLQuery, 0); 60 | scope := sqlPkg.Scope() 61 | for _, name := range scope.Names() { 62 | o := scope.Lookup(name); 63 | if !o.Exported() { 64 | continue; 65 | } 66 | if _, ok := o.(*types.TypeName); !ok { 67 | continue; 68 | } 69 | n := o.Type().(*types.Named); 70 | for i := 0; i < n.NumMethods(); i++ { 71 | m := n.Method(i); 72 | if !m.Exported() { 73 | continue; 74 | } 75 | sig := m.Type().(*types.Signature); 76 | if num, ok := FuncHasQuery(sqlPackages, sig); ok { 77 | methods = append(methods, &SQLQuery{ 78 | Func: m, 79 | SSA: ssa.FuncValue(m), 80 | ArgCount: sig.Params().Len(), 81 | Param: num, 82 | }); 83 | } 84 | } 85 | } 86 | return methods; 87 | } 88 | 89 | func FuncHasQuery(sqlPackages sqlPackage, sig *types.Signature) (int, bool) { 90 | params := sig.Params(); 91 | for i := 0; i < params.Len(); i++ { 92 | v := params.At(i); 93 | for _, paramName := range sqlPackages.argNames { 94 | if v.Name() == paramName { 95 | // i is offset 96 | return i, true; 97 | } 98 | } 99 | } 100 | return 0, false 101 | } 102 | 103 | func GetNonConstantCalls(cGraph *callgraph.Graph, queries []*SQLQuery) []ssa.CallInstruction { 104 | cGraph.DeleteSyntheticNodes(); 105 | 106 | suspected := make([]ssa.CallInstruction, 0); 107 | for _, m := range queries { 108 | node := cGraph.CreateNode(m.SSA); 109 | for _, edge := range node.In { 110 | 111 | isInternalSQLPkg := false 112 | for _, pkg := range sqlPackages { 113 | if pkg.packageName == edge.Caller.Func.Pkg.Pkg.Path() { 114 | isInternalSQLPkg = true; 115 | break 116 | } 117 | } 118 | if isInternalSQLPkg { 119 | continue 120 | } 121 | 122 | cc := edge.Site.Common() 123 | args := cc.Args; 124 | 125 | v := args[m.Param]; 126 | 127 | if _, ok := v.(*ssa.Const); !ok { 128 | if inter, ok := v.(*ssa.MakeInterface); ok && types.IsInterface(v.(*ssa.MakeInterface).Type()) { 129 | if inter.X.Referrers() == nil || inter.X.Type() != types.Typ[types.String] { 130 | continue; 131 | } 132 | } 133 | suspected = append(suspected, edge.Site); 134 | } 135 | } 136 | } 137 | 138 | return suspected; 139 | } 140 | 141 | func sqlCheck(f *File, node ast.Node) { 142 | if isCheckedSQL { 143 | return; 144 | } 145 | 146 | if (f.pkg.lp == nil) || (f.pkg.ssaProg == nil) || (f.pkg.cGraph == nil) { 147 | // skip this test 148 | // run a different one 149 | // mark the check as complete for now so this is not run again 150 | isCheckedSQL = true; 151 | SQLCheckFailed = true; 152 | warnf("unable to complete primary check for potential SQL injection"); 153 | return; 154 | } 155 | 156 | imports := getPkgImports(f.pkg.lp); 157 | if len(imports) == 0 { 158 | return; 159 | } 160 | 161 | isSqlImported := false; 162 | for i := range sqlPackages { 163 | if _, ok := imports[sqlPackages[i].packageName]; ok { 164 | sqlPackages[i].enabled = true; 165 | isSqlImported = true; 166 | sqlPackages[i].pkg = imports[sqlPackages[i].packageName]; 167 | } 168 | } 169 | if !isSqlImported { 170 | // maybe make a mention of not finding any SQL in use? 171 | // todo: see above 172 | return; 173 | } 174 | 175 | queries := make([]*SQLQuery, 0); 176 | 177 | for i := range sqlPackages { 178 | if sqlPackages[i].enabled { 179 | queries = append(queries, GetQueries(sqlPackages[i], sqlPackages[i].pkg, f.pkg.ssaProg)...); 180 | } 181 | } 182 | 183 | suspected := GetNonConstantCalls(f.pkg.cGraph, queries); 184 | 185 | for _, suspectCall := range suspected { 186 | f.Reportf(suspectCall.Pos(), "audit use of non-constant query: %s", suspectCall); 187 | } 188 | 189 | isCheckedSQL = true; 190 | 191 | return; 192 | } 193 | 194 | -------------------------------------------------------------------------------- /sql2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | //"fmt" 9 | "regexp" 10 | ) 11 | 12 | func init() { 13 | register("sqlBackup", 14 | "this is a backup test for the SQL injection test", 15 | sql2Check, 16 | funcDecl) 17 | } 18 | 19 | // May want to consider the possibility that someone gives db or Conn an alias 20 | // Consider also checking if the sql package was imported 21 | var expressions = []string{ 22 | "^((Conn.|db.))*(Exec)|(Query)$", 23 | } 24 | 25 | var regexps []*regexp.Regexp; 26 | 27 | func loadRegexps() { 28 | for _, expression := range expressions { 29 | rexp := regexp.MustCompile(expression); 30 | regexps = append(regexps, rexp); 31 | } 32 | } 33 | 34 | func isSQLCall(funcName string) bool { 35 | // move this somewhere else to be faster and only performed once 36 | loadRegexps(); 37 | 38 | for _, re := range regexps { 39 | if matches := re.MatchString(funcName); matches { 40 | return true; 41 | } 42 | } 43 | return false; 44 | } 45 | 46 | func checkTainted(expr *ast.Expr, tainted *map[string]bool) bool { 47 | switch exp := (*expr).(type) { 48 | case *ast.BinaryExpr: 49 | x := exp.X; 50 | y := exp.Y; 51 | // yes this is recursive but how deeply nested to people actually write assignments? 52 | return (checkTainted(&x, tainted) || checkTainted(&y, tainted)); 53 | case *ast.BasicLit: 54 | // if it is a constant, it is not tainted 55 | return false; 56 | case *ast.Ident: 57 | if(exp.Obj != nil) { 58 | varName := exp.Obj.Name; 59 | if _, isTainted := (*tainted)[varName]; isTainted { 60 | return true; 61 | } 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | func addTainted(exprs []ast.Expr, tainted *map[string]bool) { 68 | for _, expr := range exprs { 69 | if ident, ok := expr.(*ast.Ident); ok { 70 | (*tainted)[ident.Obj.Name] = true; 71 | } 72 | } 73 | } 74 | 75 | func sql2Check(f *File, node ast.Node) { 76 | // only run if the other SQL failed 77 | // todo: consider replacing the entirety of the other checker 78 | if(!SQLCheckFailed) { 79 | return; 80 | } 81 | 82 | tainted := make(map[string]bool); 83 | if fun, ok := node.(*ast.FuncDecl); ok { 84 | // perform a sanity check 85 | if(fun.Body == nil || len(fun.Body.List) < 1) { 86 | return; 87 | } 88 | 89 | // get input parameter names and types 90 | for _, field := range fun.Type.Params.List { 91 | // add params to tainted list 92 | if t, ok := field.Type.(*ast.Ident); ok { 93 | if(t.Name == "string") { 94 | for _, name := range field.Names { 95 | tainted[name.Name] = true; 96 | } 97 | } 98 | } 99 | } 100 | 101 | 102 | // get assignment statements and check if variables in assignments are tainted 103 | for _, statement := range fun.Body.List { 104 | if assign, ok := statement.(*ast.AssignStmt); ok { 105 | for _, expr := range assign.Rhs { 106 | switch exp := expr.(type) { 107 | case *ast.BinaryExpr, *ast.Ident: 108 | isTainted := checkTainted(&exp, &tainted); 109 | if(isTainted) { 110 | // add in variable name from lhs 111 | // right now it is just adding in all the Lhs 112 | addTainted(assign.Lhs, &tainted); 113 | } 114 | case *ast.BasicLit: 115 | case *ast.CallExpr: 116 | default: 117 | // do nothing for now 118 | // basic literals are safe 119 | // call expressions might need to be reconsidered 120 | } 121 | } 122 | // look for things that look like SQL query calls 123 | // also for variables to map 124 | for _, expr := range assign.Rhs { 125 | if call, ok := expr.(*ast.CallExpr); ok { 126 | funcName, err := getFullFuncName(call); 127 | if err != nil { 128 | // not sure what to do here 129 | } 130 | if(isSQLCall(funcName)) { 131 | // check if arguments are tainted 132 | for _, arg := range call.Args { 133 | // todo: should checkTainted just use value arguments not references? 134 | isTainted := checkTainted(&arg, &tainted); 135 | if(isTainted) { 136 | x := f.ASTString(expr); 137 | f.Reportf(expr.Pos(), "audit tainted input to SQL query, %s", x); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | // get things that are _probably_ sql exec calls 146 | if exprStmt, ok := statement.(*ast.ExprStmt); ok { 147 | if call, ok := exprStmt.X.(*ast.CallExpr); ok { 148 | var funcName string; 149 | funcName, err := getFullFuncName(call); 150 | if err != nil { 151 | funcName = getFuncName(call); 152 | } 153 | if(isSQLCall(funcName)) { 154 | // extract parameters of the call 155 | // check if they are tainted 156 | } 157 | } 158 | } 159 | } 160 | 161 | } 162 | return; 163 | } 164 | 165 | -------------------------------------------------------------------------------- /testdata/bind.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "crypto/tls" 6 | ) 7 | 8 | func bindAll() { 9 | tcpListener, err := net.Listen("tcp", "0.0.0.0:80"); 10 | tlsListener, err := tls.Listen("tcp", "0.0.0.0:443", nil); 11 | 12 | if err != nil { 13 | err = nil; 14 | } 15 | if(tcpListener == nil || tlsListener == nil) { 16 | tcpListener = nil; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testdata/constTest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const val = "testval"; 4 | 5 | func testConst() { 6 | return; 7 | } 8 | -------------------------------------------------------------------------------- /testdata/errors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "errors" 5 | ) 6 | 7 | // this function returns an error 8 | func retError1(a int) error { 9 | return errors.New("error 1"); 10 | } 11 | 12 | func retError2(a, b int) (int, error) { 13 | if(a < b) { 14 | return a, nil 15 | } 16 | return 0, errors.New("error 2"); 17 | } 18 | 19 | func retError3(a, b int) (int, error, int) { 20 | if(a < b) { 21 | return a, nil, b; 22 | } 23 | if(a == b) { 24 | return b, nil, a; 25 | } 26 | return 0, errors.New("error 3"), 0; 27 | } 28 | 29 | func retError4(a, b int) (int, int, error) { 30 | return a, b, errors.New("error 4"); 31 | } 32 | 33 | func errorsTest() int { 34 | var err error; 35 | var a, b int; 36 | 37 | // good 38 | err = retError1(1); 39 | 40 | // bad 41 | retError1(1); 42 | 43 | // good 44 | a, err = retError2(0,1); 45 | 46 | // bad 47 | retError2(0,1); 48 | 49 | // good 50 | a, err, b = retError3(0,1); 51 | 52 | // bad 53 | a, _, b = retError3(0, 1); 54 | 55 | // good 56 | a, b, err = retError4(0,1); 57 | 58 | // bad 59 | a, b, _ = retError4(0,1); 60 | 61 | if err != nil { 62 | return 1 63 | } else { 64 | return a + b 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /testdata/exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func executor() int { 8 | cmd := exec.Command("ls", "la"); 9 | err := cmd.Run(); 10 | if err != nil { 11 | return 1; 12 | } 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /testdata/hardcoded.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "fmt" 5 | ) 6 | 7 | const cred = "123password" 8 | const salt = "1f2606b6eec1654e" 9 | 10 | func hardcoded1(password string) bool { 11 | if (password == "password") { 12 | return true; 13 | } 14 | return false; 15 | } 16 | 17 | func hardcoded2() { 18 | var pwd string = "password"; 19 | var key string = "jb4HjJ8FV5j5d6XW"; 20 | var hash string = "c4a15dd62973f33c54bcc002d8ce5d517901053b"; 21 | var errorStr string = "Error: Not Found"; 22 | apiCred := "key123" 23 | var notACred1, notACred2, notACred3 string; 24 | notACred1 = "Error: undefined reference"; 25 | notACred2 = "Segmentation fault"; 26 | notACred3 = "error"; 27 | 28 | if hardcoded1(pwd) { 29 | fmt.Println(pwd); 30 | } 31 | if hardcoded1(key) { 32 | fmt.Println(2); 33 | } 34 | if hardcoded1(hash) { 35 | fmt.Println(2); 36 | } 37 | if hardcoded1("d99fce9480205c4b201fbc5fa80fd3232a4eefb6fba5cbefb5702d171ec14c33") { 38 | fmt.Println(3); 39 | } 40 | if hardcoded1(apiCred) { 41 | fmt.Println(apiCred); 42 | } 43 | 44 | fmt.Println(notACred1, notACred2, notACred3, errorStr); 45 | } 46 | -------------------------------------------------------------------------------- /testdata/importFail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "doesNotExist" 5 | "crypto/md5" 6 | ) 7 | 8 | func ImportFail() { 9 | doesNotExist.Stuff(); 10 | /* this check might seem redundant because 11 | * there is already crypto check 12 | * so the point is to make sure tests are still run 13 | * even after the import fails. 14 | */ 15 | h := md5.New(); 16 | if h != nil { 17 | return; 18 | } 19 | return 20 | } 21 | 22 | -------------------------------------------------------------------------------- /testdata/insecureCrypto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "crypto/des" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | ) 8 | 9 | func badCrypto() int { 10 | var key []byte; 11 | h := md5.New(); 12 | h = sha1.New(); 13 | c, err := des.NewTripleDESCipher(key); 14 | if err != nil { 15 | return 1; 16 | } 17 | if h != nil && c != nil { 18 | return 0; 19 | } 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /testdata/insecureTLS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | ) 6 | 7 | func insecureTLS() { 8 | config := &tls.Config{ 9 | InsecureSkipVerify: true, 10 | PreferServerCipherSuites: false, 11 | MinVersion: 0, 12 | MaxVersion: 1, 13 | CipherSuites: []uint16{0x000a, 0x0005, 0xc007, 0xc030}, 14 | }; 15 | 16 | tls.Dial("tcp", "mail.google.com:443", config); 17 | } 18 | -------------------------------------------------------------------------------- /testdata/int2strCon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func retInt() int { 4 | return 90; 5 | } 6 | 7 | func stringCon() string { 8 | var a int; 9 | a = 123; 10 | 11 | // bad 12 | b := string(a); 13 | 14 | // bad 15 | b = string(123) 16 | 17 | // bad 18 | c := string(80) 19 | 20 | // bad 21 | c = string(retInt()) 22 | 23 | b = c 24 | 25 | return b 26 | } 27 | -------------------------------------------------------------------------------- /testdata/ioutilReadAll.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "io/ioutil" 5 | "strings" 6 | ) 7 | 8 | func testReadAll() string { 9 | r := strings.NewReader("this is a test for use of ioutil.ReadAll"); 10 | 11 | b, err := ioutil.ReadAll(r) 12 | if err != nil { 13 | return "" 14 | } 15 | return string(b) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/noClose.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "os" 5 | ) 6 | 7 | func noClose() int { 8 | file, err := os.Open("noClose.go") 9 | os.Open("pristAST.go"); 10 | if err != nil { 11 | os.Exit(1) 12 | } 13 | if file != nil { 14 | return 0 15 | } 16 | if _, err := os.Open("printAST.go"); err != nil { 17 | return 1 18 | } 19 | return 0 20 | } 21 | -------------------------------------------------------------------------------- /testdata/rand.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "math/rand" 5 | ) 6 | 7 | func insecureRand() int { 8 | // there are many possible uses for math/rand 9 | // it's impractical to check for every possible use 10 | 11 | return rand.Int(); 12 | } 13 | -------------------------------------------------------------------------------- /testdata/sql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | var db *sql.DB; 8 | 9 | func sqlInject1(userInput string) { 10 | var args []string; 11 | 12 | _, _ = db.Exec(userInput, args); 13 | _, _ = db.Query(userInput, args); 14 | } 15 | 16 | func sqlInject2(userInput string) { 17 | var args []string; 18 | q := "SELECT user WHERE user ="; 19 | q = q + "test"; 20 | 21 | _, _ = db.Exec(q, args); 22 | } 23 | 24 | func main() { 25 | sqlInject1("test"); 26 | sqlInject2("test"); 27 | } 28 | -------------------------------------------------------------------------------- /testdata/textTemp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" 4 | import "text/template" 5 | 6 | func handler(w http.ResponseWriter, r *http.Request) { 7 | param1 := r.URL.Query().Get("param1") 8 | 9 | tmpl := template.New("hello") 10 | tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`) 11 | tmpl.ExecuteTemplate(w, "T", param1) 12 | } 13 | 14 | func textTemp() { 15 | http.HandleFunc("/", handler) 16 | http.ListenAndServe(":8080", nil) 17 | } 18 | -------------------------------------------------------------------------------- /testdata/unsafe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | 8 | func useUnsafe() uintptr { 9 | var i int; 10 | p := unsafe.Pointer(uintptr(i) + 0); 11 | 12 | return uintptr(p); 13 | } 14 | -------------------------------------------------------------------------------- /textTemp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // add a license 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | ) 9 | 10 | func init() { 11 | register("textTemp", 12 | "this is a test to see if template/text and http methods are in use", 13 | textTempCheck, 14 | fileNode) 15 | } 16 | 17 | func textTempCheck(f *File, node ast.Node) { 18 | importedPkgs := make(map[string]bool); 19 | if fileNode, ok := node.(*ast.File); ok { 20 | imports := getImports(fileNode); 21 | for _, imported := range imports { 22 | importedPkgs[imported] = true; 23 | } 24 | if a, b := importedPkgs["net/http"], importedPkgs["text/template"]; a && b { 25 | f.Reportf(node.Pos(), "audit use of text/template in HTTP responses"); 26 | } 27 | } 28 | 29 | return; 30 | } 31 | -------------------------------------------------------------------------------- /tlsConfig.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "strconv" 9 | ) 10 | 11 | // TLS is an acronym, therefore it should be in caps. It doesn't matter if it is exportable. 12 | func init() { 13 | register("TLSConfig", 14 | "this is a check for insecure TLS configuration", 15 | iTLSConfigCheck, 16 | compositeLit) 17 | } 18 | 19 | const ( 20 | VersionSSL30 = 0x0300 21 | VersionTLS10 = 0x0301 22 | VersionTLS11 = 0x0302 23 | VersionTLS12 = 0x0303 24 | ) 25 | 26 | var secureCiphers = []string { 27 | "TLS_RSA_WITH_AES_128_CBC_SHA", 28 | "TLS_RSA_WITH_AES_256_CBC_SHA", 29 | "TLS_RSA_WITH_AES_128_CBC_SHA256", 30 | "TLS_RSA_WITH_AES_128_GCM_SHA256", 31 | "TLS_RSA_WITH_AES_256_GCM_SHA384", 32 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 33 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 34 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 35 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 36 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 37 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 38 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 39 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 40 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 41 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 42 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", 43 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", 44 | "0x002f", "0x0035", "0x003c", "0x009c", "0x009d", "0xc009", "0xc00a", "0xc013", "0xc014", "0xc023", 45 | "0xc027", "0xc02f", "0xc02b", "0xc030", "0xc02c", "0xcca8", "0xcca9", 46 | } 47 | 48 | func sliceContains(s string, slice []string) bool { 49 | for _, str := range secureCiphers { 50 | if s == str { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | // checkTLSConfVal checks a key value expression from within a tls.Config composite element 58 | // the idea is to check the values of these things for bad ones 59 | func checkTLSConfVal(f *File, keyValueExpr *ast.KeyValueExpr) { 60 | // we are converting this down to identifiers where it is a 'basic' element type just in case 61 | if ident, ok := keyValueExpr.Key.(*ast.Ident); ok { 62 | switch ident.Name { 63 | 64 | case "InsecureSkipVerify": 65 | if val, ok := keyValueExpr.Value.(*ast.Ident); ok { 66 | if (val.Name == "true") { 67 | f.Reportf(keyValueExpr.Pos(), "InsecureSkipVerify is enabled, %s", f.ASTString(keyValueExpr)); 68 | } 69 | } else { 70 | // value is not a basic identifier, so simple boolean values can't be checked 71 | f.Reportf(keyValueExpr.Pos(), "Audit use of InsecureSkipVerify, %s", f.ASTString(keyValueExpr)); 72 | } 73 | case "PreferServerCipherSuites": 74 | if val, ok := keyValueExpr.Value.(*ast.Ident); ok { 75 | if val.Name == "false" { 76 | f.Reportf(keyValueExpr.Pos(), "PreferServerCipherSuites set to false, %s", f.ASTString(keyValueExpr)); 77 | } 78 | } else { 79 | // can't be shown to be true; some sort of weird expression instead of simple true or false 80 | f.Reportf(keyValueExpr.Pos(), "Audit use of PreferServerCipherSuites, %s", f.ASTString(keyValueExpr)); 81 | } 82 | case "MinVersion": 83 | if val, ok := keyValueExpr.Value.(*ast.BasicLit); ok { 84 | i, err := strconv.Atoi(val.Value); 85 | if err == nil { 86 | if ((int16)(i) < VersionTLS10) { 87 | f.Reportf(keyValueExpr.Pos(), "TLS minimum version is outdated, %s", f.ASTString(keyValueExpr)); 88 | } 89 | } 90 | } 91 | case "MaxVersion": 92 | if val, ok := keyValueExpr.Value.(*ast.BasicLit); ok { 93 | i, err := strconv.Atoi(val.Value); 94 | if err == nil { 95 | if ((int16)(i) < VersionTLS11) { 96 | // todo: maybe reword this issue? 97 | f.Reportf(keyValueExpr.Pos(), "TLS maximum version is weak, %s", f.ASTString(keyValueExpr)); 98 | } 99 | } 100 | } 101 | case "CipherSuites": 102 | if val, ok := keyValueExpr.Value.(*ast.CompositeLit); ok { 103 | for _, elt := range val.Elts { 104 | if cipherLit, ok := elt.(*ast.BasicLit); ok { 105 | if !sliceContains(cipherLit.Value, secureCiphers) { 106 | f.Reportf(cipherLit.Pos(), "Weak cipher, %s, is in use", f.ASTString(cipherLit)); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | // iTLSConfigCheck checks TLS configuration structs. 116 | // i stands for insecure. It also prevents this from being exportable. 117 | func iTLSConfigCheck(f *File, node ast.Node) { 118 | if compLit, ok := node.(*ast.CompositeLit); ok && compLit.Type != nil { 119 | // elt stands for element, as in, composite element 120 | for _, elt := range compLit.Elts { 121 | if keyValueExpr, ok := elt.(*ast.KeyValueExpr); ok { 122 | // more succinct to call another function here 123 | checkTLSConfVal(f, keyValueExpr); 124 | } 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Terence Tarvis. All rights reserved. 2 | // 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | ) 9 | 10 | func init() { 11 | register("unsafe", 12 | "this checks for use of the unsafe package", 13 | unsafeCheck, 14 | callExpr) 15 | } 16 | 17 | func unsafeCheck(f *File, node ast.Node) { 18 | // todo: dry this out. The function name extraction needs to be moved 19 | // as similar code appears elsewhere 20 | if call, ok := node.(*ast.CallExpr); ok { 21 | if fun, ok := call.Fun.(*ast.SelectorExpr); ok { 22 | // SelectorExpr has two fields 23 | // X and Sel 24 | // X (through reflection) was found to be an Ident 25 | // Ident's have a field Name also. 26 | if id, ok := (fun.X).(*ast.Ident); ok { 27 | // todo: this test could be more rigorous but may be sufficient 28 | if(id.Name == "unsafe") { 29 | callStr := f.ASTString(call); 30 | f.Reportf(node.Pos(), "audit use of unsafe package: %s", callStr); 31 | } 32 | } 33 | } 34 | } 35 | return; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "math" 5 | "regexp" 6 | ) 7 | 8 | // computes Shannon Entropy, H. Note this is 9 | // computed with a slice of bytes, not a string 10 | // H = -Sum(i in {1,n}) ((counti/N) * log2(counti/N)) 11 | // counti is count of character i 12 | // n is number of different characters in a string s 13 | // N is length of string s 14 | func H(s []byte) (float64, float64) { 15 | var entropy, sum float64; 16 | // may want to map runes to float at some point 17 | count := make(map[byte]float64); 18 | 19 | for _, b := range s { 20 | count[b]++; 21 | } 22 | 23 | N := float64(len(s)); 24 | 25 | for _, ni := range count { 26 | sum += ni * math.Log2(ni); 27 | } 28 | entropy = math.Log2(N) - sum/N; 29 | Hn := entropy / math.Log2(float64(len(count))); 30 | 31 | return entropy, Hn; 32 | } 33 | 34 | // normalised specific entropy 35 | func H2(s []byte) float64 { 36 | entropy, ni := H(s); 37 | 38 | return entropy / math.Log2(ni); 39 | } 40 | 41 | func isHex(s string) bool { 42 | matched, err := regexp.MatchString("^(0x|0X)?[a-fA-F0-9]+$", s); 43 | if err != nil { 44 | warnf("In isHex, %v", err); 45 | return false; 46 | } 47 | if matched { 48 | return true 49 | } 50 | return false; 51 | } 52 | 53 | func isBase64(s string) bool { 54 | isMatch, err := regexp.MatchString("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", s); 55 | if err != nil { 56 | warnf("In isBase64, %v", err); 57 | return false; 58 | } 59 | if isMatch { 60 | return true; 61 | } 62 | return false; 63 | } 64 | 65 | func isAlphanum(s string) (bool, error) { 66 | b := []byte(s); 67 | isMatch, err := regexp.Match("[a-zA-Z0-9]$", b); 68 | if err != nil { 69 | return false, err; 70 | } 71 | if isMatch { 72 | return true, nil; 73 | } 74 | return false, nil; 75 | } 76 | --------------------------------------------------------------------------------