Repository: google/go-flow-levee
Pull request #108 (2020-09-14 15:14:20)

Mark type as source if it holds a tagged field
## This PR

The current PR is in relation to issue #96.

My original intent was to fix the inconsistent behavior we have right now where a type that holds a tagged field is not considered to be a Source. As a side effect, the way I implemented it gets us some of the way towards supporting identifying `NamedType` as a source type in `type NamedType Source`.

This PR uses the data collected by the `fieldtags` analyzer in the `sourcetype` analyzer in order to mark a `type` containing a tagged field as a `Source`. To do so, the new code scans the fields of the `types.Struct` underlying the type being declared and if it finds a tagged field, declares the type to be a `Source` type.

## Side effects

A side effect of the current implementation is that in `type NamedType Source`, because `Source` has tagged fields, and `NamedType`'s underlying `types.Struct` is the same as the `types.Struct` underlying `Source`, `NamedType` is identified as a source type. `NamedType` would not be identified as a source type if `Source` did not have tagged fields. This behavior may be surprising to users, since we don't handle other cases of `type NamedType Source`.


## Alternative implementation

An alternative implementation would be to export `type`s that hold a tagged field in the `fieldtags` analyzer, and consume those types in the `sourcetype` analyzer. This would circumvent the above-mentioned side effects.

## Discussion

Given all of the above, WDYT? **Is the current approach, with the "side effects" it brings, "ok" to you or should I use a different approach?** 

- [x] Tests pass
- [x] Appropriate changes to README are included in PR


Patch:
diff --git a/internal/pkg/debug/render/dot.go b/internal/pkg/debug/render/dot.go
index ba15a9e..8fa208c 100644
--- a/internal/pkg/debug/render/dot.go
+++ b/internal/pkg/debug/render/dot.go
@@ -74,13 +74,11 @@ func (r *renderer) writeEdges() {
 }
 
 func (r *renderer) addReferrer(n ssa.Node, ref ssa.Node) {
-	// TODO: document this somewhere?
 	// Red as in R-eferrer
 	r.addEdge(n, ref, "red")
 }
 
 func (r *renderer) addOperand(n ssa.Node, op ssa.Node) {
-	// TODO: document this somewhere?
 	// Orange as in O-perand
 	r.addEdge(n, op, "orange")
 }
diff --git a/internal/pkg/levee/levee.go b/internal/pkg/levee/levee.go
index 6be9bc0..edf1903 100644
--- a/internal/pkg/levee/levee.go
+++ b/internal/pkg/levee/levee.go
@@ -39,8 +39,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	if err != nil {
 		return nil, err
 	}
-	// TODO: respect configuration scope
-
 	sourcesMap := pass.ResultOf[source.Analyzer].(source.ResultType)
 	fieldPropagators := pass.ResultOf[fieldpropagator.Analyzer].(fieldpropagator.ResultType)
 
@@ -48,7 +46,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	for fn, sources := range sourcesMap {
 		for _, b := range fn.Blocks {
 			if b == fn.Recover {
-				// TODO Handle calls to sinks in a recovery block.
 				continue // skipping Recover since it does not have instructions, rather a single block.
 			}
 
@@ -79,6 +76,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
 func report(pass *analysis.Pass, source *source.Source, sink ssa.Node) {
 	var b strings.Builder
 	b.WriteString("a source has reached a sink")
-	fmt.Fprintf(&b, ", source: %v", pass.Fset.Position(source.Node().Pos()))
+	fmt.Fprintf(&b, ", source: %v", pass.Fset.Position(source.Pos()))
 	pass.Reportf(sink.Pos(), b.String())
 }
diff --git a/internal/pkg/source/analyzer.go b/internal/pkg/source/analyzer.go
index 348caa9..e7c4d47 100644
--- a/internal/pkg/source/analyzer.go
+++ b/internal/pkg/source/analyzer.go
@@ -35,10 +35,6 @@ var Analyzer = &analysis.Analyzer{
 	ResultType: reflect.TypeOf(new(ResultType)).Elem(),
 }
 
-// When reporting is true, report findings to pass.Report.
-// TODO This should be a flag passable to the common config.
-var reporting bool
-
 func run(pass *analysis.Pass) (interface{}, error) {
 	ssaInput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
 	conf, err := config.ReadConfig()
@@ -48,17 +44,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
 
 	sourceMap := identify(conf, ssaInput)
 
-	if reporting {
-		for _, srcs := range sourceMap {
-			for _, s := range srcs {
-				// Extracts don't have a registered position in the source code,
-				// so we need to use the position of their related Tuple.
-				if e, ok := s.node.(*ssa.Extract); ok {
-					report(pass, e.Tuple.Pos())
-					continue
-				}
-				report(pass, s.node.Pos())
-			}
+	for _, srcs := range sourceMap {
+		for _, s := range srcs {
+			report(pass, s.Pos())
 		}
 	}
 
@@ -66,5 +54,5 @@ func run(pass *analysis.Pass) (interface{}, error) {
 }
 
 func report(pass *analysis.Pass, pos token.Pos) {
-	pass.Reportf(pos, "source identified")
+	pass.Reportf(pos, "source identified at %s", pass.Fset.Position(pos))
 }
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index a5ee9c8..10f040d 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -49,100 +49,100 @@ type Source struct {
 	lastBlockVisited *ssa.BasicBlock
 }
 
-// Node returns the underlying ssa.Node of the Source.
-func (a *Source) Node() ssa.Node {
-	return a.node
+// Pos returns the token position of the SSA Node associated with the Source.
+func (s *Source) Pos() token.Pos {
+	// Extracts don't have a registered position in the source code,
+	// so we need to use the position of their related Tuple.
+	if e, ok := s.node.(*ssa.Extract); ok {
+		return e.Tuple.Pos()
+	}
+	return s.node.Pos()
 }
 
 // New constructs a Source
 func New(in ssa.Node, config classifier) *Source {
-	a := &Source{
+	s := &Source{
 		node:            in,
 		marked:          make(map[ssa.Node]bool),
 		config:          config,
 		maxInstrReached: map[*ssa.BasicBlock]int{},
 	}
-	a.dfs(in)
-	return a
+	s.dfs(in)
+	return s
 }
 
 // dfs performs Depth-First-Search on the def-use graph of the input Source.
 // While traversing the graph we also look for potential sanitizers of this Source.
 // If the Source passes through a sanitizer, dfs does not continue through that Node.
-func (a *Source) dfs(n ssa.Node) {
-	a.preOrder = append(a.preOrder, n)
-	a.marked[n.(ssa.Node)] = true
-
+func (s *Source) dfs(n ssa.Node) {
 	if instr, ok := n.(ssa.Instruction); ok {
-		a.record(instr)
+		if !s.reachableFromSource(instr) {
+			return
+		}
+		s.record(instr)
 	}
+	s.preOrder = append(s.preOrder, n)
+	s.marked[n.(ssa.Node)] = true
 
-	a.visitReferrers(n)
+	s.visitReferrers(n)
 
 	operands := n.Operands(nil)
 	if operands != nil {
-		a.visitOperands(n, operands)
+		s.visitOperands(n, operands)
 	}
 }
 
-func (a *Source) record(target ssa.Instruction) {
+func (s *Source) record(target ssa.Instruction) {
 	b := target.Block()
-	a.lastBlockVisited = b
+	s.lastBlockVisited = b
 	i, ok := indexInBlock(target)
 	if !ok {
 		return
 	}
-	if a.maxInstrReached[b] < i {
-		a.maxInstrReached[b] = i
+	if s.maxInstrReached[b] < i {
+		s.maxInstrReached[b] = i
 	}
 }
 
-func (a *Source) visitReferrers(n ssa.Node) {
-	referrers := a.referrersToVisit(n)
+func (s *Source) visitReferrers(n ssa.Node) {
+	referrers := s.referrersToVisit(n)
 
 	for _, r := range referrers {
-		if a.marked[r.(ssa.Node)] {
+		if s.marked[r.(ssa.Node)] {
 			continue
 		}
 
 		switch v := r.(type) {
 		case *ssa.Call:
-			if a.config.IsSanitizer(v) {
-				a.sanitizers = append(a.sanitizers, &sanitizer.Sanitizer{Call: v})
-			}
-
-		case *ssa.FieldAddr:
-			if !a.config.IsSourceFieldAddr(v) {
-				continue
+			if s.config.IsSanitizer(v) {
+				s.sanitizers = append(s.sanitizers, &sanitizer.Sanitizer{Call: v})
 			}
 		}
 
-		a.dfs(r.(ssa.Node))
+		s.dfs(r.(ssa.Node))
 	}
 }
 
 // referrersToVisit produces a filtered list of Referrers for an ssa.Node.
-// Specifically, we avoid referrers that:
-// - Are in a block that is not reachable from the current instruction
-// - Are calls to a Source method
-// - Are calls that occur earlier in the same block as the value being referred
-func (a *Source) referrersToVisit(n ssa.Node) (referrers []ssa.Instruction) {
+// Specifically, we want to avoid referrers that shouldn't be visited, e.g.
+// because they would not be reachable in an actual execution of the program.
+func (s *Source) referrersToVisit(n ssa.Node) (referrers []ssa.Instruction) {
 	if n.Referrers() == nil {
 		return
 	}
 	for _, r := range *n.Referrers() {
 		// If the referrer is in a different block from the one we last visited,
 		// and it can't be reached from the block we are visiting, then stop visiting.
-		if rb := r.Block(); a.lastBlockVisited != nil &&
-			rb != a.lastBlockVisited &&
-			!a.canReach(a.lastBlockVisited, rb) {
+		if rb := r.Block(); s.lastBlockVisited != nil &&
+			rb != s.lastBlockVisited &&
+			!s.canReach(s.lastBlockVisited, rb) {
 			continue
 		}
 
 		if c, ok := r.(*ssa.Call); ok {
 			// This is to avoid attaching calls where the source is the receiver, ex:
 			// core.Sinkf("Source id: %v", wrapper.Source.GetID())
-			if recv := c.Call.Signature().Recv(); recv != nil && a.config.IsSource(utils.Dereference(recv.Type())) {
+			if recv := c.Call.Signature().Recv(); recv != nil && s.config.IsSource(utils.Dereference(recv.Type())) {
 				continue
 			}
 
@@ -152,16 +152,48 @@ func (a *Source) referrersToVisit(n ssa.Node) (referrers []ssa.Instruction) {
 			if !ok {
 				continue
 			}
-			if i < a.maxInstrReached[r.Block()] {
+			if i < s.maxInstrReached[r.Block()] {
 				continue
 			}
 		}
+
+		if fa, ok := r.(*ssa.FieldAddr); ok && !s.config.IsSourceFieldAddr(fa) {
+			continue
+		}
+
 		referrers = append(referrers, r)
 	}
 	return referrers
 }
 
-func (a *Source) canReach(start *ssa.BasicBlock, dest *ssa.BasicBlock) bool {
+func (s *Source) reachableFromSource(target ssa.Instruction) bool {
+	// If the Source isn't produced by an instruction, be conservative and
+	// assume the target instruction is reachable.
+	sInstr, ok := s.node.(ssa.Instruction)
+	if !ok {
+		return true
+	}
+
+	// If these calls fail, be conservative and assume the target
+	// instruction is reachable.
+	sIndex, sOk := indexInBlock(sInstr)
+	targetIndex, targetOk := indexInBlock(target)
+	if !sOk || !targetOk {
+		return true
+	}
+
+	if sInstr.Block() == target.Block() && sIndex > targetIndex {
+		return false
+	}
+
+	if !s.canReach(sInstr.Block(), target.Block()) {
+		return false
+	}
+
+	return true
+}
+
+func (s *Source) canReach(start *ssa.BasicBlock, dest *ssa.BasicBlock) bool {
 	if start.Dominates(dest) {
 		return true
 	}
@@ -184,12 +216,12 @@ func (a *Source) canReach(start *ssa.BasicBlock, dest *ssa.BasicBlock) bool {
 	return false
 }
 
-func (a *Source) visitOperands(n ssa.Node, operands []*ssa.Value) {
+func (s *Source) visitOperands(n ssa.Node, operands []*ssa.Value) {
 	_, visitingFromExtract := n.(*ssa.Extract)
 
 	for _, o := range operands {
 		n, ok := (*o).(ssa.Node)
-		if !ok || a.marked[n] {
+		if !ok || s.marked[n] {
 			continue
 		}
 
@@ -214,7 +246,7 @@ func (a *Source) visitOperands(n ssa.Node, operands []*ssa.Value) {
 				return
 			}
 		}
-		a.dfs(n)
+		s.dfs(n)
 	}
 }
 
@@ -222,9 +254,9 @@ func (a *Source) visitOperands(n ssa.Node, operands []*ssa.Value) {
 // taint-propagation analysis. Concretely, only propagators, sanitizers and
 // sinks should constitute the output. Since, we already know what the source
 // is, it is also removed.
-func (a *Source) compress() []ssa.Node {
+func (s *Source) compress() []ssa.Node {
 	var compressed []ssa.Node
-	for _, n := range a.preOrder {
+	for _, n := range s.preOrder {
 		switch n.(type) {
 		case *ssa.Call:
 			compressed = append(compressed, n)
@@ -234,19 +266,19 @@ func (a *Source) compress() []ssa.Node {
 	return compressed
 }
 
-func (a *Source) RefersTo(n ssa.Node) bool {
-	return a.HasPathTo(n)
+func (s *Source) RefersTo(n ssa.Node) bool {
+	return s.HasPathTo(n)
 }
 
 // HasPathTo returns true when a Node is part of declaration-use graph.
-func (a *Source) HasPathTo(n ssa.Node) bool {
-	return a.marked[n]
+func (s *Source) HasPathTo(n ssa.Node) bool {
+	return s.marked[n]
 }
 
 // IsSanitizedAt returns true when the Source is sanitized by the supplied instruction.
-func (a *Source) IsSanitizedAt(call ssa.Instruction) bool {
-	for _, s := range a.sanitizers {
-		if s.Dominates(call) {
+func (s *Source) IsSanitizedAt(call ssa.Instruction) bool {
+	for _, san := range s.sanitizers {
+		if san.Dominates(call) {
 			return true
 		}
 	}
@@ -255,9 +287,9 @@ func (a *Source) IsSanitizedAt(call ssa.Instruction) bool {
 }
 
 // String implements Stringer interface.
-func (a *Source) String() string {
+func (s *Source) String() string {
 	var b strings.Builder
-	for _, n := range a.compress() {
+	for _, n := range s.compress() {
 		b.WriteString(fmt.Sprintf("%v ", n))
 	}
 
@@ -293,8 +325,6 @@ func sourcesFromParams(fn *ssa.Function, conf classifier) []*Source {
 			if n, ok := t.Elem().(*types.Named); ok && conf.IsSource(n) {
 				sources = append(sources, New(p, conf))
 			}
-			// TODO Handle the case where sources arepassed by value: func(c sourceType)
-			// TODO Handle cases where PII is wrapped in struct/slice/map
 		}
 	}
 	return sources
@@ -320,7 +350,6 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 	var sources []*Source
 	for _, b := range fn.Blocks {
 		if b == fn.Recover {
-			// TODO Handle calls to log in a recovery block.
 			continue
 		}
 
diff --git a/internal/pkg/sourcetype/analyzer.go b/internal/pkg/sourcetype/analyzer.go
index 9ec612f..74aa9a3 100644
--- a/internal/pkg/sourcetype/analyzer.go
+++ b/internal/pkg/sourcetype/analyzer.go
@@ -96,7 +96,8 @@ func run(pass *analysis.Pass) (interface{}, error) {
 
 	// Members contains all named entities
 	for _, mem := range ssaInput.Pkg.Members {
-		if ssaType, ok := mem.(*ssa.Type); ok && conf.IsSource(ssaType.Type()) {
+		if ssaType, ok := mem.(*ssa.Type); ok &&
+			(conf.IsSource(ssaType.Type()) || hasTaggedField(mem, taggedFields)) {
 			exportSourceFacts(pass, ssaType, conf, taggedFields)
 		}
 	}
@@ -109,6 +110,23 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	return classifier, nil
 }
 
+func hasTaggedField(mem ssa.Member, taggedFields fieldtags.ResultType) bool {
+	n, ok := mem.Type().(*types.Named)
+	if !ok {
+		return false
+	}
+	s, ok := n.Underlying().(*types.Struct)
+	if !ok {
+		return false
+	}
+	var has bool
+	for i := 0; i < s.NumFields(); i++ {
+		f := s.Field(i)
+		has = has || taggedFields.IsSource(f)
+	}
+	return has
+}
+
 func exportSourceFacts(pass *analysis.Pass, ssaType *ssa.Type, conf *config.Config, taggedFields fieldtags.ResultType) {
 	pass.ExportObjectFact(ssaType.Object(), &typeDeclFact{})
 	if under, ok := ssaType.Type().Underlying().(*types.Struct); ok {


Test patch:
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/callorder/beforesource.go b/internal/pkg/levee/testdata/src/example.com/tests/callorder/beforesource.go
new file mode 100644
index 0000000..a924287
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/callorder/beforesource.go
@@ -0,0 +1,54 @@
+// 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 (
+	"example.com/core"
+)
+
+// This type should *not* be identified as a Source.
+type key struct {
+	name string
+}
+
+func (k *key) Name() string {
+	return k.name
+}
+
+func newKey() *key {
+	return &key{
+		name: "",
+	}
+}
+
+func TestDoesNotReachSinkAfterSourceThroughValueCreatedBeforeSource() {
+	// Taint should not propagate to this value.
+	k := newKey()
+
+	_ = map[string]core.Source{}[k.name]
+
+	core.Sink(k.Name())
+}
+
+func TestDoesNotReachSinkInIfBeforeSourceThroughValueCreatedBeforeSource() {
+	// Taint should not propagate to this value.
+	k := newKey()
+
+	if true {
+		core.Sink(k.Name())
+	}
+
+	_ = map[string]core.Source{}[k.name]
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/position/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/position/tests.go
new file mode 100644
index 0000000..9dde540
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/position/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 position
+
+import (
+	"example.com/core"
+)
+
+func TestSourcePointerExtract() {
+	s, _ := NewSource()
+	core.Sink(s) // want "a source has reached a sink, source: .*tests.go:22:19"
+}
+
+func NewSource() (*core.Source, error) {
+	return &core.Source{}, nil
+}
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/typealias/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/typealias/tests.go
new file mode 100644
index 0000000..ada8d16
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/example.com/tests/typealias/tests.go
@@ -0,0 +1,31 @@
+// 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 typealias
+
+import (
+	"example.com/core"
+)
+
+type Foo core.Source
+
+type Bar = core.Source
+
+func TestTypeDefinition() {
+	core.Sink(Foo{})
+}
+
+func TestTypeAlias() {
+	core.Sink(Bar{}) // want "a source has reached a sink"
+}
diff --git a/internal/pkg/source/analyzer_test.go b/internal/pkg/source/analyzer_test.go
index b57becb..ac7cb76 100644
--- a/internal/pkg/source/analyzer_test.go
+++ b/internal/pkg/source/analyzer_test.go
@@ -22,8 +22,6 @@ import (
 )
 
 func TestSourceAnalysis(t *testing.T) {
-	reporting = true
-
 	testdata := analysistest.TestData()
 	if err := config.FlagSet.Set("config", testdata+"/src/analyzertest/test-config.json"); err != nil {
 		t.Error(err)
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/test.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
similarity index 88%
rename from internal/pkg/source/testdata/src/analyzertest/sourcetest/test.go
rename to internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
index 82808cb..8c90ce8 100644
--- a/internal/pkg/source/testdata/src/analyzertest/sourcetest/test.go
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
@@ -14,11 +14,11 @@
 
 package sourcetest
 
-// source container
-type Source struct {
-	Data string // source field
-	ID   int    // public
-}
+// type alias
+type Alias = Source
+
+// type definition where the underlying type is a Source
+type Definition Source
 
 // This function allows us to consume multiple arguments in a single line so this file can compile
 func noop(args ...interface{}) {}
@@ -40,7 +40,10 @@ func TestSourceDeclarations() {
 	ptrToDeclZero := &Source{}                            // want "source identified"
 	ptrToDeclPopulataed := &Source{Data: "secret", ID: 1} // want "source identified"
 
-	noop(varZeroVal, declZeroVal, populatedVal, constPtr, ptr, newPtr, ptrToDeclZero, ptrToDeclPopulataed)
+	alias := Alias{} // want "source identified"
+	def := Definition{}
+
+	noop(varZeroVal, declZeroVal, populatedVal, constPtr, ptr, newPtr, ptrToDeclZero, ptrToDeclPopulataed, alias, def)
 }
 
 // A report should be emitted for each parameter.
@@ -57,11 +60,3 @@ func TestSourceExtracts() {
 	chanSourcePtr, ok := <-(make(chan *Source))  // want "source identified"
 	_, _, _, _, _, _, _, _ = s, sptr, mapSource, chanSource, mapSourcePtr, chanSourcePtr, err, ok
 }
-
-func CreateSource() (Source, error) {
-	return Source{}, nil // want "source identified"
-}
-
-func NewSource() (*Source, error) {
-	return &Source{}, nil // want "source identified"
-}
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/position.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/position.go
new file mode 100644
index 0000000..7b5af9a
--- /dev/null
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/position.go
@@ -0,0 +1,20 @@
+// 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 sourcetest
+
+func TestSourcePointerExtract() {
+	s, _ := NewSource() // want "source identified at .*position.go:18:19"
+	_ = s
+}
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go
new file mode 100644
index 0000000..6eefc4d
--- /dev/null
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/source.go
@@ -0,0 +1,29 @@
+// 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 sourcetest
+
+// source container
+type Source struct {
+	Data string // source field
+	ID   int    // public
+}
+
+func CreateSource() (Source, error) {
+	return Source{}, nil // want "source identified"
+}
+
+func NewSource() (*Source, error) {
+	return &Source{}, nil // want "source identified"
+}
diff --git a/internal/pkg/sourcetype/testdata/src/sourcetype/test.go b/internal/pkg/sourcetype/testdata/src/sourcetype/test.go
index b7dd560..58e197d 100644
--- a/internal/pkg/sourcetype/testdata/src/sourcetype/test.go
+++ b/internal/pkg/sourcetype/testdata/src/sourcetype/test.go
@@ -21,10 +21,14 @@ type Source struct { // want Source:"source type"
 	ID           int
 }
 
+type SourceContainingTaggedField struct { // want SourceContainingTaggedField:"source type"
+	Tagged string `levee:"source"` // want Tagged:"source field"
+}
+
 type AliasStruct = Source // want AliasStruct:"source type"
 
-// TODO Consider automatic detection of the following types.
-type NamedType Source
+type NamedType Source // want NamedType:"source type"
+
 type SliceContainer []Source
 type ArrayContainer [5]Source
 type MapKeyContainer map[Source]interface{}