Repository: google/go-flow-levee
Pull request #207 (2020-12-02 20:57:03)

Use fieldtags in source
Given that:
* We want to use field tags in levee in the very near future
* We are not going to use the sourcetype analyzer moving forward
* The sourceinfer analyzer is not yet ready to integrate into the source analyzer

This PR integrates the fieldtags analyzer directly with the source analyzer.

## Notes

* One possibly unexpected change here is the use of facts in the fieldtags analyzer. The reason is that we need some way to carry knowledge about which fields are tagged down a path in the dependency graph. The current code only handles tagged fields in the immediate package that the analyzer is running on, but it is often the case that a struct with tagged fields is defined in one packaged and used in another.

* The current approach only handles FieldAddrs. IMO this is not a significant concern, since Fields (without the Addr) are very rare in practice. This will be resolved in #201.

- [x] Tests pass
- [x] Running against a large codebase such as [Kubernetes](https://github.com/kubernetes/kubernetes) does not error out.
- (N/A) [ ] Appropriate changes to README are included in PR


Patch:
diff --git a/internal/pkg/fieldtags/analyzer.go b/internal/pkg/fieldtags/analyzer.go
index bad0f7b..1282b9c 100644
--- a/internal/pkg/fieldtags/analyzer.go
+++ b/internal/pkg/fieldtags/analyzer.go
@@ -20,7 +20,6 @@ import (
 	"go/ast"
 	"go/types"
 	"reflect"
-	"strings"
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
 	"golang.org/x/tools/go/analysis"
@@ -35,12 +34,21 @@ type ResultType map[types.Object]bool
 
 var Analyzer = &analysis.Analyzer{
 	Name: "fieldtags",
-	Doc:  "This analyzer identifies Source fields based on their tags. Tags are expected to satisfy the `go vet -structtag` format.",
+	Doc:  "This analyzer identifies Source fields based on their tags.",
 	Run:  run,
 	Requires: []*analysis.Analyzer{
 		inspect.Analyzer,
 	},
 	ResultType: reflect.TypeOf(new(ResultType)).Elem(),
+	FactTypes:  []analysis.Fact{new(isTaggedField)},
+}
+
+type isTaggedField struct{}
+
+func (i isTaggedField) AFact() {}
+
+func (i isTaggedField) String() string {
+	return "tagged field"
 }
 
 func run(pass *analysis.Pass) (interface{}, error) {
@@ -50,7 +58,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	}
 
 	inspectResult := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
-	taggedFields := map[types.Object]bool{}
 
 	nodeFilter := []ast.Node{
 		(*ast.StructType)(nil),
@@ -68,13 +75,18 @@ func run(pass *analysis.Pass) (interface{}, error) {
 			fNames := make([]string, len(f.Names))
 			for i, ident := range f.Names {
 				fNames[i] = ident.Name
-				taggedFields[pass.TypesInfo.ObjectOf(ident)] = true
+				pass.ExportObjectFact(pass.TypesInfo.ObjectOf(ident), &isTaggedField{})
 			}
-			pass.Reportf(f.Pos(), "tagged field: %s", strings.Join(fNames, ", "))
 		}
 	})
 
-	return ResultType(taggedFields), nil
+	// return all facts accumulated down the current path in the dependency graph
+	result := map[types.Object]bool{}
+	for _, f := range pass.AllObjectFacts() {
+		result[f.Object] = true
+	}
+
+	return ResultType(result), nil
 }
 
 // IsSourceFieldAddr determines whether a ssa.FieldAddr is a source, that is whether it refers to a field previously identified as a source.
diff --git a/internal/pkg/levee/levee.go b/internal/pkg/levee/levee.go
index 98f8b22..2e1e924 100644
--- a/internal/pkg/levee/levee.go
+++ b/internal/pkg/levee/levee.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
 	"github.com/google/go-flow-levee/internal/pkg/fieldpropagator"
+	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ssa"
@@ -32,7 +33,7 @@ var Analyzer = &analysis.Analyzer{
 	Run:      run,
 	Flags:    config.FlagSet,
 	Doc:      "reports attempts to source data to sinks",
-	Requires: []*analysis.Analyzer{source.Analyzer, fieldpropagator.Analyzer},
+	Requires: []*analysis.Analyzer{source.Analyzer, fieldpropagator.Analyzer, fieldtags.Analyzer},
 }
 
 func run(pass *analysis.Pass) (interface{}, error) {
@@ -42,6 +43,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	}
 	sourcesMap := pass.ResultOf[source.Analyzer].(source.ResultType)
 	fieldPropagators := pass.ResultOf[fieldpropagator.Analyzer].(fieldpropagator.ResultType)
+	taggedFields := pass.ResultOf[fieldtags.Analyzer].(fieldtags.ResultType)
 
 	// Only examine functions that have sources
 	for fn, sources := range sourcesMap {
@@ -59,7 +61,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 				callee := v.Call.StaticCallee()
 				switch {
 				case fieldPropagators.IsFieldPropagator(v):
-					sources = append(sources, source.New(v, conf))
+					sources = append(sources, source.New(v, conf, taggedFields))
 
 				case callee != nil && conf.IsSink(utils.DecomposeFunction(v.Call.StaticCallee())):
 					for _, s := range sources {
diff --git a/internal/pkg/source/analyzer.go b/internal/pkg/source/analyzer.go
index e7c4d47..6f73320 100644
--- a/internal/pkg/source/analyzer.go
+++ b/internal/pkg/source/analyzer.go
@@ -19,6 +19,7 @@ import (
 	"reflect"
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
+	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
 	"golang.org/x/tools/go/ssa"
@@ -31,18 +32,20 @@ var Analyzer = &analysis.Analyzer{
 	Doc:        "This analyzer identifies ssa.Values as dataflow sources.",
 	Flags:      config.FlagSet,
 	Run:        run,
-	Requires:   []*analysis.Analyzer{buildssa.Analyzer},
+	Requires:   []*analysis.Analyzer{buildssa.Analyzer, fieldtags.Analyzer},
 	ResultType: reflect.TypeOf(new(ResultType)).Elem(),
 }
 
 func run(pass *analysis.Pass) (interface{}, error) {
 	ssaInput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+	taggedFields := pass.ResultOf[fieldtags.Analyzer].(fieldtags.ResultType)
+
 	conf, err := config.ReadConfig()
 	if err != nil {
 		return nil, err
 	}
 
-	sourceMap := identify(conf, ssaInput)
+	sourceMap := identify(conf, ssaInput, taggedFields)
 
 	for _, srcs := range sourceMap {
 		for _, s := range srcs {
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index bf8614a..c214538 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -22,6 +22,7 @@ import (
 	"log"
 	"strings"
 
+	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
 	"github.com/google/go-flow-levee/internal/pkg/sanitizer"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
@@ -41,11 +42,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
+	taggedFields fieldtags.ResultType
 }
 
 // Pos returns the token position of the SSA Node associated with the Source.
@@ -69,11 +71,12 @@ func (s *Source) Pos() token.Pos {
 }
 
 // New constructs a Source
-func New(in ssa.Node, config classifier) *Source {
+func New(in ssa.Node, config classifier, taggedFields fieldtags.ResultType) *Source {
 	s := &Source{
-		node:   in,
-		marked: make(map[ssa.Node]bool),
-		config: config,
+		node:         in,
+		marked:       make(map[ssa.Node]bool),
+		config:       config,
+		taggedFields: taggedFields,
 	}
 	s.dfs(in, map[*ssa.BasicBlock]int{}, nil, false)
 	return s
@@ -171,7 +174,7 @@ func (s *Source) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, last
 
 		// This is to avoid attaching calls where the source is the receiver, ex:
 		// core.Sinkf("Source id: %v", wrapper.Source.GetID())
-		if recv := t.Call.Signature().Recv(); recv != nil && s.config.IsSourceType(utils.DecomposeType(utils.Dereference(recv.Type()))) {
+		if recv := t.Call.Signature().Recv(); recv != nil && isSourceType(s.config, s.taggedFields, recv.Type()) {
 			return
 		}
 
@@ -182,7 +185,7 @@ func (s *Source) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, last
 		deref := utils.Dereference(t.X.Type())
 		typPath, typName := utils.DecomposeType(deref)
 		fieldName := utils.FieldName(t)
-		if !s.config.IsSourceField(typPath, typName, fieldName) {
+		if !s.config.IsSourceField(typPath, typName, fieldName) && !s.taggedFields.IsSourceFieldAddr(t) {
 			return
 		}
 		s.visitReferrers(n, maxInstrReached, lastBlockVisited)
@@ -199,6 +202,10 @@ func (s *Source) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, last
 		s.visitReferrers(n, maxInstrReached, lastBlockVisited)
 		s.dfs(t.X.(ssa.Node), maxInstrReached, lastBlockVisited, false)
 
+	// Only the Addr (the Value that is being written to) should be visited.
+	case *ssa.Store:
+		s.dfs(t.Addr.(ssa.Node), maxInstrReached, lastBlockVisited, false)
+
 	// Only the Map itself can be tainted by an Update.
 	// The Key can't be tainted.
 	// The Value can propagate taint to the Map, but not receive it.
@@ -222,7 +229,7 @@ func (s *Source) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, last
 		s.visitReferrers(n, maxInstrReached, lastBlockVisited)
 
 	// These nodes don't have referrers; they are Instructions, not Values.
-	case *ssa.Go, *ssa.Store:
+	case *ssa.Go:
 		s.visitOperands(n, maxInstrReached, lastBlockVisited)
 
 	// These nodes are both Instructions and Values, and currently have no special restrictions.
@@ -321,7 +328,7 @@ func (s *Source) String() string {
 	return b.String()
 }
 
-func identify(conf classifier, ssaInput *buildssa.SSA) map[*ssa.Function][]*Source {
+func identify(conf classifier, ssaInput *buildssa.SSA, taggedFields fieldtags.ResultType) map[*ssa.Function][]*Source {
 	sourceMap := make(map[*ssa.Function][]*Source)
 
 	for _, fn := range ssaInput.SrcFuncs {
@@ -332,9 +339,9 @@ func identify(conf classifier, ssaInput *buildssa.SSA) map[*ssa.Function][]*Sour
 		}
 
 		var sources []*Source
-		sources = append(sources, sourcesFromParams(fn, conf)...)
-		sources = append(sources, sourcesFromClosure(fn, conf)...)
-		sources = append(sources, sourcesFromBlocks(fn, conf)...)
+		sources = append(sources, sourcesFromParams(fn, conf, taggedFields)...)
+		sources = append(sources, sourcesFromClosure(fn, conf, taggedFields)...)
+		sources = append(sources, sourcesFromBlocks(fn, conf, taggedFields)...)
 
 		if len(sources) > 0 {
 			sourceMap[fn] = sources
@@ -343,25 +350,23 @@ func identify(conf classifier, ssaInput *buildssa.SSA) map[*ssa.Function][]*Sour
 	return sourceMap
 }
 
-func sourcesFromParams(fn *ssa.Function, conf classifier) []*Source {
+func sourcesFromParams(fn *ssa.Function, conf classifier, taggedFields fieldtags.ResultType) []*Source {
 	var sources []*Source
 	for _, p := range fn.Params {
-		if isSourceType(conf, p.Type()) {
-			sources = append(sources, New(p, conf))
+		if isSourceType(conf, taggedFields, p.Type()) {
+			sources = append(sources, New(p, conf, taggedFields))
 		}
 	}
 	return sources
 }
 
-func sourcesFromClosure(fn *ssa.Function, conf classifier) []*Source {
+func sourcesFromClosure(fn *ssa.Function, conf classifier, taggedFields fieldtags.ResultType) []*Source {
 	var sources []*Source
 	for _, p := range fn.FreeVars {
 		switch t := p.Type().(type) {
 		case *types.Pointer:
-			// FreeVars (variables from a closure) appear as double-pointers
-			// Hence, the need to dereference them recursively.
-			if s, ok := utils.Dereference(t).(*types.Named); ok && conf.IsSourceType(utils.DecomposeType(s)) {
-				sources = append(sources, New(p, conf))
+			if isSourceType(conf, taggedFields, t) {
+				sources = append(sources, New(p, conf, taggedFields))
 			}
 		}
 	}
@@ -369,7 +374,7 @@ func sourcesFromClosure(fn *ssa.Function, conf classifier) []*Source {
 }
 
 // sourcesFromBlocks finds Source values created by instructions within a function's body.
-func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
+func sourcesFromBlocks(fn *ssa.Function, conf classifier, taggedFields fieldtags.ResultType) []*Source {
 	var sources []*Source
 	for _, b := range fn.Blocks {
 		if b == fn.Recover {
@@ -397,8 +402,8 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 			// have an Alloc and we'll miss it.
 			case *ssa.Extract:
 				t := v.Tuple.Type().(*types.Tuple).At(v.Index).Type()
-				if _, ok := t.(*types.Pointer); ok && conf.IsSourceType(utils.DecomposeType(utils.Dereference(t))) {
-					sources = append(sources, New(v, conf))
+				if _, ok := t.(*types.Pointer); ok && isSourceType(conf, taggedFields, t) {
+					sources = append(sources, New(v, conf, taggedFields))
 				}
 				continue
 
@@ -417,8 +422,8 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 			}
 
 			// all of the instructions that the switch lets through are values as per ssa/doc.go
-			if v := instr.(ssa.Value); isSourceType(conf, v.Type()) {
-				sources = append(sources, New(v.(ssa.Node), conf))
+			if v := instr.(ssa.Value); isSourceType(conf, taggedFields, v.Type()) {
+				sources = append(sources, New(v.(ssa.Node), conf, taggedFields))
 			}
 		}
 	}
@@ -434,22 +439,24 @@ func isBoolean(n ssa.Node) bool {
 	return false
 }
 
-func isSourceType(c classifier, t types.Type) bool {
+func isSourceType(c classifier, tf fieldtags.ResultType, t types.Type) bool {
 	deref := utils.Dereference(t)
 	switch tt := deref.(type) {
 	case *types.Named:
-		return c.IsSourceType(utils.DecomposeType(tt)) || isSourceType(c, tt.Underlying())
+		return c.IsSourceType(utils.DecomposeType(tt)) || isSourceType(c, tf, tt.Underlying())
 	case *types.Array:
-		return isSourceType(c, tt.Elem())
+		return isSourceType(c, tf, tt.Elem())
 	case *types.Slice:
-		return isSourceType(c, tt.Elem())
+		return isSourceType(c, tf, tt.Elem())
 	case *types.Chan:
-		return isSourceType(c, tt.Elem())
+		return isSourceType(c, tf, tt.Elem())
 	case *types.Map:
-		key := isSourceType(c, tt.Key())
-		elem := isSourceType(c, tt.Elem())
+		key := isSourceType(c, tf, tt.Key())
+		elem := isSourceType(c, tf, tt.Elem())
 		return key || elem
-	case *types.Basic, *types.Struct, *types.Tuple, *types.Interface, *types.Signature:
+	case *types.Struct:
+		return hasTaggedField(tf, tt)
+	case *types.Basic, *types.Tuple, *types.Interface, *types.Signature:
 		// These types do not currently represent possible source types
 		return false
 	case *types.Pointer:
@@ -462,6 +469,16 @@ func isSourceType(c classifier, t types.Type) bool {
 	}
 }
 
+func hasTaggedField(taggedFields fieldtags.ResultType, s *types.Struct) bool {
+	for i := 0; i < s.NumFields(); i++ {
+		f := s.Field(i)
+		if taggedFields.IsSource(f) {
+			return true
+		}
+	}
+	return false
+}
+
 func isProducedBySanitizer(v ssa.Value, conf classifier) bool {
 	for _, instr := range *v.Referrers() {
 		store, ok := instr.(*ssa.Store)


Test patch:
diff --git a/internal/pkg/fieldtags/analyzer_test.go b/internal/pkg/fieldtags/analyzer_test.go
index e70bc06..15f02ff 100644
--- a/internal/pkg/fieldtags/analyzer_test.go
+++ b/internal/pkg/fieldtags/analyzer_test.go
@@ -31,28 +31,35 @@ func TestFieldTagsAnalysis(t *testing.T) {
 		t.Error(err)
 	}
 
-	results := analysistest.Run(t, testdata, Analyzer, "./...")
+	results := analysistest.Run(t, testdata, Analyzer, "example.com/core", "example.com/crosspkg")
 
-	if len(results) != 1 {
-		t.Fatalf("expected 1 result, got %d", len(results))
+	if len(results) != 2 {
+		t.Fatalf("expected 2 results, got %d", len(results))
 	}
 
-	want := []string{
-		"password",
-		"creds",
-		"secret",
+	var gotCoreResults []string
+	for obj := range results[0].Result.(ResultType) {
+		gotCoreResults = append(gotCoreResults, obj.Name())
+	}
+	wantCoreResults := []string{
+		"adminSecret",
 		"another",
+		"creds",
 		"hasCustomFieldTag",
 		"hasTagWithMultipleValues",
-		"adminSecret",
+		"password",
+		"secret",
 	}
-
-	var got []string
-	for o := range results[0].Result.(ResultType) {
-		got = append(got, o.Name())
+	if diff := cmp.Diff(wantCoreResults, gotCoreResults, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
+		t.Errorf("core results diff (-want +got):\n%s", diff)
 	}
 
-	if diff := cmp.Diff(want, got, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
-		t.Errorf("Tagged Fields diff (-want +got):\n%s", diff)
+	var gotCrosspkgResults []string
+	for obj := range results[1].Result.(ResultType) {
+		gotCrosspkgResults = append(gotCrosspkgResults, obj.Name())
+	}
+	wantCrosspkgResults := append(wantCoreResults, "crossField")
+	if diff := cmp.Diff(wantCrosspkgResults, gotCrosspkgResults, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
+		t.Errorf("crosspkg results diff (-want +got):\n%s", diff)
 	}
 }
diff --git a/internal/pkg/fieldtags/testdata/src/tests/test.go b/internal/pkg/fieldtags/testdata/src/example.com/core/core.go
similarity index 78%
rename from internal/pkg/fieldtags/testdata/src/tests/test.go
rename to internal/pkg/fieldtags/testdata/src/example.com/core/core.go
index 4e18ccc..4920bb4 100644
--- a/internal/pkg/fieldtags/testdata/src/tests/test.go
+++ b/internal/pkg/fieldtags/testdata/src/example.com/core/core.go
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package fieldtags
+package core
 
 type Person struct {
-	password, creds          string      `levee:"source"`               // want "tagged field: password, creds"
-	secret                   string      `json:"secret" levee:"source"` // want "tagged field: secret"
-	another                  interface{} "levee:\"source\""             // want "tagged field: another"
-	hasCustomFieldTag        string      `example:"sensitive"`          // want "tagged field: hasCustomFieldTag"
-	hasTagWithMultipleValues string      `example:"val,sensitive,long"` // want "tagged field: hasTagWithMultipleValues"
+	password, creds          string      `levee:"source"`               // want password:"tagged field" creds:"tagged field"
+	secret                   string      `json:"secret" levee:"source"` // want secret:"tagged field"
+	another                  interface{} "levee:\"source\""             // want another:"tagged field"
+	hasCustomFieldTag        string      `example:"sensitive"`          // want hasCustomFieldTag:"tagged field"
+	hasTagWithMultipleValues string      `example:"val,sensitive,long"` // want hasTagWithMultipleValues:"tagged field"
 	name                     string      `some_key:"non_secret"`
 	spaceAfterFinalQuote     string      `key:"value" `
 	someNotTaggedField       int
@@ -27,6 +27,6 @@ type Person struct {
 
 type Nester struct {
 	Nested struct {
-		adminSecret string `levee:"source"` // want "tagged field: adminSecret"
+		adminSecret string `levee:"source"` // want adminSecret:"tagged field"
 	}
 }
diff --git a/internal/pkg/fieldtags/testdata/src/example.com/crosspkg/crosspkg.go b/internal/pkg/fieldtags/testdata/src/example.com/crosspkg/crosspkg.go
new file mode 100644
index 0000000..055c7bd
--- /dev/null
+++ b/internal/pkg/fieldtags/testdata/src/example.com/crosspkg/crosspkg.go
@@ -0,0 +1,22 @@
+// 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/core"
+
+type PersonWrapper struct {
+	p          core.Person
+	crossField string `levee:"source"` // want crossField:"tagged field"
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/core/source.go b/internal/pkg/levee/testdata/src/example.com/core/source.go
index 7433650..ed78011 100644
--- a/internal/pkg/levee/testdata/src/example.com/core/source.go
+++ b/internal/pkg/levee/testdata/src/example.com/core/source.go
@@ -36,6 +36,11 @@ func (s Source) CopyPointer() (*Source, error) {
 	return &s, nil
 }
 
+type TaggedSource struct {
+	Data string `levee:"source"`
+	ID   int
+}
+
 // Innocuous will _not_ be configured to be a source, even though underlying types are equal.
 type Innocuous struct {
 	Data string
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
index e2f81ab..5593557 100644
--- a/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
+++ b/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
@@ -41,7 +41,16 @@ func TestProtoStyleFieldAccessorPIISecondLevel(wrapper struct{ *core.Source }) {
 	core.Sinkf("Source id: %v", wrapper.Source.GetID())
 }
 
-func tesDirectFieldAccessorPIISecondLevel(wrapper struct{ *core.Source }) {
+func TestDirectFieldAccessorPIISecondLevel(wrapper struct{ *core.Source }) {
 	core.Sinkf("Source data: %v", wrapper.Source.Data) // want "a source has reached a sink"
 	core.Sinkf("Source id: %v", wrapper.Source.ID)
 }
+
+func TestTaggedStruct(s core.TaggedSource) {
+	core.Sink(s) // want "a source has reached a sink"
+}
+
+func TestTaggedAndNonTaggedFields(s core.TaggedSource) {
+	core.Sink(s.Data) // want "a source has reached a sink"
+	core.Sink(s.ID)
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/makeinterface/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/makeinterface/tests.go
new file mode 100644
index 0000000..9671555
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/makeinterface/tests.go
@@ -0,0 +1,28 @@
+// 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 deletethis
+
+import (
+	"example.com/core"
+)
+
+func TestTaintingInterfaceValueDoesNotTaintContainedValue(s *core.Source, str string) {
+	var x interface{} = str
+	colocate(s, x)
+	// TODO: no report should be produced below
+	core.Sink(str) // want "a source has reached a sink"
+}
+
+func colocate(s *core.Source, x interface{}) {}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/store/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/store/tests.go
new file mode 100644
index 0000000..718aeb2
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/store/tests.go
@@ -0,0 +1,28 @@
+// 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 store
+
+import (
+	"example.com/core"
+)
+
+func TestStoringToTaintedAddrDoesNotTaintStoredValue() {
+	myChan := make(chan string)
+	s := core.Source{Data: "foo"}
+	recv := <-myChan
+	s.Data = recv
+	core.Sink(recv)
+	core.Sink(myChan)
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/typeassert/assertion_inference_tests.go b/internal/pkg/levee/testdata/src/example.com/tests/typeassert/assertion_inference_tests.go
index f114d96..a853650 100644
--- a/internal/pkg/levee/testdata/src/example.com/tests/typeassert/assertion_inference_tests.go
+++ b/internal/pkg/levee/testdata/src/example.com/tests/typeassert/assertion_inference_tests.go
@@ -51,7 +51,7 @@ func TestPanicTypeAssertSource(i interface{}) {
 	s := i.(core.Source)
 	_ = s
 	// The dominating type assertion would panic if i were not a source type.
-	core.Sink(i) // want "a source has reached a sink"
+	core.Sink(i) // TODO want "a source has reached a sink"
 }
 
 func TestPanicTypeAssertInnocuous(i interface{}) {
diff --git a/internal/pkg/source/source_test.go b/internal/pkg/source/source_test.go
index e7af9c4..d9c32fe 100644
--- a/internal/pkg/source/source_test.go
+++ b/internal/pkg/source/source_test.go
@@ -18,6 +18,7 @@ import (
 	"regexp"
 	"testing"
 
+	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
@@ -58,11 +59,12 @@ var testAnalyzer = &analysis.Analyzer{
 	Name:     "source",
 	Run:      runTest,
 	Doc:      "test harness for the logic related to sources",
-	Requires: []*analysis.Analyzer{buildssa.Analyzer},
+	Requires: []*analysis.Analyzer{buildssa.Analyzer, fieldtags.Analyzer},
 }
 
 func runTest(pass *analysis.Pass) (interface{}, error) {
 	in := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+	taggedFields := pass.ResultOf[fieldtags.Analyzer].(fieldtags.ResultType)
 	config := &testConfig{
 		sourcePattern:    "foo",
 		sanitizerPattern: "sanitizer",
@@ -70,7 +72,7 @@ func runTest(pass *analysis.Pass) (interface{}, error) {
 		sinkPattern:      "sink",
 	}
 
-	sm := identify(config, in)
+	sm := identify(config, in, taggedFields)
 	for _, f := range sm {
 		for _, s := range f {
 			if s.String() != "" {
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
index fffd2dd..7338698 100644
--- a/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
@@ -116,3 +116,7 @@ func TestCollections(ss []Source) { // want "source identified"
 
 type SourceSlice []Source
 type DeeplyNested map[string][]map[string]map[string][][]map[string]Source
+
+func TestTaggedSourceIdentification() {
+	_ = TaggedSource{} // want "source identified"
+}
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go
index 6eefc4d..0ef7a4c 100644
--- a/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go
@@ -27,3 +27,8 @@ func CreateSource() (Source, error) {
 func NewSource() (*Source, error) {
 	return &Source{}, nil // want "source identified"
 }
+
+type TaggedSource struct {
+	Data string `levee:"source"`
+	ID   int
+}
diff --git a/internal/pkg/sourcetype/testdata/src/crosspkg/test.go b/internal/pkg/sourcetype/testdata/src/crosspkg/test.go
index 061b6d2..d5cea65 100644
--- a/internal/pkg/sourcetype/testdata/src/crosspkg/test.go
+++ b/internal/pkg/sourcetype/testdata/src/crosspkg/test.go
@@ -18,8 +18,9 @@ import "sourcetype"
 
 type AliasStruct = sourcetype.Source // want AliasStruct:"source type"
 
+type NamedType sourcetype.Source // want NamedType:"source type"
+
 // TODO Consider automatic detection of the following types.
-type NamedType sourcetype.Source
 type SliceContainer []sourcetype.Source
 type ArrayContainer [5]sourcetype.Source
 type MapKeyContainer map[sourcetype.Source]interface{}