Initial public release

This commit is contained in:
Tim Kelsey 2016-07-20 11:02:01 +01:00
commit 4f3d620d37
37 changed files with 3357 additions and 0 deletions

26
.gitignore vendored Normal file
View file

@ -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

154
LICENSE.txt Normal file
View file

@ -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

84
README.md Normal file
View file

@ -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
```

152
core/analyzer.go Normal file
View file

@ -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
}

82
core/helpers.go Normal file
View file

@ -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)
}

96
core/issue.go Normal file
View file

@ -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,
}
}

401
core/select.go Normal file
View file

@ -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
}

46
filelist.go Normal file
View file

@ -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
}

158
main.go Normal file
View file

@ -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)
}
}

95
output/formatter.go Normal file
View file

@ -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)
}

99
rulelist.go Normal file
View file

@ -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)
}

53
rules/bind.go Normal file
View file

@ -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
}

62
rules/bind_test.go Normal file
View file

@ -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")
}

63
rules/errors.go Normal file
View file

@ -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
}

87
rules/errors_test.go Normal file
View file

@ -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, "")
}

67
rules/fileperms.go Normal file
View file

@ -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
}

51
rules/fileperms_test.go Normal file
View file

@ -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")
}

View file

@ -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
}

View file

@ -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")
}

53
rules/rsa.go Normal file
View file

@ -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
}

48
rules/rsa_test.go Normal file
View file

@ -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")
}

89
rules/sql.go Normal file
View file

@ -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
}

146
rules/sql_test.go Normal file
View file

@ -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")
}

59
rules/subproc.go Normal file
View file

@ -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
}

99
rules/subproc_test.go Normal file
View file

@ -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.")
}

50
rules/tempfiles.go Normal file
View file

@ -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
}

45
rules/tempfiles_test.go Normal file
View file

@ -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")
}

50
rules/templates.go Normal file
View file

@ -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
}

131
rules/templates_test.go Normal file
View file

@ -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 <b>World</b>",
"Body": template.HTML("<script>alert(1)</script>"),
}
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 <b>World</b>",
"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 <b>World</b>",
"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 <b>World</b>",
"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")
}

185
rules/tls.go Normal file
View file

@ -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
}

136
rules/tls_test.go Normal file
View file

@ -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")
}

46
rules/unsafe.go Normal file
View file

@ -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
}

47
rules/unsafe_test.go Normal file
View file

@ -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")
}

40
rules/utils_test.go Normal file
View file

@ -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)
}
}
}

78
rules/weakcrypto.go Normal file
View file

@ -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
}

110
rules/weakcrypto_test.go Normal file
View file

@ -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")
}

77
tools.go Normal file
View file

@ -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)
}
}