From 4f3d620d37b91d68a4522ac2cca00872ef398c9f Mon Sep 17 00:00:00 2001 From: Tim Kelsey Date: Wed, 20 Jul 2016 11:02:01 +0100 Subject: [PATCH] Initial public release --- .gitignore | 26 ++ LICENSE.txt | 154 +++++++++++ README.md | 84 ++++++ core/analyzer.go | 152 +++++++++++ core/helpers.go | 82 ++++++ core/issue.go | 96 +++++++ core/select.go | 401 ++++++++++++++++++++++++++++ filelist.go | 46 ++++ main.go | 158 +++++++++++ output/formatter.go | 95 +++++++ rulelist.go | 99 +++++++ rules/bind.go | 53 ++++ rules/bind_test.go | 62 +++++ rules/errors.go | 63 +++++ rules/errors_test.go | 87 ++++++ rules/fileperms.go | 67 +++++ rules/fileperms_test.go | 51 ++++ rules/hardcoded_credentials.go | 53 ++++ rules/hardcoded_credentials_test.go | 39 +++ rules/rsa.go | 53 ++++ rules/rsa_test.go | 48 ++++ rules/sql.go | 89 ++++++ rules/sql_test.go | 146 ++++++++++ rules/subproc.go | 59 ++++ rules/subproc_test.go | 99 +++++++ rules/tempfiles.go | 50 ++++ rules/tempfiles_test.go | 45 ++++ rules/templates.go | 50 ++++ rules/templates_test.go | 131 +++++++++ rules/tls.go | 185 +++++++++++++ rules/tls_test.go | 136 ++++++++++ rules/unsafe.go | 46 ++++ rules/unsafe_test.go | 47 ++++ rules/utils_test.go | 40 +++ rules/weakcrypto.go | 78 ++++++ rules/weakcrypto_test.go | 110 ++++++++ tools.go | 77 ++++++ 37 files changed, 3357 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 core/analyzer.go create mode 100644 core/helpers.go create mode 100644 core/issue.go create mode 100644 core/select.go create mode 100644 filelist.go create mode 100644 main.go create mode 100644 output/formatter.go create mode 100644 rulelist.go create mode 100644 rules/bind.go create mode 100644 rules/bind_test.go create mode 100644 rules/errors.go create mode 100644 rules/errors_test.go create mode 100644 rules/fileperms.go create mode 100644 rules/fileperms_test.go create mode 100644 rules/hardcoded_credentials.go create mode 100644 rules/hardcoded_credentials_test.go create mode 100644 rules/rsa.go create mode 100644 rules/rsa_test.go create mode 100644 rules/sql.go create mode 100644 rules/sql_test.go create mode 100644 rules/subproc.go create mode 100644 rules/subproc_test.go create mode 100644 rules/tempfiles.go create mode 100644 rules/tempfiles_test.go create mode 100644 rules/templates.go create mode 100644 rules/templates_test.go create mode 100644 rules/tls.go create mode 100644 rules/tls_test.go create mode 100644 rules/unsafe.go create mode 100644 rules/unsafe_test.go create mode 100644 rules/utils_test.go create mode 100644 rules/weakcrypto.go create mode 100644 rules/weakcrypto_test.go create mode 100644 tools.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf2bb1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.DS_Store diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1756c78 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,154 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or +Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work +to which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate as of the date +such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and in +Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and You must cause any modified files to carry prominent notices +stating that You changed the files; and You must retain, in the Source form of +any Derivative Works that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, excluding those notices +that do not pertain to any part of the Derivative Works; and If the Work +includes a "NOTICE" text file as part of its distribution, then any Derivative +Works that You distribute must include a readable copy of the attribution +notices contained within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one of the following +places: within a NOTICE text file distributed as part of the Derivative Works; +within the Source form or documentation, if provided along with the Derivative +Works; or, within a display generated by the Derivative Works, if and wherever +such third-party notices normally appear. The contents of the NOTICE file are +for informational purposes only and do not modify the License. You may add Your +own attribution notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided that such +additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. 5. Submission of Contributions. +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks +associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in +tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or +Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such +obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree to +indemnify, defend, and hold each Contributor harmless for any liability incurred +by, or claims asserted against, such Contributor by reason of your accepting any +such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a539ae --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +## GAS - Go AST Scanner + +Inspects source code for security problems by scanning the Go AST. + +### Usage + +Gas can be configured to only run a subset of rules, to exclude certain file +paths, and produce reports in different formats. By default all rules will be +run against the supplied input files. To recursively scan from the current +directory you can supply './...' as the input argument. + +#### Selecting rules + +By default Gas will run all rules against the supplied file paths. It is however possible to select a subset of rules to run via the '-rule=' flag. + +##### Available rules + +- __crypto__ - Detects use of weak cryptography primatives +- __tls__ - Detects if TLS certificate verification is disabled +- __sql__ - SQL injection vectors +- __hardcoded__ - Potential hardcoded credentials +- __perms__ - Insecure file permissions +- __tempfile__ - Insecure creation of temporary files +- __unsafe__- Detects use of the unsafe pointer functions +- __bind__- Listening on all network interfaces +- __rsa__- Weak RSA keys + + +``` +$ gas -rule=rsa -rule=tls -rule=crypto ./... +``` + +#### Excluding files: + +Gas will ignore paths that match a supplied pattern via +[filepath.Match](https://golang.org/pkg/path/filepath/#Match). +Multiple patterns can be specified as follows: + +``` +$ gas -exclude tests* -exclude *_example.go ./... +``` + +#### Annotating code + +In cases where Gas reports a failure that has been verified as being safe. +In these cases it is possible to annotate the code with a '#nosec' comment. +The annotation causes Gas to stop processing any further nodes within the +AST so can apply to a whole block or more granularly to a single expression. + +```go + +import "md5" // #nosec + + +func main(){ + + /* # nosec */ + if x > y { + h := md5.New() // this will also be ignored + } + +} + +``` + +In some cases you may also want to revisit places where #nosec annotations +have been used. To run the scanner and ignore any #nosec annotations you can + do the following: + +``` +$ gas -nosec=true ./... +``` + +### Output formats + +Gas currently supports text, json and csv output formats. By default results +will be reported to stdout, but can also be written to an output file. The +output format is controlled by the '-fmt' flag, and the output file is +controlled by the '-out' flag as follows: + +``` +# Write output in json format to results.json +$ gas -fmt=json -out=results.json *.go +``` diff --git a/core/analyzer.go b/core/analyzer.go new file mode 100644 index 0000000..5325f8a --- /dev/null +++ b/core/analyzer.go @@ -0,0 +1,152 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "log" + "os" + "reflect" + "strings" +) + +type Context struct { + FileSet *token.FileSet + Comments ast.CommentMap + Info *types.Info + Pkg *types.Package +} + +type Rule interface { + Match(ast.Node, *Context) (*Issue, error) +} + +type RuleSet map[reflect.Type][]Rule + +type Metrics struct { + NumFiles int + NumLines int + NumNosec int + NumFound int +} + +type Analyzer struct { + annotations bool + ruleset RuleSet + context Context + logger *log.Logger + Issues []Issue + Stats Metrics +} + +func NewAnalyzer(annotations bool, logger *log.Logger) Analyzer { + if logger == nil { + logger = log.New(os.Stdout, "[gas]", 0) + } + return Analyzer{ + annotations: annotations, + ruleset: make(RuleSet), + Issues: make([]Issue, 0), + context: Context{token.NewFileSet(), nil, nil, nil}, + logger: logger, + } +} + +func (gas *Analyzer) process(filename string, source interface{}) error { + mode := parser.ParseComments + root, err := parser.ParseFile(gas.context.FileSet, filename, source, mode) + if err == nil { + gas.context.Comments = ast.NewCommentMap(gas.context.FileSet, root, root.Comments) + + // here we get type info + gas.context.Info = &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + } + + conf := types.Config{Importer: importer.Default()} + gas.context.Pkg, _ = conf.Check("pkg", gas.context.FileSet, []*ast.File{root}, gas.context.Info) + // TODO: we need to look at the err ret + + ast.Walk(gas, root) + gas.Stats.NumFiles++ + } + return err +} + +func (gas *Analyzer) AddRule(r Rule, n ast.Node) { + t := reflect.TypeOf(n) + if val, ok := gas.ruleset[t]; ok { + gas.ruleset[t] = append(val, r) + } else { + gas.ruleset[t] = []Rule{r} + } +} + +func (gas *Analyzer) Process(filename string) error { + err := gas.process(filename, nil) + fun := func(f *token.File) bool { + gas.Stats.NumLines += f.LineCount() + return true + } + gas.context.FileSet.Iterate(fun) + return err +} + +func (gas *Analyzer) ProcessSource(filename string, source string) error { + err := gas.process(filename, source) + fun := func(f *token.File) bool { + gas.Stats.NumLines += f.LineCount() + return true + } + gas.context.FileSet.Iterate(fun) + return err +} + +func (gas *Analyzer) Ignore(n ast.Node) bool { + if groups, ok := gas.context.Comments[n]; ok { + for _, group := range groups { + if strings.Contains(group.Text(), "nosec") { + gas.Stats.NumNosec++ + return true + } + } + } + return false +} + +func (gas *Analyzer) Visit(n ast.Node) ast.Visitor { + if !gas.annotations || gas.Ignore(n) { + if val, ok := gas.ruleset[reflect.TypeOf(n)]; ok { + for _, rule := range val { + ret, err := rule.Match(n, &gas.context) + if err != nil { + // will want to give more info than this ... + gas.logger.Println("internal error running rule:", err) + } + if ret != nil { + gas.Issues = append(gas.Issues, *ret) + gas.Stats.NumFound++ + } + } + } + } + return gas +} diff --git a/core/helpers.go b/core/helpers.go new file mode 100644 index 0000000..ddabe26 --- /dev/null +++ b/core/helpers.go @@ -0,0 +1,82 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "fmt" + "go/ast" + "go/token" + "reflect" + "regexp" + "strconv" + "strings" +) + +// helpfull "canned" matching routines ---------------------------------------- + +func selectName(n ast.Node, s reflect.Type) (string, bool) { + t := reflect.TypeOf(&ast.SelectorExpr{}) + if node, ok := SimpleSelect(n, s, t).(*ast.SelectorExpr); ok { + t = reflect.TypeOf(&ast.Ident{}) + if ident, ok := SimpleSelect(node.X, t).(*ast.Ident); ok { + return strings.Join([]string{ident.Name, node.Sel.Name}, "."), ok + } + } + return "", false +} + +func MatchCall(n ast.Node, r *regexp.Regexp) *ast.CallExpr { + t := reflect.TypeOf(&ast.CallExpr{}) + if name, ok := selectName(n, t); ok && r.MatchString(name) { + return n.(*ast.CallExpr) + } + return nil +} + +func MatchCompLit(n ast.Node, r *regexp.Regexp) *ast.CompositeLit { + t := reflect.TypeOf(&ast.CompositeLit{}) + if name, ok := selectName(n, t); ok && r.MatchString(name) { + return n.(*ast.CompositeLit) + } + return nil +} + +func GetInt(n ast.Node) (int64, error) { + if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.INT { + return strconv.ParseInt(node.Value, 0, 64) + } + return 0, fmt.Errorf("Unexpected AST node type: %T", n) +} + +func GetFloat(n ast.Node) (float64, error) { + if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.FLOAT { + return strconv.ParseFloat(node.Value, 64) + } + return 0.0, fmt.Errorf("Unexpected AST node type: %T", n) +} + +func GetChar(n ast.Node) (byte, error) { + if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.CHAR { + return node.Value[0], nil + } + return 0, fmt.Errorf("Unexpected AST node type: %T", n) +} + +func GetString(n ast.Node) (string, error) { + if node, ok := n.(*ast.BasicLit); ok && node.Kind == token.STRING { + return strconv.Unquote(node.Value) + } + return "", fmt.Errorf("Unexpected AST node type: %T", n) +} diff --git a/core/issue.go b/core/issue.go new file mode 100644 index 0000000..ad8ec30 --- /dev/null +++ b/core/issue.go @@ -0,0 +1,96 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package core + +import ( + "fmt" + "go/ast" + "os" +) + +type Score int + +const ( + Low Score = iota + Medium + High +) + +type Issue struct { + Severity Score + Confidence Score + What string + File string + Code string + Line int +} + +type MetaData struct { + Severity Score + Confidence Score + What string +} + +func (c Score) String() string { + switch c { + case High: + return "HIGH" + case Medium: + return "MEDIUM" + case Low: + return "LOW" + } + return "UNDEFINED" +} + +func codeSnippet(file *os.File, start int64, end int64, n ast.Node) (string, error) { + if n == nil { + return "", fmt.Errorf("Invalid AST node provided") + } + + size := (int)(end - start) // Go bug, os.File.Read should return int64 ... + file.Seek(start, 0) + + buf := make([]byte, size) + if nread, err := file.Read(buf); err != nil || nread != size { + return "", fmt.Errorf("Unable to read code") + } + return string(buf), nil +} + +func NewIssue(ctx *Context, node ast.Node, desc string, severity Score, confidence Score) *Issue { + var code string + fobj := ctx.FileSet.File(node.Pos()) + name := fobj.Name() + line := fobj.Line(node.Pos()) + + if file, err := os.Open(fobj.Name()); err == nil { + defer file.Close() + s := (int64)(fobj.Position(node.Pos()).Offset) // Go bug, should be int64 + e := (int64)(fobj.Position(node.End()).Offset) // Go bug, should be int64 + code, err = codeSnippet(file, s, e, node) + if err != nil { + code = err.Error() + } + } + + return &Issue{ + File: name, + Line: line, + What: desc, + Confidence: confidence, + Severity: severity, + Code: code, + } +} diff --git a/core/select.go b/core/select.go new file mode 100644 index 0000000..24f2316 --- /dev/null +++ b/core/select.go @@ -0,0 +1,401 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "fmt" + "go/ast" + "reflect" +) + +// A selector function. This is like a visitor, but has a richer interface. It +// is called with the current ast.Node being visitied and that nodes depth in +// the tree. The function can return true to continue traversing the tree, or +// false to end traversal here. +type SelectFunc func(ast.Node, int) bool + +func walkIdentList(list []*ast.Ident, depth int, fun SelectFunc) { + for _, x := range list { + depthWalk(x, depth, fun) + } +} + +func walkExprList(list []ast.Expr, depth int, fun SelectFunc) { + for _, x := range list { + depthWalk(x, depth, fun) + } +} + +func walkStmtList(list []ast.Stmt, depth int, fun SelectFunc) { + for _, x := range list { + depthWalk(x, depth, fun) + } +} + +func walkDeclList(list []ast.Decl, depth int, fun SelectFunc) { + for _, x := range list { + depthWalk(x, depth, fun) + } +} + +func depthWalk(node ast.Node, depth int, fun SelectFunc) { + if !fun(node, depth) { + return + } + + switch n := node.(type) { + // Comments and fields + case *ast.Comment: + + case *ast.CommentGroup: + for _, c := range n.List { + depthWalk(c, depth+1, fun) + } + + case *ast.Field: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + walkIdentList(n.Names, depth+1, fun) + depthWalk(n.Type, depth+1, fun) + if n.Tag != nil { + depthWalk(n.Tag, depth+1, fun) + } + if n.Comment != nil { + depthWalk(n.Comment, depth+1, fun) + } + + case *ast.FieldList: + for _, f := range n.List { + depthWalk(f, depth+1, fun) + } + + // Expressions + case *ast.BadExpr, *ast.Ident, *ast.BasicLit: + + case *ast.Ellipsis: + if n.Elt != nil { + depthWalk(n.Elt, depth+1, fun) + } + + case *ast.FuncLit: + depthWalk(n.Type, depth+1, fun) + depthWalk(n.Body, depth+1, fun) + + case *ast.CompositeLit: + if n.Type != nil { + depthWalk(n.Type, depth+1, fun) + } + walkExprList(n.Elts, depth+1, fun) + + case *ast.ParenExpr: + depthWalk(n.X, depth+1, fun) + + case *ast.SelectorExpr: + depthWalk(n.X, depth+1, fun) + depthWalk(n.Sel, depth+1, fun) + + case *ast.IndexExpr: + depthWalk(n.X, depth+1, fun) + depthWalk(n.Index, depth+1, fun) + + case *ast.SliceExpr: + depthWalk(n.X, depth+1, fun) + if n.Low != nil { + depthWalk(n.Low, depth+1, fun) + } + if n.High != nil { + depthWalk(n.High, depth+1, fun) + } + if n.Max != nil { + depthWalk(n.Max, depth+1, fun) + } + + case *ast.TypeAssertExpr: + depthWalk(n.X, depth+1, fun) + if n.Type != nil { + depthWalk(n.Type, depth+1, fun) + } + + case *ast.CallExpr: + depthWalk(n.Fun, depth+1, fun) + walkExprList(n.Args, depth+1, fun) + + case *ast.StarExpr: + depthWalk(n.X, depth+1, fun) + + case *ast.UnaryExpr: + depthWalk(n.X, depth+1, fun) + + case *ast.BinaryExpr: + depthWalk(n.X, depth+1, fun) + depthWalk(n.Y, depth+1, fun) + + case *ast.KeyValueExpr: + depthWalk(n.Key, depth+1, fun) + depthWalk(n.Value, depth+1, fun) + + // Types + case *ast.ArrayType: + if n.Len != nil { + depthWalk(n.Len, depth+1, fun) + } + depthWalk(n.Elt, depth+1, fun) + + case *ast.StructType: + depthWalk(n.Fields, depth+1, fun) + + case *ast.FuncType: + if n.Params != nil { + depthWalk(n.Params, depth+1, fun) + } + if n.Results != nil { + depthWalk(n.Results, depth+1, fun) + } + + case *ast.InterfaceType: + depthWalk(n.Methods, depth+1, fun) + + case *ast.MapType: + depthWalk(n.Key, depth+1, fun) + depthWalk(n.Value, depth+1, fun) + + case *ast.ChanType: + depthWalk(n.Value, depth+1, fun) + + // Statements + case *ast.BadStmt: + + case *ast.DeclStmt: + depthWalk(n.Decl, depth+1, fun) + + case *ast.EmptyStmt: + + case *ast.LabeledStmt: + depthWalk(n.Label, depth+1, fun) + depthWalk(n.Stmt, depth+1, fun) + + case *ast.ExprStmt: + depthWalk(n.X, depth+1, fun) + + case *ast.SendStmt: + depthWalk(n.Chan, depth+1, fun) + depthWalk(n.Value, depth+1, fun) + + case *ast.IncDecStmt: + depthWalk(n.X, depth+1, fun) + + case *ast.AssignStmt: + walkExprList(n.Lhs, depth+1, fun) + walkExprList(n.Rhs, depth+1, fun) + + case *ast.GoStmt: + depthWalk(n.Call, depth+1, fun) + + case *ast.DeferStmt: + depthWalk(n.Call, depth+1, fun) + + case *ast.ReturnStmt: + walkExprList(n.Results, depth+1, fun) + + case *ast.BranchStmt: + if n.Label != nil { + depthWalk(n.Label, depth+1, fun) + } + + case *ast.BlockStmt: + walkStmtList(n.List, depth+1, fun) + + case *ast.IfStmt: + if n.Init != nil { + depthWalk(n.Init, depth+1, fun) + } + depthWalk(n.Cond, depth+1, fun) + depthWalk(n.Body, depth+1, fun) + if n.Else != nil { + depthWalk(n.Else, depth+1, fun) + } + + case *ast.CaseClause: + walkExprList(n.List, depth+1, fun) + walkStmtList(n.Body, depth+1, fun) + + case *ast.SwitchStmt: + if n.Init != nil { + depthWalk(n.Init, depth+1, fun) + } + if n.Tag != nil { + depthWalk(n.Tag, depth+1, fun) + } + depthWalk(n.Body, depth+1, fun) + + case *ast.TypeSwitchStmt: + if n.Init != nil { + depthWalk(n.Init, depth+1, fun) + } + depthWalk(n.Assign, depth+1, fun) + depthWalk(n.Body, depth+1, fun) + + case *ast.CommClause: + if n.Comm != nil { + depthWalk(n.Comm, depth+1, fun) + } + walkStmtList(n.Body, depth+1, fun) + + case *ast.SelectStmt: + depthWalk(n.Body, depth+1, fun) + + case *ast.ForStmt: + if n.Init != nil { + depthWalk(n.Init, depth+1, fun) + } + if n.Cond != nil { + depthWalk(n.Cond, depth+1, fun) + } + if n.Post != nil { + depthWalk(n.Post, depth+1, fun) + } + depthWalk(n.Body, depth+1, fun) + + case *ast.RangeStmt: + if n.Key != nil { + depthWalk(n.Key, depth+1, fun) + } + if n.Value != nil { + depthWalk(n.Value, depth+1, fun) + } + depthWalk(n.X, depth+1, fun) + depthWalk(n.Body, depth+1, fun) + + // Declarations + case *ast.ImportSpec: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + if n.Name != nil { + depthWalk(n.Name, depth+1, fun) + } + depthWalk(n.Path, depth+1, fun) + if n.Comment != nil { + depthWalk(n.Comment, depth+1, fun) + } + + case *ast.ValueSpec: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + walkIdentList(n.Names, depth+1, fun) + if n.Type != nil { + depthWalk(n.Type, depth+1, fun) + } + walkExprList(n.Values, depth+1, fun) + if n.Comment != nil { + depthWalk(n.Comment, depth+1, fun) + } + + case *ast.TypeSpec: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + depthWalk(n.Name, depth+1, fun) + depthWalk(n.Type, depth+1, fun) + if n.Comment != nil { + depthWalk(n.Comment, depth+1, fun) + } + + case *ast.BadDecl: + + case *ast.GenDecl: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + for _, s := range n.Specs { + depthWalk(s, depth+1, fun) + } + + case *ast.FuncDecl: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + if n.Recv != nil { + depthWalk(n.Recv, depth+1, fun) + } + depthWalk(n.Name, depth+1, fun) + depthWalk(n.Type, depth+1, fun) + if n.Body != nil { + depthWalk(n.Body, depth+1, fun) + } + + // Files and packages + case *ast.File: + if n.Doc != nil { + depthWalk(n.Doc, depth+1, fun) + } + depthWalk(n.Name, depth+1, fun) + walkDeclList(n.Decls, depth+1, fun) + // don't walk n.Comments - they have been + // visited already through the individual + // nodes + + case *ast.Package: + for _, f := range n.Files { + depthWalk(f, depth+1, fun) + } + + default: + panic(fmt.Sprintf("gas.depthWalk: unexpected node type %T", n)) + } +} + +type Selector interface { + Final(ast.Node) + Partial(ast.Node) bool +} + +func Select(s Selector, n ast.Node, bits ...reflect.Type) { + fun := func(n ast.Node, d int) bool { + if d < len(bits) && reflect.TypeOf(n) == bits[d] { + if d == len(bits)-1 { + s.Final(n) + return false + } else if s.Partial(n) { + return true + } + } + return false + } + depthWalk(n, 0, fun) +} + +func SimpleSelect(n ast.Node, bits ...reflect.Type) ast.Node { + var found ast.Node + fun := func(n ast.Node, d int) bool { + if found != nil { + return false // short cut logic if we have found a match + } + + if d < len(bits) && reflect.TypeOf(n) == bits[d] { + if d == len(bits)-1 { + found = n + return false + } + return true + } + return false + } + + depthWalk(n, 0, fun) + return found +} diff --git a/filelist.go b/filelist.go new file mode 100644 index 0000000..73c8a36 --- /dev/null +++ b/filelist.go @@ -0,0 +1,46 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "path/filepath" + "strings" +) + +type filelist []string + +func (f *filelist) String() string { + return strings.Join([]string(*f), ", ") +} + +func (f *filelist) Set(val string) error { + *f = append(*f, val) + return nil +} + +func (f *filelist) Contains(path string) bool { + // Ignore dot files + _, filename := filepath.Split(path) + if strings.HasPrefix(filename, ".") { + return true + } + for _, pattern := range *f { + // Match entire path + if rv, err := filepath.Match(pattern, path); rv && err == nil { + return true + } + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6d85fbc --- /dev/null +++ b/main.go @@ -0,0 +1,158 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + gas "github.com/HewlettPackard/gas/core" + "github.com/HewlettPackard/gas/output" +) + +// #nosec flag +var flagNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set") + +// format output +var flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv of text") + +// output file +var flagOutput = flag.String("out", "", "Set output file for results") + +var usageText = ` +GAS - Go AST Scanner + +Gas analyzes Go source code to look for common programming mistakes that +can lead to security problems. + +USAGE: + + # Check a single Go file + $ gas example.go + + # Check all files under the current directory and save results in + # json format. + $ gas -fmt=json -out=results.json ./... + + # Run a specific set of rules (by default all rules will be run): + $ gas -rule=sql -rule=sql ./... + +` + +func usage() { + fmt.Fprintln(os.Stderr, usageText) + fmt.Fprint(os.Stderr, "OPTIONS:\n\n") + flag.PrintDefaults() +} + +func main() { + + // Setup usage description + flag.Usage = usage + + // Exclude files + var excluded filelist = []string{"*_test.go"} + flag.Var(&excluded, "exclude", "File pattern to exclude from scan") + + // Rule configuration + rules := newRulelist() + flag.Var(&rules, "rule", "GAS rules enabled when performing a scan") + + // Custom commands / utilities to run instead of default analyzer + tools := newUtils() + flag.Var(tools, "tool", "GAS utilities to assist with rule development") + + // Parse command line arguments + flag.Parse() + + // Setup logging + logger := log.New(os.Stderr, "[gas]", log.LstdFlags) + + // Ensure at least one file was specified + if flag.NArg() == 0 { + + fmt.Fprintf(os.Stderr, "\nerror: FILE [FILE...] or './...' expected\n") + flag.Usage() + os.Exit(1) + } + + // Run utils instead of analysis + if len(tools.call) > 0 { + tools.run(flag.Args()...) + os.Exit(0) + } + + // Setup analyzer + analyzer := gas.NewAnalyzer(*flagNoSec, logger) + if !rules.overwritten { + rules.useDefaults() + } + rules.apply(&analyzer) + + // Traverse directory structure if './...' + if flag.NArg() == 1 && flag.Arg(0) == "./..." { + + cwd, err := os.Getwd() + if err != nil { + logger.Fatalf("Unable to traverse path %s, reason - %s", flag.Arg(0), err) + } + filepath.Walk(cwd, func(path string, info os.FileInfo, err error) error { + if excluded.Contains(path) && info.IsDir() { + logger.Printf("Skipping %s\n", path) + return filepath.SkipDir + } + if !info.IsDir() && !excluded.Contains(path) && + strings.HasSuffix(path, ".go") { + err = analyzer.Process(path) + if err != nil { + logger.Fatal(err) + } + } + return nil + }) + + } else { + + // Process each file individually + for _, filename := range flag.Args() { + if finfo, err := os.Stat(filename); err == nil { + if !finfo.IsDir() && !excluded.Contains(filename) && + strings.HasSuffix(filename, ".go") { + if err = analyzer.Process(filename); err != nil { + logger.Fatal(err) + } + } + } else { + logger.Fatal(err) + } + } + } + + // Create output report + if *flagOutput != "" { + outfile, err := os.Create(*flagOutput) + if err != nil { + logger.Fatalf("Couldn't open: %s for writing. Reason - %s", *flagOutput, err) + } + defer outfile.Close() + output.CreateReport(outfile, *flagFormat, &analyzer) + } else { + output.CreateReport(os.Stdout, *flagFormat, &analyzer) + } +} diff --git a/output/formatter.go b/output/formatter.go new file mode 100644 index 0000000..0de65ee --- /dev/null +++ b/output/formatter.go @@ -0,0 +1,95 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "io" + "text/template" + + gas "github.com/HewlettPackard/gas/core" +) + +// The output format for reported issues +type ReportFormat int + +const ( + ReportText ReportFormat = iota // Plain text format + ReportJSON // Json format + ReportCSV // CSV format +) + +var text = `Results: +{{ range $index, $issue := .Issues }} +[{{ $issue.File }}:{{ $issue.Line }}] - {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }}) + > {{ $issue.Code }} + +{{ end }} +Summary: + Files: {{.Stats.NumFiles}} + Lines: {{.Stats.NumLines}} + Nosec: {{.Stats.NumNosec}} + Issues: {{.Stats.NumFound}} + +` + +var json = `{ + "metrics": [ + Files: {{.Stats.NumFiles}}, + Lines: {{.Stats.NumLines}}, + Nosec: {{.Stats.NumNosec}}, + Issues: {{.Stats.NumFound}}], + + "issues": [ + {{ range $index, $issue := .Issues }}{{ if $index }}, {{ end }}{ + "file": "{{ $issue.File }}", + "line": "{{ $issue.Line }}", + "details": "{{ $issue.What }}", + "confidence": "{{ $issue.Confidence }}", + "severity": "{{ $issue.Severity }}", + "code": "{{ js $issue.Code }}" + }{{ end }} + ] +}` + +var csv = `{{ range $index, $issue := .Issues -}} +{{- $issue.File -}}, +{{- $issue.Line -}}, +{{- $issue.What -}}, +{{- $issue.Severity -}}, +{{- $issue.Confidence -}}, +{{- printf "%q" $issue.Code }} +{{ end }}` + +func CreateReport(w io.Writer, format string, data *gas.Analyzer) error { + reportType := text + + switch format { + case "csv": + reportType = csv + case "json": + reportType = json + case "text": + reportType = text + default: + reportType = text + } + + t, e := template.New("gas").Parse(reportType) + if e != nil { + return e + } + + return t.Execute(w, data) +} diff --git a/rulelist.go b/rulelist.go new file mode 100644 index 0000000..78208dc --- /dev/null +++ b/rulelist.go @@ -0,0 +1,99 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "go/ast" + "strings" + + gas "github.com/HewlettPackard/gas/core" + "github.com/HewlettPackard/gas/rules" +) + +type ruleMaker func() (gas.Rule, ast.Node) +type ruleConfig struct { + enabled bool + constructors []ruleMaker +} + +type rulelist struct { + rules map[string]*ruleConfig + overwritten bool +} + +func newRulelist() rulelist { + var rs rulelist + rs.rules = make(map[string]*ruleConfig) + rs.overwritten = false + rs.register("sql", rules.NewSqlStrConcat, rules.NewSqlStrFormat) + rs.register("crypto", rules.NewImportsWeakCryptography, rules.NewUsesWeakCryptography) + rs.register("hardcoded", rules.NewHardcodedCredentials) + rs.register("perms", rules.NewMkdirPerms, rules.NewChmodPerms) + rs.register("tempfile", rules.NewBadTempFile) + rs.register("tls_good", rules.NewModernTlsCheck) + rs.register("tls_ok", rules.NewIntermediateTlsCheck) + rs.register("tls_old", rules.NewCompatTlsCheck) + rs.register("bind", rules.NewBindsToAllNetworkInterfaces) + rs.register("unsafe", rules.NewUsingUnsafe) + rs.register("rsa", rules.NewWeakKeyStrength) + rs.register("templates", rules.NewTemplateCheck) + rs.register("exec", rules.NewSubproc) + rs.register("errors", rules.NewNoErrorCheck) + return rs +} + +func (r *rulelist) register(name string, cons ...ruleMaker) { + r.rules[name] = &ruleConfig{false, cons} +} + +func (r *rulelist) useDefaults() { + for k := range r.rules { + r.rules[k].enabled = true + } +} + +func (r *rulelist) list() []string { + i := 0 + keys := make([]string, len(r.rules)) + for k := range r.rules { + keys[i] = k + i++ + } + return keys +} + +func (r *rulelist) apply(g *gas.Analyzer) { + for _, v := range r.rules { + if v.enabled { + for _, ctor := range v.constructors { + g.AddRule(ctor()) + } + } + } +} + +func (r *rulelist) String() string { + return strings.Join(r.list(), ", ") +} + +func (r *rulelist) Set(opt string) error { + r.overwritten = true + if x, ok := r.rules[opt]; ok { + x.enabled = true + return nil + } + return fmt.Errorf("Valid rules are: %s", r) +} diff --git a/rules/bind.go b/rules/bind.go new file mode 100644 index 0000000..ae0b619 --- /dev/null +++ b/rules/bind.go @@ -0,0 +1,53 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +// Looks for net.Listen("0.0.0.0") or net.Listen(":8080") +type BindsToAllNetworkInterfaces struct { + gas.MetaData + call *regexp.Regexp + pattern *regexp.Regexp +} + +func (r *BindsToAllNetworkInterfaces) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCall(n, r.call); node != nil { + if arg, err := gas.GetString(node.Args[1]); err == nil { + if r.pattern.MatchString(arg) { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + } + } + return +} + +func NewBindsToAllNetworkInterfaces() (r gas.Rule, n ast.Node) { + r = &BindsToAllNetworkInterfaces{ + call: regexp.MustCompile(`^net.Listen$`), + pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "Binds to all network interfaces", + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/bind_test.go b/rules/bind_test.go new file mode 100644 index 0000000..13ca4ee --- /dev/null +++ b/rules/bind_test.go @@ -0,0 +1,62 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestBind0000(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewBindsToAllNetworkInterfaces()) + + issues := gasTestRunner(` + package main + import ( + "log" + "net" + ) + func main() { + l, err := net.Listen("tcp", "0.0.0.0:2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() + }`, analyzer) + + checkTestResults(t, issues, 1, "Binds to all network interfaces") +} + +func TestBindEmptyHost(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewBindsToAllNetworkInterfaces()) + + issues := gasTestRunner(` + package main + import ( + "log" + "net" + ) + func main() { + l, err := net.Listen("tcp", ":2000") + if err != nil { + log.Fatal(err) + } + defer l.Close() + }`, analyzer) + + checkTestResults(t, issues, 1, "Binds to all network interfaces") +} diff --git a/rules/errors.go b/rules/errors.go new file mode 100644 index 0000000..75d6634 --- /dev/null +++ b/rules/errors.go @@ -0,0 +1,63 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "go/ast" + "go/types" + "reflect" + + gas "github.com/HewlettPackard/gas/core" +) + +type NoErrorCheck struct { + gas.MetaData +} + +func (r *NoErrorCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node, ok := n.(*ast.AssignStmt); ok { + sel := reflect.TypeOf(&ast.CallExpr{}) + if call, ok := gas.SimpleSelect(node.Rhs[0], sel).(*ast.CallExpr); ok { + if t := c.Info.Types[call].Type; t != nil { + if typeVal, typeErr := t.(*types.Tuple); typeErr { + for i := 0; i < typeVal.Len(); i++ { + if typeVal.At(i).Type().String() == "error" { // TODO(tkelsey): is there a better way? + if id, ok := node.Lhs[i].(*ast.Ident); ok && id.Name == "_" { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + } + } + } else if t.String() == "error" { // TODO(tkelsey): is there a better way? + if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "_" { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + } + } + } + } + return nil, nil +} + +func NewNoErrorCheck() (r gas.Rule, n ast.Node) { + r = &NoErrorCheck{ + MetaData: gas.MetaData{ + Severity: gas.Low, + Confidence: gas.High, + What: "Errors unhandled.", + }, + } + n = (*ast.AssignStmt)(nil) + return +} diff --git a/rules/errors_test.go b/rules/errors_test.go new file mode 100644 index 0000000..f45161f --- /dev/null +++ b/rules/errors_test.go @@ -0,0 +1,87 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "testing" + + gas "github.com/HewlettPackard/gas/core" +) + +func TestErrorsMulti(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewNoErrorCheck()) + + issues := gasTestRunner( + `package main + + import ( + "fmt" + ) + + func test() (val int, err error) { + return 0, nil + } + + func main() { + v, _ := test() + }`, analyzer) + + checkTestResults(t, issues, 1, "Errors unhandled") +} + +func TestErrorsSingle(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewNoErrorCheck()) + + issues := gasTestRunner( + `package main + + import ( + "fmt" + ) + + func test() (err error) { + return nil + } + + func main() { + _ := test() + }`, analyzer) + + checkTestResults(t, issues, 1, "Errors unhandled") +} + +func TestErrorsGood(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewNoErrorCheck()) + + issues := gasTestRunner( + `package main + + import ( + "fmt" + ) + + func test() err error { + return 0, nil + } + + func main() { + e := test() + }`, analyzer) + + checkTestResults(t, issues, 0, "") +} diff --git a/rules/fileperms.go b/rules/fileperms.go new file mode 100644 index 0000000..afd1392 --- /dev/null +++ b/rules/fileperms.go @@ -0,0 +1,67 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "fmt" + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +type FilePermissions struct { + gas.MetaData + pattern *regexp.Regexp + mode int64 +} + +func (r *FilePermissions) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + if node := gas.MatchCall(n, r.pattern); node != nil { + if val, err := gas.GetInt(node.Args[1]); err == nil && val > r.mode { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + } + return nil, nil +} + +func NewChmodPerms() (r gas.Rule, n ast.Node) { + mode := 0600 + r = &FilePermissions{ + pattern: regexp.MustCompile(`^os.Chmod$`), + mode: (int64)(mode), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: fmt.Sprintf("Expect chmod permissions to be %#o or less", mode), + }, + } + n = (*ast.CallExpr)(nil) + return +} + +func NewMkdirPerms() (r gas.Rule, n ast.Node) { + mode := 0700 + r = &FilePermissions{ + pattern: regexp.MustCompile(`^(os.Mkdir|os.MkdirAll)$`), + mode: (int64)(mode), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: fmt.Sprintf("Expect directory permissions to be %#o or less", mode), + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/fileperms_test.go b/rules/fileperms_test.go new file mode 100644 index 0000000..e5e5016 --- /dev/null +++ b/rules/fileperms_test.go @@ -0,0 +1,51 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestChmod(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewChmodPerms()) + + issues := gasTestRunner(` + package main + import "os" + func main() { + os.Chmod("/tmp/somefile", 0777) + os.Chmod("/tmp/someotherfile", 0600) + }`, analyzer) + + checkTestResults(t, issues, 1, "Expect chmod permissions") +} + +func TestMkdir(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewMkdirPerms()) + + issues := gasTestRunner(` + package main + import "os" + func main() { + os.Mkdir("/tmp/mydir", 0777) + os.Mkdir("/tmp/mydir", 0600) + os.MkdirAll("/tmp/mydir/mysubidr", 0775) + }`, analyzer) + + checkTestResults(t, issues, 2, "Expect directory permissions") +} diff --git a/rules/hardcoded_credentials.go b/rules/hardcoded_credentials.go new file mode 100644 index 0000000..99f4733 --- /dev/null +++ b/rules/hardcoded_credentials.go @@ -0,0 +1,53 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +type CredsAssign struct { + gas.MetaData + pattern *regexp.Regexp +} + +func (r *CredsAssign) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node, ok := n.(*ast.AssignStmt); ok { + for _, i := range node.Lhs { + if ident, ok := i.(*ast.Ident); ok { + if r.pattern.MatchString(ident.Name) { + gi = gas.NewIssue(c, n, r.What, r.Severity, r.Confidence) + break + } + } + } + } + return +} + +func NewHardcodedCredentials() (r gas.Rule, n ast.Node) { + r = &CredsAssign{ + pattern: regexp.MustCompile("(?i)passwd|pass|password|pwd|secret|token"), + MetaData: gas.MetaData{ + What: "Potential hardcoded credentials", + Confidence: gas.Low, + Severity: gas.High, + }, + } + n = (*ast.AssignStmt)(nil) + return +} diff --git a/rules/hardcoded_credentials_test.go b/rules/hardcoded_credentials_test.go new file mode 100644 index 0000000..e59fe37 --- /dev/null +++ b/rules/hardcoded_credentials_test.go @@ -0,0 +1,39 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestHardcoded(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewHardcodedCredentials()) + + issues := gasTestRunner( + ` + package samples + + import "fmt" + + func main() { + username := "admin" + password := "admin" + fmt.Println("Doing something with: ", username, password) + }`, analyzer) + + checkTestResults(t, issues, 1, "Potential hardcoded credentials") +} diff --git a/rules/rsa.go b/rules/rsa.go new file mode 100644 index 0000000..7639f66 --- /dev/null +++ b/rules/rsa.go @@ -0,0 +1,53 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "fmt" + "go/ast" + "regexp" + + gas "github.com/HewlettPackard/gas/core" +) + +type WeakKeyStrength struct { + gas.MetaData + pattern *regexp.Regexp + bits int +} + +func (w *WeakKeyStrength) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + if node := gas.MatchCall(n, w.pattern); node != nil { + if bits, err := gas.GetInt(node.Args[1]); err == nil && bits < (int64)(w.bits) { + return gas.NewIssue(c, n, w.What, w.Severity, w.Confidence), nil + } + } + return nil, nil +} + +func NewWeakKeyStrength() (r gas.Rule, n ast.Node) { + bits := 2048 + r = &WeakKeyStrength{ + pattern: regexp.MustCompile(`^rsa.GenerateKey$`), + bits: bits, + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: fmt.Sprintf("RSA keys should be at least %d bits", bits), + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/rsa_test.go b/rules/rsa_test.go new file mode 100644 index 0000000..64e804c --- /dev/null +++ b/rules/rsa_test.go @@ -0,0 +1,48 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestRSAKeys(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewWeakKeyStrength()) + + issues := gasTestRunner( + `package main + + import ( + "crypto/rand" + "crypto/rsa" + "fmt" + ) + + func main() { + + //Generate Private Key + pvk, err := rsa.GenerateKey(rand.Reader, 1024) + + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) + + }`, analyzer) + + checkTestResults(t, issues, 1, "RSA keys should") +} diff --git a/rules/sql.go b/rules/sql.go new file mode 100644 index 0000000..9d98398 --- /dev/null +++ b/rules/sql.go @@ -0,0 +1,89 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "reflect" + "regexp" +) + +type SqlStatement struct { + gas.MetaData + pattern *regexp.Regexp +} + +type SqlStrConcat struct { + SqlStatement +} + +// Look for "SELECT * FROM table WHERE " + " ' OR 1=1" +func (s *SqlStrConcat) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + a := reflect.TypeOf(&ast.BinaryExpr{}) + b := reflect.TypeOf(&ast.BasicLit{}) + if node := gas.SimpleSelect(n, a, b); node != nil { + if str, _ := gas.GetString(node); s.pattern.MatchString(str) { + return gas.NewIssue(c, n, s.What, s.Severity, s.Confidence), nil + } + } + return nil, nil +} + +func NewSqlStrConcat() (r gas.Rule, n ast.Node) { + r = &SqlStrConcat{ + SqlStatement: SqlStatement{ + pattern: regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "SQL string concatenation", + }, + }, + } + n = (*ast.BinaryExpr)(nil) + return +} + +type SqlStrFormat struct { + SqlStatement + call *regexp.Regexp +} + +// Looks for "fmt.Sprintf("SELECT * FROM foo where '%s', userInput)" +func (s *SqlStrFormat) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCall(n, s.call); node != nil { + if arg, _ := gas.GetString(node.Args[0]); s.pattern.MatchString(arg) { + return gas.NewIssue(c, n, s.What, s.Severity, s.Confidence), nil + } + } + return nil, nil +} + +func NewSqlStrFormat() (r gas.Rule, n ast.Node) { + r = &SqlStrFormat{ + call: regexp.MustCompile("^fmt.Sprintf$"), + SqlStatement: SqlStatement{ + pattern: regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "SQL string formatting", + }, + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/sql_test.go b/rules/sql_test.go new file mode 100644 index 0000000..76cf920 --- /dev/null +++ b/rules/sql_test.go @@ -0,0 +1,146 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestSQLInjectionViaConcatenation(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSqlStrConcat()) + + source := ` + package main + import ( + "database/sql" + "os" + _ "github.com/mattn/go-sqlite3" + ) + func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query("SELECT * FROM foo WHERE name = " + os.Args[1]) + if err != nil { + panic(err) + } + defer rows.Close() + } + ` + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 1, "SQL string concatenation") +} + +func TestSQLInjectionViaIntepolation(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSqlStrFormat()) + + source := ` + package main + import ( + "database/sql" + "fmt" + "os" + _ "github.com/mattn/go-sqlite3" + ) + func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", os.Args[1]) + rows, err := db.Query(q) + if err != nil { + panic(err) + } + defer rows.Close() + } + ` + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 1, "SQL string formatting") +} + +func TestSQLInjectionFalsePositiveA(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSqlStrConcat()) + analyzer.AddRule(NewSqlStrFormat()) + + source := ` + + package main + import ( + "database/sql" + "fmt" + "os" + _ "github.com/mattn/go-sqlite3" + ) + + var staticQuery = "SELECT * FROM foo WHERE age < 32" + + func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query(staticQuery) + if err != nil { + panic(err) + } + defer rows.Close() + } + + ` + issues := gasTestRunner(source, analyzer) + + checkTestResults(t, issues, 0, "Not expected to match") +} + +func TestSQLInjectionFalsePositiveB(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSqlStrConcat()) + analyzer.AddRule(NewSqlStrFormat()) + + source := ` + + package main + import ( + "database/sql" + "fmt" + "os" + _ "github.com/mattn/go-sqlite3" + ) + + var staticQuery = "SELECT * FROM foo WHERE age < 32" + + func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query(staticQuery) + if err != nil { + panic(err) + } + defer rows.Close() + } + + ` + issues := gasTestRunner(source, analyzer) + + checkTestResults(t, issues, 0, "Not expected to match") +} diff --git a/rules/subproc.go b/rules/subproc.go new file mode 100644 index 0000000..99f8f68 --- /dev/null +++ b/rules/subproc.go @@ -0,0 +1,59 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" + "strings" +) + +type Subprocess struct { + pattern *regexp.Regexp +} + +func (r *Subprocess) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + if node := gas.MatchCall(n, r.pattern); node != nil { + // call with variable command or arguments + for _, arg := range node.Args { + if _, test := arg.(*ast.BasicLit); !test { + // TODO: try to resolve the symbol ... + what := "Subprocess launching with variable." + return gas.NewIssue(c, n, what, gas.High, gas.High), nil + } + } + + // call with partially qualified command + if str, err := gas.GetString(node.Args[0]); err == nil { + if !strings.HasPrefix(str, "/") { + what := "Subprocess launching with partial path." + return gas.NewIssue(c, n, what, gas.Medium, gas.High), nil + } + } + + what := "Subprocess launching should be audited." + return gas.NewIssue(c, n, what, gas.Low, gas.High), nil + } + return nil, nil +} + +func NewSubproc() (r gas.Rule, n ast.Node) { + r = &Subprocess{ + pattern: regexp.MustCompile(`^exec.Command$`), + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/subproc_test.go b/rules/subproc_test.go new file mode 100644 index 0000000..6ea95d0 --- /dev/null +++ b/rules/subproc_test.go @@ -0,0 +1,99 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestSubprocess(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSubproc()) + + issues := gasTestRunner(` + package main + + import ( + "log" + "os/exec" + ) + + func main() { + cmd := exec.Command("/bin/sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) + }`, analyzer) + + checkTestResults(t, issues, 1, "Subprocess launching should be audited.") +} + +func TestSubprocessVar(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSubproc()) + + issues := gasTestRunner(` + package main + + import ( + "log" + "os/exec" + ) + + func main() { + run := "sleep" + cmd := exec.Command(run, "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) + }`, analyzer) + + checkTestResults(t, issues, 1, "Subprocess launching with variable.") +} + +func TestSubprocessPath(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewSubproc()) + + issues := gasTestRunner(` + package main + + import ( + "log" + "os/exec" + ) + + func main() { + cmd := exec.Command("sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) + }`, analyzer) + + checkTestResults(t, issues, 1, "Subprocess launching with partial path.") +} diff --git a/rules/tempfiles.go b/rules/tempfiles.go new file mode 100644 index 0000000..0b04a21 --- /dev/null +++ b/rules/tempfiles.go @@ -0,0 +1,50 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +type BadTempFile struct { + gas.MetaData + args *regexp.Regexp + call *regexp.Regexp +} + +func (t *BadTempFile) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCall(n, t.call); node != nil { + if arg, _ := gas.GetString(node.Args[0]); t.args.MatchString(arg) { + return gas.NewIssue(c, n, t.What, t.Severity, t.Confidence), nil + } + } + return nil, nil +} + +func NewBadTempFile() (r gas.Rule, n ast.Node) { + r = &BadTempFile{ + call: regexp.MustCompile("ioutil.WriteFile|os.Create"), + args: regexp.MustCompile("^/tmp/.*$|^/var/tmp/.*$"), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "File creation in shared tmp directory without using ioutil.Tempfile", + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/tempfiles_test.go b/rules/tempfiles_test.go new file mode 100644 index 0000000..911e644 --- /dev/null +++ b/rules/tempfiles_test.go @@ -0,0 +1,45 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestTempfiles(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewBadTempFile()) + + source := ` + package samples + + import ( + "io/ioutil" + "os" + ) + + func main() { + + file1, _ := os.Create("/tmp/demo1") + defer file1.Close() + + ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644) + } + ` + + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 2, "shared tmp directory") +} diff --git a/rules/templates.go b/rules/templates.go new file mode 100644 index 0000000..eb59cac --- /dev/null +++ b/rules/templates.go @@ -0,0 +1,50 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +type TemplateCheck struct { + gas.MetaData + call *regexp.Regexp +} + +func (t *TemplateCheck) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCall(n, t.call); node != nil { + for _, arg := range node.Args { + if _, ok := arg.(*ast.BasicLit); !ok { // basic lits are safe + return gas.NewIssue(c, n, t.What, t.Severity, t.Confidence), nil + } + } + } + return nil, nil +} + +func NewTemplateCheck() (r gas.Rule, n ast.Node) { + r = &TemplateCheck{ + call: regexp.MustCompile("^template.(HTML|JS|URL)$"), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.Low, + What: "this method will not auto-escape HTML. Verify data is well formed.", + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/templates_test.go b/rules/templates_test.go new file mode 100644 index 0000000..c969182 --- /dev/null +++ b/rules/templates_test.go @@ -0,0 +1,131 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestTemplateCheckSafe(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewTemplateCheck()) + + source := ` + package samples + + import ( + "html/template" + "os" + ) + + const tmpl = "" + + func main() { + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.HTML(""), + } + t.Execute(os.Stdout, v) + }` + + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 0, "this method will not auto-escape HTML. Verify data is well formed") +} + +func TestTemplateCheckBadHTML(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewTemplateCheck()) + + source := ` + package samples + + import ( + "html/template" + "os" + ) + + const tmpl = "" + + func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.HTML(a), + } + t.Execute(os.Stdout, v) + }` + + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed") +} + +func TestTemplateCheckBadJS(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewTemplateCheck()) + + source := ` + package samples + + import ( + "html/template" + "os" + ) + + const tmpl = "" + + func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.JS(a), + } + t.Execute(os.Stdout, v) + }` + + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed") +} + +func TestTemplateCheckBadURL(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewTemplateCheck()) + + source := ` + package samples + + import ( + "html/template" + "os" + ) + + const tmpl = "" + + func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.URL(a), + } + t.Execute(os.Stdout, v) + }` + + issues := gasTestRunner(source, analyzer) + checkTestResults(t, issues, 1, "this method will not auto-escape HTML. Verify data is well formed") +} diff --git a/rules/tls.go b/rules/tls.go new file mode 100644 index 0000000..3faafd9 --- /dev/null +++ b/rules/tls.go @@ -0,0 +1,185 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "fmt" + "go/ast" + "reflect" + "regexp" + + gas "github.com/HewlettPackard/gas/core" +) + +type InsecureConfigTLS struct { + MinVersion int16 + MaxVersion int16 + pattern *regexp.Regexp + goodCiphers []string +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func (t *InsecureConfigTLS) processTlsCipherSuites(n ast.Node, c *gas.Context) *gas.Issue { + a := reflect.TypeOf(&ast.KeyValueExpr{}) + b := reflect.TypeOf(&ast.CompositeLit{}) + if node, ok := gas.SimpleSelect(n, a, b).(*ast.CompositeLit); ok { + for _, elt := range node.Elts { + if ident, ok := elt.(*ast.SelectorExpr); ok { + if !stringInSlice(ident.Sel.Name, t.goodCiphers) { + str := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name) + return gas.NewIssue(c, n, str, gas.High, gas.High) + } + } + } + } + return nil +} + +func (t *InsecureConfigTLS) processTlsConfVal(n *ast.KeyValueExpr, c *gas.Context) *gas.Issue { + if ident, ok := n.Key.(*ast.Ident); ok { + switch ident.Name { + case "InsecureSkipVerify": + if node, ok := n.Value.(*ast.Ident); ok { + if node.Name != "false" { + return gas.NewIssue(c, n, "TLS InsecureSkipVerify set true.", gas.High, gas.High) + } + } else { + // TODO(tk): symbol tab look up to get the actual value + return gas.NewIssue(c, n, "TLS InsecureSkipVerify may be true.", gas.High, gas.Low) + } + + case "MinVersion": + if ival, ierr := gas.GetInt(n.Value); ierr == nil { + if (int16)(ival) < t.MinVersion { + return gas.NewIssue(c, n, "TLS MinVersion too low.", gas.High, gas.High) + } + // TODO(tk): symbol tab look up to get the actual value + return gas.NewIssue(c, n, "TLS MinVersion may be too low.", gas.High, gas.Low) + } + + case "MaxVersion": + if ival, ierr := gas.GetInt(n.Value); ierr == nil { + if (int16)(ival) < t.MaxVersion { + return gas.NewIssue(c, n, "TLS MaxVersion too low.", gas.High, gas.High) + } + // TODO(tk): symbol tab look up to get the actual value + return gas.NewIssue(c, n, "TLS MaxVersion may be too low.", gas.High, gas.Low) + } + + case "CipherSuites": + if ret := t.processTlsCipherSuites(n, c); ret != nil { + return ret + } + } + } + return nil +} + +func (t *InsecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCompLit(n, t.pattern); node != nil { + for _, elt := range node.Elts { + if kve, ok := elt.(*ast.KeyValueExpr); ok { + gi = t.processTlsConfVal(kve, c) + if gi != nil { + break + } + } + } + } + return +} + +func NewModernTlsCheck() (r gas.Rule, n ast.Node) { + // https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility + r = &InsecureConfigTLS{ + pattern: regexp.MustCompile("^tls.Config$"), + MinVersion: 0x0303, // TLS 1.2 only + MaxVersion: 0x0303, + goodCiphers: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + }, + } + n = (*ast.CompositeLit)(nil) + return +} + +func NewIntermediateTlsCheck() (r gas.Rule, n ast.Node) { + // https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 + r = &InsecureConfigTLS{ + pattern: regexp.MustCompile("^tls.Config$"), + MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 + MaxVersion: 0x0303, + goodCiphers: []string{ + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + }, + } + n = (*ast.CompositeLit)(nil) + return +} + +func NewCompatTlsCheck() (r gas.Rule, n ast.Node) { + // https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29 + r = &InsecureConfigTLS{ + pattern: regexp.MustCompile("^tls.Config$"), + MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 + MaxVersion: 0x0303, + goodCiphers: []string{ + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + }, + } + n = (*ast.CompositeLit)(nil) + return +} diff --git a/rules/tls_test.go b/rules/tls_test.go new file mode 100644 index 0000000..0c103b0 --- /dev/null +++ b/rules/tls_test.go @@ -0,0 +1,136 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "testing" + + gas "github.com/HewlettPackard/gas/core" +) + +func TestInsecureSkipVerify(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewModernTlsCheck()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/tls" + "fmt" + "net/http" + ) + + func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } + } + `, analyzer) + + checkTestResults(t, issues, 1, "TLS InsecureSkipVerify set true") +} + +func TestInsecureMinVersion(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewModernTlsCheck()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/tls" + "fmt" + "net/http" + ) + + func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{MinVersion: 0}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } + } + `, analyzer) + + checkTestResults(t, issues, 1, "TLS MinVersion too low") +} + +func TestInsecureMaxVersion(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewModernTlsCheck()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/tls" + "fmt" + "net/http" + ) + + func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{MaxVersion: 0}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } + } + `, analyzer) + + checkTestResults(t, issues, 1, "TLS MaxVersion too low") +} + +func TestInsecureCipherSuite(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewModernTlsCheck()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/tls" + "fmt" + "net/http" + ) + + func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_DERP, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + },}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } + } + `, analyzer) + + checkTestResults(t, issues, 1, "TLS Bad Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_DERP") +} diff --git a/rules/unsafe.go b/rules/unsafe.go new file mode 100644 index 0000000..bf0313c --- /dev/null +++ b/rules/unsafe.go @@ -0,0 +1,46 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "regexp" +) + +type UsingUnsafe struct { + gas.MetaData + pattern *regexp.Regexp +} + +func (r *UsingUnsafe) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + if node := gas.MatchCall(n, r.pattern); node != nil { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + return nil, nil +} + +func NewUsingUnsafe() (r gas.Rule, n ast.Node) { + r = &UsingUnsafe{ + pattern: regexp.MustCompile("unsafe.*"), + MetaData: gas.MetaData{ + What: "Use of unsafe calls should be audited", + Severity: gas.Low, + Confidence: gas.High, + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/unsafe_test.go b/rules/unsafe_test.go new file mode 100644 index 0000000..326a430 --- /dev/null +++ b/rules/unsafe_test.go @@ -0,0 +1,47 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestUnsafe(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewUsingUnsafe()) + + issues := gasTestRunner(` + package main + + import ( + "fmt" + "unsafe" + ) + + func main() { + intArray := [...]int{1, 2} + fmt.Printf("\nintArray: %v\n", intArray) + intPtr := &intArray[0] + fmt.Printf("\nintPtr=%p, *intPtr=%d.\n", intPtr, *intPtr) + addressHolder := uintptr(unsafe.Pointer(intPtr)) + unsafe.Sizeof(intArray[0]) + intPtr = (*int)(unsafe.Pointer(addressHolder)) + fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr) + } + `, analyzer) + + checkTestResults(t, issues, 3, "Use of unsafe calls") + +} diff --git a/rules/utils_test.go b/rules/utils_test.go new file mode 100644 index 0000000..a7eda4e --- /dev/null +++ b/rules/utils_test.go @@ -0,0 +1,40 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + "strings" + "testing" + + gas "github.com/HewlettPackard/gas/core" +) + +func gasTestRunner(source string, analyzer gas.Analyzer) []gas.Issue { + analyzer.ProcessSource("dummy.go", source) + return analyzer.Issues +} + +func checkTestResults(t *testing.T, issues []gas.Issue, expected int, msg string) { + found := len(issues) + if found != expected { + t.Errorf("Found %d issues, expected %d", found, expected) + } + + for _, issue := range issues { + if !strings.Contains(issue.What, msg) { + t.Errorf("Unexpected issue identified: %s", issue.What) + } + } +} diff --git a/rules/weakcrypto.go b/rules/weakcrypto.go new file mode 100644 index 0000000..59886b2 --- /dev/null +++ b/rules/weakcrypto.go @@ -0,0 +1,78 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "go/ast" + "reflect" + "regexp" +) + +type ImportsWeakCryptography struct { + gas.MetaData + pattern *regexp.Regexp +} + +func (r *ImportsWeakCryptography) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { + a := reflect.TypeOf(&ast.ImportSpec{}) + b := reflect.TypeOf(&ast.BasicLit{}) + if node := gas.SimpleSelect(n, a, b); node != nil { + if str, _ := gas.GetString(node); r.pattern.MatchString(str) { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + } + return +} + +// Imports crypto/md5, crypto/des crypto/rc4 +func NewImportsWeakCryptography() (r gas.Rule, n ast.Node) { + r = &ImportsWeakCryptography{ + pattern: regexp.MustCompile("crypto/md5|crypto/des|crypto/rc4"), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "Import of weak cryptographic primitive", + }, + } + n = (*ast.ImportSpec)(nil) + return +} + +type UsesWeakCryptography struct { + gas.MetaData + pattern *regexp.Regexp +} + +func (r *UsesWeakCryptography) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + if node := gas.MatchCall(n, r.pattern); node != nil { + return gas.NewIssue(c, n, r.What, r.Severity, r.Confidence), nil + } + return nil, nil +} + +// Uses des.* md5.* or rc4.* +func NewUsesWeakCryptography() (r gas.Rule, n ast.Node) { + r = &UsesWeakCryptography{ + pattern: regexp.MustCompile("des.NewCipher|des.NewTripleDESCipher|md5.New|md5.Sum|rc4.NewCipher"), + MetaData: gas.MetaData{ + Severity: gas.Medium, + Confidence: gas.High, + What: "Use of weak cryptographic primitive", + }, + } + n = (*ast.CallExpr)(nil) + return +} diff --git a/rules/weakcrypto_test.go b/rules/weakcrypto_test.go new file mode 100644 index 0000000..774d51b --- /dev/null +++ b/rules/weakcrypto_test.go @@ -0,0 +1,110 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rules + +import ( + gas "github.com/HewlettPackard/gas/core" + "testing" +) + +func TestMD5(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewImportsWeakCryptography()) + analyzer.AddRule(NewUsesWeakCryptography()) + + issues := gasTestRunner(` + package main + import ( + "crypto/md5" + "fmt" + "os" + ) + func main() { + for _, arg := range os.Args { + fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg) + } + } + `, analyzer) + checkTestResults(t, issues, 2, "weak cryptographic") +} + +func TestDES(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewImportsWeakCryptography()) + analyzer.AddRule(NewUsesWeakCryptography()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/cipher" + "crypto/des" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + ) + + func main() { + block, err := des.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, des.BlockSize+len(plaintext)) + iv := ciphertext[:des.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic(err) + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[des.BlockSize:], plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) + } + `, analyzer) + + checkTestResults(t, issues, 2, "weak cryptographic") +} + +func TestRC4(t *testing.T) { + analyzer := gas.NewAnalyzer(false, nil) + analyzer.AddRule(NewImportsWeakCryptography()) + analyzer.AddRule(NewUsesWeakCryptography()) + + issues := gasTestRunner(` + package main + + import ( + "crypto/rc4" + "encoding/hex" + "fmt" + ) + + func main() { + cipher, err := rc4.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, len(plaintext)) + cipher.XORKeyStream(ciphertext, plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) + } + `, analyzer) + + checkTestResults(t, issues, 2, "weak cryptographic") +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..315339b --- /dev/null +++ b/tools.go @@ -0,0 +1,77 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "strings" +) + +type command func(args ...string) +type utilities struct { + commands map[string]command + call []string +} + +// Custom commands / utilities to run instead of default analyzer +func newUtils() *utilities { + utils := make(map[string]command) + utils["dump"] = dumpAst + return &utilities{utils, make([]string, 0)} +} + +func (u *utilities) String() string { + i := 0 + keys := make([]string, len(u.commands)) + for k := range u.commands { + keys[i] = k + i++ + } + return strings.Join(keys, ", ") +} + +func (u *utilities) Set(opt string) error { + if _, ok := u.commands[opt]; !ok { + return fmt.Errorf("valid tools are: %s", u.String()) + + } + u.call = append(u.call, opt) + return nil +} + +func (u *utilities) run(args ...string) { + for _, util := range u.call { + if cmd, ok := u.commands[util]; ok { + cmd(args...) + } + } +} + +func dumpAst(files ...string) { + for _, arg := range files { + // Create the AST by parsing src. + fset := token.NewFileSet() // positions are relative to fset + f, err := parser.ParseFile(fset, arg, nil, 0) + if err != nil { + panic(err) + } + + // Print the AST. + ast.Print(fset, f) + } +}