Repository: google/go-flow-levee
Pull request #66 (2020-08-26 15:16:19)

Handle sources inlined in sink calls (and other similar cases)
## The bug
Consider the following piece of code:
```go
func CreateSource() core.Source {
	return core.Source{}
}

func TestWrappedCall() {
	core.Sink(CreateSource()) // want "a source has reached a sink"
}

func TestNonWrappedCall() {
	s := CreateSource()
	core.Sink(s) // want "a source has reached a sink"
}
```

Certainly, it should not matter whether the call to `CreateSource` is inlined into the call to `core.Sink`, right? Unfortunately, it does. The reason why is plain to see in the SSA:

This is the inlined version:
```
0: entry
	0(*ssa.Call           ): t0 = CreateSource()
	...
	(instructions setting up the call to core.Sink)
	...
	6(*ssa.Call           ): t5 = example.com/core.Sink(t4...)
	7(*ssa.Return         ): return
```

This is the non-inlined version:
```
0: entry
	0(*ssa.Alloc          ): t0 = local example.com/core.Source (s)
	1(*ssa.Call           ): t1 = CreateSource()
	...
	(the remainder is identical to the inlined version)
```

(The inlined version does not have an Alloc instruction for the `Source`, therefore we do not detect the `Source` at all.)

Similar problems occur for the following when they are inlined:
- getting a source from an array by index
- getting a source from a map by key
- getting a source from a channel

There are probably other cases that I haven't though of.

## The fix
The fix I propose is simply to handle more types of value-creating instructions when we scan instructions in `source.go:sourcesFromBlocks`.

## Discussion
Some of these test cases are a little bit "weird" in the sense that it would be weird that a user would write such code. For example, it is unlikely that a user will call a source-producing function right into a call to a sink. However, such code is still incorrect and therefore we should produce a report for it.

- [x] Tests pass
- [x] Appropriate changes to README are included in PR


Patch:
diff --git a/.travis.yml b/.travis.yml
index 9cfc008..178a5ba 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,3 +34,4 @@ script:
       false
     fi
   )
+- echo "Checking json..." && find . -name '*.json' -exec jq type {} + &>/dev/null
diff --git a/configuration/example-config.json b/configuration/example-config.json
index 4a86565..26c228e 100644
--- a/configuration/example-config.json
+++ b/configuration/example-config.json
@@ -28,5 +28,5 @@
       "MethodRE": "Info|Warning|Error|Fatal|Exit"
     }
   ],
-  "Sanitizers": [],
+  "Sanitizers": []
 }
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 6d3851a..58a4379 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -53,12 +53,25 @@ func (c Config) IsSink(call *ssa.Call) bool {
 }
 
 func (c Config) IsSinkFunction(f *ssa.Function) bool {
+	// according to the documentation for ssa.Function, this can happen if
+	// f is a "shared func", e.g. "wrappers and error.Error"
+	if f.Pkg == nil {
+		return false
+	}
+
+	var recvName string
+	if recv := f.Signature.Recv(); recv != nil {
+		recvName = recv.Type().String()
+	}
+
 	for _, p := range c.Sinks {
-		if p.MethodRE.MatchString(f.Name()) {
+		if !p.PackageRE.MatchString(f.Pkg.Pkg.Path()) || !p.MethodRE.MatchString(f.Name()) {
+			continue
+		}
+		if p.ReceiverRE.MatchString(recvName) {
 			return true
 		}
 	}
-
 	return false
 }
 
diff --git a/internal/pkg/fieldtags/analyzer.go b/internal/pkg/fieldtags/analyzer.go
index 22ff822..93bac7a 100644
--- a/internal/pkg/fieldtags/analyzer.go
+++ b/internal/pkg/fieldtags/analyzer.go
@@ -115,6 +115,9 @@ func (sp *sourcePatterns) isSource(field *ast.Field) bool {
 		}
 
 		i = j + 1 // skip colon
+		if i >= len(tag) {
+			return false
+		}
 		if tag[i] == '\\' {
 			i++
 		}
diff --git a/internal/pkg/sanitizer/sanitizer.go b/internal/pkg/sanitizer/sanitizer.go
index 50822be..b703d45 100644
--- a/internal/pkg/sanitizer/sanitizer.go
+++ b/internal/pkg/sanitizer/sanitizer.go
@@ -57,11 +57,5 @@ func (s Sanitizer) Dominates(target ssa.Instruction) bool {
 		return sanitizationIdx < targetIdx
 	}
 
-	for _, d := range s.Call.Block().Dominees() {
-		if target.Block() == d {
-			return true
-		}
-	}
-
 	return false
 }
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index 8a8a93f..f5dff48 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -17,6 +17,7 @@ package source
 
 import (
 	"fmt"
+	"go/token"
 	"go/types"
 	"strings"
 
@@ -38,11 +39,12 @@ type classifier interface {
 // its referrers.
 // Source.sanitized notes sanitizer calls that sanitize this Source
 type Source struct {
-	node       ssa.Node
-	marked     map[ssa.Node]bool
-	preOrder   []ssa.Node
-	sanitizers []*sanitizer.Sanitizer
-	config     classifier
+	node            ssa.Node
+	marked          map[ssa.Node]bool
+	preOrder        []ssa.Node
+	sanitizers      []*sanitizer.Sanitizer
+	config          classifier
+	maxInstrReached map[*ssa.BasicBlock]int
 }
 
 // Node returns the underlying ssa.Node of the Source.
@@ -53,9 +55,10 @@ func (a *Source) Node() ssa.Node {
 // New constructs a Source
 func New(in ssa.Node, config classifier) *Source {
 	a := &Source{
-		node:   in,
-		marked: make(map[ssa.Node]bool),
-		config: config,
+		node:            in,
+		marked:          make(map[ssa.Node]bool),
+		config:          config,
+		maxInstrReached: map[*ssa.BasicBlock]int{},
 	}
 	a.dfs(in)
 	return a
@@ -68,6 +71,10 @@ func (a *Source) dfs(n ssa.Node) {
 	a.preOrder = append(a.preOrder, n)
 	a.marked[n.(ssa.Node)] = true
 
+	if instr, ok := n.(ssa.Instruction); ok {
+		a.recordIndex(instr)
+	}
+
 	if n.Referrers() != nil {
 		a.visitReferrers(n.Referrers())
 	}
@@ -79,6 +86,17 @@ func (a *Source) dfs(n ssa.Node) {
 	}
 }
 
+func (a *Source) recordIndex(target ssa.Instruction) {
+	b := target.Block()
+	i, ok := indexInBlock(target)
+	if !ok {
+		return
+	}
+	if a.maxInstrReached[b] < i {
+		a.maxInstrReached[b] = i
+	}
+}
+
 func (a *Source) visitReferrers(referrers *[]ssa.Instruction) {
 	for _, r := range *referrers {
 		if a.marked[r.(ssa.Node)] {
@@ -94,6 +112,18 @@ func (a *Source) visitReferrers(referrers *[]ssa.Instruction) {
 			if a.config.IsSanitizer(v) {
 				a.sanitizers = append(a.sanitizers, &sanitizer.Sanitizer{Call: v})
 			}
+
+			// If this call's index is lower than the highest in its block,
+			// then this call is "in the past" and we should stop traversing.
+			i, ok := indexInBlock(r)
+			if !ok {
+				break
+			}
+
+			if i < a.maxInstrReached[r.Block()] {
+				continue
+			}
+
 		case *ssa.FieldAddr:
 			if !a.config.IsSourceFieldAddr(v) {
 				continue
@@ -222,6 +252,7 @@ func sourcesFromClosure(fn *ssa.Function, conf classifier) []*Source {
 	return sources
 }
 
+// sourcesFromBlocks finds Source values created by instructions within a function's body.
 func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 	var sources []*Source
 	for _, b := range fn.Blocks {
@@ -231,26 +262,42 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 		}
 
 		for _, instr := range b.Instrs {
+			// This type switch is used to catch instructions that could produce sources.
+			// All instructions that do not match one of the cases will hit the "default"
+			// and they will not be examined any further.
 			switch v := instr.(type) {
-			// Looking for sources of PII allocated within the body of a function.
-			case *ssa.Alloc:
-				if conf.IsSource(utils.Dereference(v.Type())) && !isProducedBySanitizer(v, conf) {
-					sources = append(sources, New(v, conf))
+			// drop anything that doesn't match one of the following cases
+			default:
+				continue
+
+			// source defined as a local variable or returned from a call
+			case *ssa.Alloc, *ssa.Call:
+				// Allocs and Calls are values
+				if isProducedBySanitizer(v.(ssa.Value), conf) {
+					continue
 				}
 
-				// Handling the case where PII may be in a receiver
-				// (ex. func(b *something) { log.Info(something.PII) }
-			case *ssa.FieldAddr:
-				if conf.IsSource(utils.Dereference(v.Type())) {
-					sources = append(sources, New(v, conf))
+			// source received from chan
+			case *ssa.UnOp:
+				// not a <-chan operation
+				if v.Op != token.ARROW {
+					continue
 				}
+
+			// source obtained through a field or an index operation
+			case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.Lookup:
+			}
+
+			// all of the instructions that the switch lets through are values as per ssa/doc.go
+			if v := instr.(ssa.Value); conf.IsSource(utils.Dereference(v.Type())) {
+				sources = append(sources, New(v.(ssa.Node), conf))
 			}
 		}
 	}
 	return sources
 }
 
-func isProducedBySanitizer(v *ssa.Alloc, conf classifier) bool {
+func isProducedBySanitizer(v ssa.Value, conf classifier) bool {
 	for _, instr := range *v.Referrers() {
 		store, ok := instr.(*ssa.Store)
 		if !ok {
@@ -266,3 +313,15 @@ func isProducedBySanitizer(v *ssa.Alloc, conf classifier) bool {
 	}
 	return false
 }
+
+// indexInBlock returns this instruction's index in its parent block.
+func indexInBlock(target ssa.Instruction) (int, bool) {
+	for i, instr := range target.Block().Instrs {
+		if instr == target {
+			return i, true
+		}
+	}
+	// we can only hit this return if there is a bug in the ssa package
+	// i.e. an instruction does not appear within its parent block
+	return 0, false
+}


Test patch:
diff --git a/internal/pkg/cfa/testdata/src/example.com/cfa/cfa.go b/internal/pkg/cfa/testdata/src/example.com/cfa/cfa.go
new file mode 100644
index 0000000..ff95135
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/cfa/cfa.go
@@ -0,0 +1,109 @@
+// 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 cfa
+
+import (
+	"fmt"
+
+	"example.com/core"
+)
+
+func OneParamSinkWrapper(a interface{}) { // want OneParamSinkWrapper:"genericFunc{ sinks: <0>, taints: <<>> }"
+	core.Sink(a)
+}
+
+func TwoParamSinkWrapper(a interface{}, b interface{}) { // want TwoParamSinkWrapper:"genericFunc{ sinks: <0 1>, taints: <<> <>> }"
+	core.Sink(a)
+	core.Sink(b)
+}
+
+func OneParamSanitizedBeforeSinkCall(a interface{}) { // want OneParamSanitizedBeforeSinkCall:"genericFunc{ sinks: <>, taints: <<>> }"
+	s := core.Sanitize(a)
+	core.Sink(s)
+}
+
+func OneParamSanitizedBeforeReturn(a interface{}) interface{} { // want OneParamSanitizedBeforeReturn:"genericFunc{ sinks: <>, taints: <<>> }"
+	s := core.Sanitize(a)
+	return s
+}
+
+func OneParamTaintingOneResult(a interface{}) interface{} { // want OneParamTaintingOneResult:"genericFunc{ sinks: <>, taints: <<0>> }"
+	return a
+}
+
+func OneParamTaintingBothResults(a interface{}) (interface{}, interface{}) { // want OneParamTaintingBothResults:"genericFunc{ sinks: <>, taints: <<0 1>> }"
+	return a, a
+}
+
+func OneParamTaintingOneOfTwoResults(a interface{}) (interface{}, interface{}) { // want OneParamTaintingOneOfTwoResults:"genericFunc{ sinks: <>, taints: <<1>> }"
+	return nil, a
+}
+
+func TwoParamsEachTaintingOneResult(a interface{}, b interface{}) (interface{}, interface{}) { // want TwoParamsEachTaintingOneResult:"genericFunc{ sinks: <>, taints: <<1> <0>> }"
+	return b, a
+}
+
+func SinkWrapper(a interface{}, b interface{}) (interface{}, interface{}) { // want SinkWrapper:"genericFunc{ sinks: <0>, taints: <<> <0>> }"
+	core.Sink(a)
+	sanitized := core.Sanitize(a)
+	tainted := []interface{}{b}
+	return tainted, sanitized
+}
+
+func SinkWrapperSinkTainted(a interface{}) { // want SinkWrapperSinkTainted:"genericFunc{ sinks: <0>, taints: <<>> }"
+	tainted, _ := SinkWrapper("", a)
+	core.Sink(tainted)
+}
+
+func SinkWrapperSinkSanitized(a interface{}) { // want SinkWrapperSinkSanitized:"genericFunc{ sinks: <>, taints: <<>> }"
+	_, sanitized := SinkWrapper("", a)
+	core.Sink(sanitized)
+}
+
+func SinkWrapperWrapper(c interface{}) { // want SinkWrapperWrapper:"genericFunc{ sinks: <0>, taints: <<>> }"
+	SinkWrapper(c, "")
+}
+
+func SinkWrapperWrapperWrapper(d interface{}, e interface{}) { // want SinkWrapperWrapperWrapper:"genericFunc{ sinks: <0 1>, taints: <<> <>> }"
+	SinkWrapper(d, "d")
+	SinkWrapper(e, "e")
+}
+
+func SinksFive(a interface{}) { // want SinksFive:"genericFunc{ sinks: <>, taints: <<>> }"
+	five := ReturnsFive(a)
+	core.Sink(five)
+}
+
+func ReturnsFive(a interface{}) int { // want ReturnsFive:"genericFunc{ sinks: <>, taints: <<>> }"
+	return 5
+}
+
+func SinksThroughIdentity(a interface{}) { // want SinksThroughIdentity:"genericFunc{ sinks: <0>, taints: <<>> }"
+	i := Identity(a)
+	core.Sink(i)
+}
+
+func Identity(a interface{}) interface{} { // want Identity:"genericFunc{ sinks: <>, taints: <<0>> }"
+	return a
+}
+
+func TestStringify(e interface{}) { // want TestStringify:"genericFunc{ sinks: <0>, taints: <<>> }"
+	s := Stringify(e)
+	core.Sink(s)
+}
+
+func Stringify(e interface{}) string { // want Stringify:"genericFunc{ sinks: <>, taints: <<0>> }"
+	return fmt.Sprintf("%v", e)
+}
diff --git a/internal/pkg/cfa/testdata/src/example.com/cfa/recursion.go b/internal/pkg/cfa/testdata/src/example.com/cfa/recursion.go
new file mode 100644
index 0000000..572481b
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/cfa/recursion.go
@@ -0,0 +1,51 @@
+// 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 cfa
+
+import (
+	"example.com/core"
+)
+
+func RecursiveSinkWrapper(i int, a interface{}) { // want RecursiveSinkWrapper:"genericFunc{ sinks: <1>, taints: <<> <>> }"
+	if i <= 0 {
+		core.Sink(a)
+		return
+	}
+	RecursiveSinkWrapper(i-1, a)
+}
+
+func IsEven(i int) bool { // want IsEven:"genericFunc{ sinks: <>, taints: <<0>> }"
+	return !IsOdd(i)
+}
+
+func IsOdd(i int) bool { // want IsOdd:"genericFunc{ sinks: <>, taints: <<0>> }"
+	return !IsEven(i)
+}
+
+func A(e interface{}) { // want A:"genericFunc{ sinks: <0>, taints: <<>> }"
+	B(e)
+}
+
+func B(e interface{}) { // want B:"genericFunc{ sinks: <0>, taints: <<>> }"
+	C(e)
+}
+
+func C(e interface{}) { // want C:"genericFunc{ sinks: <0>, taints: <<>> }"
+	if _, ok := e.(int); ok {
+		A(0)
+	} else {
+		core.Sink(e)
+	}
+}
diff --git a/internal/pkg/cfa/testdata/src/example.com/core/sanitize.go b/internal/pkg/cfa/testdata/src/example.com/core/sanitize.go
new file mode 100644
index 0000000..e32520c
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/core/sanitize.go
@@ -0,0 +1,23 @@
+// 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 core
+
+func Sanitize(args ...interface{}) []interface{} { // want Sanitize:"sanitizer"
+	return args
+}
+
+func SanitizeSource(s Source) Source { // want SanitizeSource:"sanitizer"
+	return Source{ID: s.ID}
+}
diff --git a/internal/pkg/cfa/testdata/src/example.com/core/sink.go b/internal/pkg/cfa/testdata/src/example.com/core/sink.go
new file mode 100644
index 0000000..2169544
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/core/sink.go
@@ -0,0 +1,25 @@
+// 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 core
+
+import "io"
+
+func Sink(args ...interface{}) {} // want Sink:"sink"
+
+func Sinkf(format string, args ...interface{}) {} // want Sinkf:"sink"
+
+func FSinkf(writer io.Writer, args ...interface{}) {} // want FSinkf:"sink"
+
+func OneArgSink(interface{}) {} // want OneArgSink:"sink"
diff --git a/internal/pkg/cfa/testdata/src/example.com/core/source.go b/internal/pkg/cfa/testdata/src/example.com/core/source.go
new file mode 100644
index 0000000..cfb578c
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/core/source.go
@@ -0,0 +1,43 @@
+// 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 core
+
+// Source will be configured to be detected as a source struct, with Source.Data as the source field.
+type Source struct {
+	Data string
+	ID   int
+}
+
+func (s Source) GetID() int {
+	return s.ID
+}
+
+func (s Source) GetData() string {
+	return s.Data
+}
+
+// Innocuous will _not_ be configured to be a source, even though underlying types are equal.
+type Innocuous struct {
+	Data string
+	ID   int
+}
+
+func (i Innocuous) GetID() int {
+	return i.ID
+}
+
+func (i Innocuous) GetData() string {
+	return i.Data
+}
diff --git a/internal/pkg/cfa/testdata/src/example.com/crosspkg/crosspkg.go b/internal/pkg/cfa/testdata/src/example.com/crosspkg/crosspkg.go
new file mode 100644
index 0000000..50b2cf6
--- /dev/null
+++ b/internal/pkg/cfa/testdata/src/example.com/crosspkg/crosspkg.go
@@ -0,0 +1,21 @@
+// 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 crosspkg
+
+import "example.com/cfa"
+
+func TestCrossPkg(e interface{}) { // want TestCrossPkg:"genericFunc{ sinks: <0>, taints: <<>> }"
+	cfa.OneParamSinkWrapper(e)
+}
diff --git a/internal/pkg/cfa/testdata/test-config.json b/internal/pkg/cfa/testdata/test-config.json
new file mode 100644
index 0000000..0664e03
--- /dev/null
+++ b/internal/pkg/cfa/testdata/test-config.json
@@ -0,0 +1,21 @@
+{
+  "Sources": [
+    {
+      "PackageRE": "^example.com/core$",
+      "TypeRE": "^Source$",
+      "FieldRE": "^Data"
+    }
+  ],
+  "Sinks": [
+    {
+      "PackageRE": "^example.com/core$",
+      "MethodRE": "Sinkf?$"
+    }
+  ],
+  "Sanitizers": [
+    {
+      "PackageRE": "^example.com/core$",
+      "MethodRE": "^Sanitize"
+    }
+  ]
+}
diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go
new file mode 100644
index 0000000..36d36d9
--- /dev/null
+++ b/internal/pkg/config/config_test.go
@@ -0,0 +1,58 @@
+// 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 config
+
+import (
+	"path/filepath"
+	"testing"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/passes/buildssa"
+)
+
+var testAnalyzer = &analysis.Analyzer{
+	Name:     "config",
+	Run:      runTest,
+	Doc:      "test harness for the logic related to config",
+	Requires: []*analysis.Analyzer{buildssa.Analyzer},
+}
+
+func runTest(pass *analysis.Pass) (interface{}, error) {
+	in := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+
+	conf, err := ReadConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, f := range in.SrcFuncs {
+		if conf.IsSinkFunction(f) {
+			pass.Reportf(f.Pos(), "sink")
+		}
+	}
+
+	return nil, nil
+}
+
+func TestConfig(t *testing.T) {
+	testdata := analysistest.TestData()
+	if err := FlagSet.Set("config", filepath.Join(testdata, "test-config.json")); err != nil {
+		t.Fatal(err)
+	}
+	for _, p := range []string{"example.com/core", "example.com/notcore", "notexample.com/core"} {
+		analysistest.Run(t, testdata, testAnalyzer, p)
+	}
+}
diff --git a/internal/pkg/config/testdata/src/example.com/core/core.go b/internal/pkg/config/testdata/src/example.com/core/core.go
new file mode 100644
index 0000000..38cd223
--- /dev/null
+++ b/internal/pkg/config/testdata/src/example.com/core/core.go
@@ -0,0 +1,25 @@
+// 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 core
+
+func Sink() {} // want "sink"
+
+func NotSink() {}
+
+type Sinker struct{}
+
+func (s Sinker) Do() {} // want "sink"
+
+func (s Sinker) DoNot() {}
diff --git a/internal/pkg/config/testdata/src/example.com/notcore/notcore.go b/internal/pkg/config/testdata/src/example.com/notcore/notcore.go
new file mode 100644
index 0000000..c469c1e
--- /dev/null
+++ b/internal/pkg/config/testdata/src/example.com/notcore/notcore.go
@@ -0,0 +1,25 @@
+// 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 notcore
+
+func Sink() {}
+
+func NotSink() {}
+
+type Sinker struct{}
+
+func (s Sinker) Do() {}
+
+func (s Sinker) DoNot() {}
diff --git a/internal/pkg/config/testdata/src/notexample.com/core/core.go b/internal/pkg/config/testdata/src/notexample.com/core/core.go
new file mode 100644
index 0000000..d56edbf
--- /dev/null
+++ b/internal/pkg/config/testdata/src/notexample.com/core/core.go
@@ -0,0 +1,25 @@
+// 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 core
+
+func Sink() {}
+
+func NotSink() {}
+
+type Sinker struct{}
+
+func (s Sinker) Do() {}
+
+func (s Sinker) DoNot() {}
diff --git a/internal/pkg/config/testdata/test-config.json b/internal/pkg/config/testdata/test-config.json
new file mode 100644
index 0000000..de3b362
--- /dev/null
+++ b/internal/pkg/config/testdata/test-config.json
@@ -0,0 +1,13 @@
+{
+  "Sinks": [
+    {
+      "PackageRE": "^example.com/core$",
+      "MethodRE": "^Sink$"
+    },
+    {
+      "PackageRE": "^example.com/core$",
+      "MethodRE": "^Do$",
+      "TypeRE": "^Sinker$"
+    }
+  ]
+}
diff --git a/internal/pkg/fieldtags/testdata/src/tests/test.go b/internal/pkg/fieldtags/testdata/src/tests/test.go
index 3c90bd8..f635d8e 100644
--- a/internal/pkg/fieldtags/testdata/src/tests/test.go
+++ b/internal/pkg/fieldtags/testdata/src/tests/test.go
@@ -15,9 +15,10 @@
 package fieldtags
 
 type Person struct {
-	password           string      `levee:"source"`               // want "tagged field: fieldtags.Person.password"
-	secret             string      `json:"secret" levee:"source"` // want "tagged field: fieldtags.Person.secret"
-	another            interface{} "levee:\"source\""             // want "tagged field: fieldtags.Person.another"
-	name               string      `some_key:"non_secret"`
-	someNotTaggedField int
+	password             string      `levee:"source"`               // want "tagged field: fieldtags.Person.password"
+	secret               string      `json:"secret" levee:"source"` // want "tagged field: fieldtags.Person.secret"
+	another              interface{} "levee:\"source\""             // want "tagged field: fieldtags.Person.another"
+	name                 string      `some_key:"non_secret"`
+	spaceAfterFinalQuote string      `key:"value" `
+	someNotTaggedField   int
 }
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/callorder/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/callorder/tests.go
new file mode 100644
index 0000000..968808a
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/callorder/tests.go
@@ -0,0 +1,38 @@
+// 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 callorder
+
+import (
+	"fmt"
+	"io"
+
+	"example.com/core"
+)
+
+func TestTaintBeforeSinking(s core.Source, w io.Writer) {
+	_, _ = fmt.Fprintf(w, "%v", s)
+	core.Sink(w) // want "a source has reached a sink"
+}
+
+func TestSinkBeforeTainting(s core.Source, w io.Writer) {
+	core.Sink(w)
+	_, _ = fmt.Fprintf(w, "%v", s)
+}
+
+func TestSinkBeforeAndAfterTainting(s core.Source, w io.Writer) {
+	core.Sink(w)
+	_, _ = fmt.Fprintf(w, "%v", s)
+	core.Sink(w) // want "a source has reached a sink"
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/collections/chans.go b/internal/pkg/levee/testdata/src/example.com/tests/collections/chans.go
new file mode 100644
index 0000000..752cafa
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/collections/chans.go
@@ -0,0 +1,41 @@
+// 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 collections
+
+import (
+	"example.com/core"
+)
+
+func TestSourceReceivedFromChannelIsTainted(sources <-chan core.Source) {
+	s := <-sources
+	core.Sink(s) // want "a source has reached a sink"
+}
+
+func TestChannelIsTaintedWhenSourceIsPlacedOnIt(sources chan<- core.Source) {
+	sources <- core.Source{}
+	core.Sink(sources) // want "a source has reached a sink"
+}
+
+func TestValueObtainedFromTaintedChannelIsTainted(c chan interface{}) {
+	c <- core.Source{}
+	s := <-c
+	core.Sink(s) // want "a source has reached a sink"
+}
+
+func TestChannelIsNoLongerTaintedWhenNilledOut(sources chan core.Source) {
+	sources <- core.Source{}
+	sources = nil
+	core.Sink(sources)
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/inlining/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/inlining/tests.go
new file mode 100644
index 0000000..d29179e
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/inlining/tests.go
@@ -0,0 +1,39 @@
+// 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 inlining
+
+import (
+	"example.com/core"
+)
+
+func NewSource() *core.Source {
+	return &core.Source{}
+}
+
+func TestInlinedCall() {
+	core.Sink(NewSource()) // want "a source has reached a sink"
+}
+
+func TestInlinedRecv(sources <-chan core.Source) {
+	core.Sink(<-sources) // want "a source has reached a sink"
+}
+
+func TestInlinedArrayIndex(sources [1]core.Source) {
+	core.Sink(sources[0]) // want "a source has reached a sink"
+}
+
+func TestInlinedMapKey(sources map[string]core.Source) {
+	core.Sink(sources["source"]) // want "a source has reached a sink"
+}