Repository: google/go-safeweb (go)

Issue:
A linter for disallowing imports and function calls
This is a subset of #116.

## Requirements

*   banning imports (with an allowlist for exceptions)
    *   e.g. don't allow calling `github.com/google/go-safeweb/safesql/legacyconversions`. Note that safesql must be allowed to import “sql”.
*   banning function calls (with an allowlist for exceptions)
    *   e.g. don't allow calling `safehttp.NewServeMuxConfig`
*   allowlists live in a configuration file (JSON)

## Suggested Usage

```
# sql.json
{
  "imports": {
    "github.com/google/go-safeweb/safesql/legacyconversions": {
      "message": "To use legacyconversions contact security@mycompany.org",
      "exemptions": [
        {
          "justification": "...",
          "allowed": "mycompany.com/my/project/vetted",
        },
        {
          "justification": "...",
          "allowed": "mycompany.com/my/subdirs/vetted/...",
        },
      ],
    }
  },
}
```

```
# mux.json
{
  "calls": {
    "github.com/google/go-safeweb/safehttp.NewServeMuxConfig": [
      "message": "Use mycompany.com/my/project/vetted.NewServeMuxConfig instead.",
      "exemptions": [
        {
          "justification": "...",
          "allowed": "mycompany.com/my/project/vetted",
        },
        {
          "justification": "...",
          "allowed": "mycompany.com/my/subdirs/vetted/...",
        },
      ],
    }
  },
}
```

(those two files could be merged, there is no requirement to have one directive per file)

```
$ bancheck -c sql.json -c mux.json ./…
```

## Useful resources

The Go analysis framework: https://pkg.go.dev/golang.org/x/tools/go/analysis. A talk about it: https://www.youtube.com/watch?v=pOl82CwQp50

The tool should likely live under `cmd/bancheck`.






Pull request:
Add linter for banned imports and function calls
Adds an analyser that checks for usage of banned APIs.
Fixes #270 

Patch:
diff --git a/cmd/bancheck/bannedapi/banned_api.go b/cmd/bancheck/bannedapi/banned_api.go
new file mode 100644
index 0000000..77dc5be
--- /dev/null
+++ b/cmd/bancheck/bannedapi/banned_api.go
@@ -0,0 +1,135 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+// 	https://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 bannedapi provides the tools for doing static analysis
+// and checking for usage of banned APIs.
+package bannedapi
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"go/token"
+	"go/types"
+	"path/filepath"
+	"strings"
+
+	"github.com/google/go-safeweb/cmd/bancheck/config"
+	"golang.org/x/tools/go/analysis"
+)
+
+// NewAnalyzer returns an analyzer that checks for usage of banned APIs.
+func NewAnalyzer() *analysis.Analyzer {
+	fs := flag.NewFlagSet("", flag.ExitOnError)
+	fs.String("configs", "", "Config files with banned APIs separated by a comma")
+
+	a := &analysis.Analyzer{
+		Name:  "bannedAPI",
+		Doc:   "Checks for usage of banned APIs",
+		Run:   checkBannedAPIs,
+		Flags: *fs,
+	}
+
+	return a
+}
+
+func checkBannedAPIs(pass *analysis.Pass) (interface{}, error) {
+	cfgFiles := pass.Analyzer.Flags.Lookup("configs").Value.String()
+	if cfgFiles == "" {
+		return nil, errors.New("missing config files")
+	}
+
+	cfg, err := config.ReadConfigs(strings.Split(cfgFiles, ","))
+	if err != nil {
+		return nil, err
+	}
+
+	checkBannedImports(pass, bannedAPIMap(cfg.Imports))
+	checkBannedFunctions(pass, bannedAPIMap(cfg.Functions))
+
+	return nil, nil
+}
+
+func checkBannedImports(pass *analysis.Pass, bannedImports map[string][]config.BannedAPI) (interface{}, error) {
+	for _, f := range pass.Files {
+		for _, i := range f.Imports {
+			importName := strings.Trim(i.Path.Value, "\"")
+			err := reportIfBanned(importName, bannedImports, i.Pos(), pass)
+			if err != nil {
+				return false, err
+			}
+		}
+	}
+	return nil, nil
+}
+
+func checkBannedFunctions(pass *analysis.Pass, bannedFns map[string][]config.BannedAPI) (interface{}, error) {
+	for id, obj := range pass.TypesInfo.Uses {
+		fn, ok := obj.(*types.Func)
+		if !ok {
+			continue
+		}
+
+		fnName := fmt.Sprintf("%s.%s", fn.Pkg().Path(), fn.Name())
+		err := reportIfBanned(fnName, bannedFns, id.Pos(), pass)
+		if err != nil {
+			return false, err
+		}
+	}
+	return nil, nil
+}
+
+func reportIfBanned(apiName string, bannedAPIs map[string][]config.BannedAPI, position token.Pos, pass *analysis.Pass) error {
+	for _, banCfg := range bannedAPIs[apiName] {
+		if apiName != banCfg.Name {
+			return nil
+		}
+		pkgAllowed, err := isPkgAllowed(pass.Pkg, banCfg)
+		if err != nil {
+			return err
+		}
+		if pkgAllowed {
+			continue
+		}
+		pass.Report(analysis.Diagnostic{
+			Pos:     position,
+			Message: fmt.Sprintf("Banned API found %q. Additional info: %s", apiName, banCfg.Msg),
+		})
+	}
+	return nil
+}
+
+// isPkgAllowed checks if the Go package should be exempted from reporting banned API usages.
+func isPkgAllowed(pkg *types.Package, bannedAPI config.BannedAPI) (bool, error) {
+	for _, e := range bannedAPI.Exemptions {
+		match, err := filepath.Match(e.AllowedPkg, pkg.Path())
+		if err != nil {
+			return false, err
+		}
+		if match {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// bannedAPIMap builds a mapping of fully qualified API name to a list of
+// all its config.BannedAPI entries.
+func bannedAPIMap(bannedAPIs []config.BannedAPI) map[string][]config.BannedAPI {
+	m := make(map[string][]config.BannedAPI)
+	for _, API := range bannedAPIs {
+		m[API.Name] = append(m[API.Name], API)
+	}
+	return m
+}
diff --git a/cmd/bancheck/config/config.go b/cmd/bancheck/config/config.go
index 10a3a95..f15c38e 100644
--- a/cmd/bancheck/config/config.go
+++ b/cmd/bancheck/config/config.go
@@ -11,6 +11,7 @@
 // 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 config
 
 import (
@@ -35,7 +36,7 @@ type BannedAPI struct {
 // Exemption represents a location that should be exempted from checking for banned APIs.
 type Exemption struct {
 	Justification string `json:"justification"`
-	AllowedDir    string `json:"allowedDir"`
+	AllowedPkg    string `json:"allowedPkg"` // Uses Go RegExp https://golang.org/pkg/regexp/syntax
 }
 
 // ReadConfigs reads banned APIs from all files.
diff --git a/cmd/bancheck/main.go b/cmd/bancheck/main.go
new file mode 100644
index 0000000..585112f
--- /dev/null
+++ b/cmd/bancheck/main.go
@@ -0,0 +1,24 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+// 	https://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 (
+	"github.com/google/go-safeweb/cmd/bancheck/bannedapi"
+	"golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() {
+	singlechecker.Main(bannedapi.NewAnalyzer())
+}


Test patch:
diff --git a/cmd/bancheck/bannedapi/banned_api_test.go b/cmd/bancheck/bannedapi/banned_api_test.go
new file mode 100644
index 0000000..cfc0610
--- /dev/null
+++ b/cmd/bancheck/bannedapi/banned_api_test.go
@@ -0,0 +1,282 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+// 	https://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 bannedapi
+
+import (
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+)
+
+func TestBannedAPIAnalyzer(t *testing.T) {
+	for _, test := range []struct {
+		desc  string
+		files map[string]string
+	}{
+		{
+			desc: "No banned APIs",
+			files: map[string]string{
+				"config.json": `
+				{}
+				`,
+				"main/test.go": `
+				package main;
+				func main() {}
+				`,
+			},
+		},
+		{
+			desc: "Banned APIs exist",
+			files: map[string]string{
+				"config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A"
+						}
+					],
+					"imports": [
+						{
+							"name": "fmt",
+							"msg": "Banned by team A"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import "fmt" // want "Banned API found \"fmt\". Additional info: Banned by team A"
+
+				func main() {
+					fmt.Printf("Hello") // want "Banned API found \"fmt.Printf\". Additional info: Banned by team A"
+				}
+				`,
+			},
+		},
+		{
+			desc: "Banned APIs in exempted package",
+			files: map[string]string{
+				"config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A",
+							"exemptions": [
+								{
+									"justification": "#yolo",
+									"allowedPkg": "main"
+								}
+							]
+						}
+					],
+					"imports": [
+						{
+							"name": "fmt",
+							"msg": "Banned by team A",
+							"exemptions": [
+								{
+									"justification": "#yolo",
+									"allowedPkg": "main"
+								}
+							]
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import "fmt"
+
+				func main() {
+					fmt.Printf("Hello")
+				}
+				`,
+			},
+		},
+		{
+			desc: "Banned renamed import",
+			files: map[string]string{
+				"config.json": `
+				{
+					"imports": [
+						{
+							"name": "fmt",
+							"msg": "Banned by team A"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import renamed "fmt" // want "Banned API found \"fmt\". Additional info: Banned by team A"
+
+				func main() {
+					renamed.Printf("Hello")
+				}
+				`,
+			},
+		},
+		{
+			desc: "Banned function from renamed import",
+			files: map[string]string{
+				"config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import renamed "fmt"
+
+				func main() {
+					renamed.Printf("Hello") // want "Banned API found \"fmt.Printf\". Additional info: Banned by team A"
+				}
+				`,
+			},
+		},
+		{
+			desc: "Package and function name collission",
+			files: map[string]string{
+				"config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				type Foo struct{}
+
+				func (f *Foo) Printf(txt string) {}
+
+				func main() {
+					var fmt = &Foo{}
+					fmt.Printf("Hello")
+				}
+				`,
+			},
+		},
+		{
+			desc: "Banned API from multiple config files",
+			files: map[string]string{
+				"team_a_config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A"
+						}
+					]
+				}
+				`,
+				"team_b_config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team B"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import "fmt"
+
+				func main() {
+					fmt.Printf("Hello") // want "Banned API found \"fmt.Printf\". Additional info: Banned by team A" "Banned API found \"fmt.Printf\". Additional info: Banned by team B"
+				}
+				`,
+			},
+		},
+		{
+			desc: "Banned API in one of many config files",
+			files: map[string]string{
+				"team_a_config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team A",
+							"exemptions": [
+								{
+									"justification": "#yolo",
+									"allowedPkg": "main"
+								}
+							]
+						}
+					]
+				}
+				`,
+				"team_b_config.json": `
+				{
+					"functions": [
+						{
+							"name": "fmt.Printf",
+							"msg": "Banned by team B for realz"
+						}
+					]
+				}
+				`,
+				"main/test.go": `
+				package main
+
+				import "fmt"
+
+				func main() {
+					fmt.Printf("Hello") // want "Banned API found \"fmt.Printf\". Additional info: Banned by team B for realz"
+				}
+				`,
+			},
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			dir, cleanup, err := analysistest.WriteFiles(test.files)
+			if err != nil {
+				t.Fatalf("WriteFiles() returned err: %v", err)
+			}
+			defer cleanup()
+
+			var configFiles []string
+			for name := range test.files {
+				if strings.HasSuffix(name, "config.json") {
+					configFiles = append(configFiles, filepath.Join(dir, "src", name))
+				}
+			}
+
+			a := NewAnalyzer()
+			a.Flags.Set("configs", strings.Join(configFiles, ","))
+			analysistest.Run(t, dir, a, "main")
+		})
+	}
+}
diff --git a/cmd/bancheck/config/config_test.go b/cmd/bancheck/config/config_test.go
index 5141ba6..431912b 100644
--- a/cmd/bancheck/config/config_test.go
+++ b/cmd/bancheck/config/config_test.go
@@ -58,7 +58,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Sample message",
 						"exemptions": [{
 							"justification": "My justification",
-							"allowedDir": "subdirs/vetted/..."
+							"allowedPkg": "subdirs/vetted/..."
 						}]
 					}]
 				}
@@ -71,7 +71,7 @@ func TestReadConfigs(t *testing.T) {
 					Exemptions: []Exemption{
 						{
 							Justification: "My justification",
-							AllowedDir:    "subdirs/vetted/...",
+							AllowedPkg:    "subdirs/vetted/...",
 						},
 					},
 				}},
@@ -111,7 +111,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Sample message",
 						"exemptions": [{
 							"justification": "My justification",
-							"allowedDir": "subdirs/vetted/..."
+							"allowedPkg": "subdirs/vetted/..."
 						}]
 					}]
 				}
@@ -123,7 +123,7 @@ func TestReadConfigs(t *testing.T) {
 				Exemptions: []Exemption{
 					{
 						Justification: "My justification",
-						AllowedDir:    "subdirs/vetted/...",
+						AllowedPkg:    "subdirs/vetted/...",
 					},
 				},
 			}}},
@@ -138,7 +138,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Sample message",
 						"exemptions": [{
 							"justification": "My justification",
-							"allowedDir": "subdirs/vetted/..."
+							"allowedPkg": "subdirs/vetted/..."
 						}]
 					}],
 					"functions": [{
@@ -146,7 +146,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Sample message",
 						"exemptions": [{
 							"justification": "My justification",
-							"allowedDir": "subdirs/vetted/..."
+							"allowedPkg": "subdirs/vetted/..."
 						}]
 					}]
 				}
@@ -160,7 +160,7 @@ func TestReadConfigs(t *testing.T) {
 						Exemptions: []Exemption{
 							{
 								Justification: "My justification",
-								AllowedDir:    "subdirs/vetted/...",
+								AllowedPkg:    "subdirs/vetted/...",
 							},
 						},
 					}},
@@ -170,7 +170,7 @@ func TestReadConfigs(t *testing.T) {
 					Exemptions: []Exemption{
 						{
 							Justification: "My justification",
-							AllowedDir:    "subdirs/vetted/...",
+							AllowedPkg:    "subdirs/vetted/...",
 						},
 					},
 				}},
@@ -210,7 +210,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Banned by team x",
 						"exemptions": [{
 							"justification": "My justification",
-							"allowedDir": "subdirs/vetted/..."
+							"allowedPkg": "subdirs/vetted/..."
 						}]
 					}]
 				}
@@ -222,7 +222,7 @@ func TestReadConfigs(t *testing.T) {
 						"msg": "Banned by team y",
 						"exemptions": [{
 							"justification": "#yolo",
-							"allowedDir": "otherdir/legacy/..."
+							"allowedPkg": "otherdir/legacy/..."
 						}]
 					}]
 				}
@@ -236,7 +236,7 @@ func TestReadConfigs(t *testing.T) {
 						Exemptions: []Exemption{
 							{
 								Justification: "My justification",
-								AllowedDir:    "subdirs/vetted/...",
+								AllowedPkg:    "subdirs/vetted/...",
 							},
 						},
 					},
@@ -246,7 +246,7 @@ func TestReadConfigs(t *testing.T) {
 						Exemptions: []Exemption{
 							{
 								Justification: "#yolo",
-								AllowedDir:    "otherdir/legacy/...",
+								AllowedPkg:    "otherdir/legacy/...",
 							},
 						},
 					},