Repository: google/go-flow-levee
Pull request #11 (2020-05-12 23:05:42)

Extract the source type into a separate package.
Extract the source type into a separate package.

PurelyApplied: As an aside, I would appreciate it if you didn't force push on your branch after the PR is open.  It makes it a harder to track changes across comments.  Since I've pulled your branch to look at the diff locally, I now have to blow the branch away for future examination rather than just pull new changes.  We can squash before merge if necessary.

Patch:
diff --git a/internal/levee.go b/internal/levee.go
index b0084e8..7ee44ec 100644
--- a/internal/levee.go
+++ b/internal/levee.go
@@ -26,10 +26,8 @@ import (
 	"golang.org/x/tools/go/analysis/passes/buildssa"
 	"golang.org/x/tools/go/ssa"
 
-	"github.com/eapache/queue"
-
 	"github.com/google/go-flow-levee/internal/pkg/config/regexp"
-	"github.com/google/go-flow-levee/internal/pkg/sanitizer"
+	"github.com/google/go-flow-levee/internal/pkg/source"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 )
 
@@ -88,7 +86,7 @@ func (c config) isSink(call *ssa.Call) bool {
 	return false
 }
 
-func (c config) isSanitizer(call *ssa.Call) bool {
+func (c config) IsSanitizer(call *ssa.Call) bool {
 	for _, p := range c.Sanitizers {
 		if p.matchMethodName(call) {
 			return true
@@ -112,7 +110,7 @@ func (c config) isSource(t types.Type) bool {
 	return false
 }
 
-func (c config) isSourceFieldAddr(fa *ssa.FieldAddr) bool {
+func (c config) IsSourceFieldAddr(fa *ssa.FieldAddr) bool {
 	// fa.Type() refers to the accessed field's type.
 	// fa.X.Type() refers to the surrounding struct's type.
 	deref := utils.Dereference(fa.X.Type())
@@ -127,7 +125,7 @@ func (c config) isSourceFieldAddr(fa *ssa.FieldAddr) bool {
 	return false
 }
 
-func (c config) isPropagator(call *ssa.Call) bool {
+func (c config) IsPropagator(call *ssa.Call) bool {
 	return c.isFieldPropagator(call) || c.isTransformingPropagator(call)
 }
 
@@ -306,81 +304,6 @@ var Analyzer = &analysis.Analyzer{
 	Requires: []*analysis.Analyzer{buildssa.Analyzer},
 }
 
-// source represents a source in an SSA call tree.
-// It is based on ssa.Node, with the added functionality of computing the recursive graph of
-// its referrers.
-// source.sanitized notes sanitizer calls that sanitize this source
-type source struct {
-	node       ssa.Node
-	marked     map[ssa.Node]bool
-	sanitizers []*sanitizer.Sanitizer
-}
-
-func newSource(in ssa.Node, config *config) *source {
-	a := &source{
-		node:   in,
-		marked: make(map[ssa.Node]bool),
-	}
-	a.bfs(config)
-	return a
-}
-
-// bfs performs Breadth-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, bfs does not continue through that Node.
-func (a *source) bfs(config *config) {
-	q := queue.New()
-	q.Add(a.node)
-	a.marked[a.node] = true
-
-	for q.Length() > 0 {
-		e := q.Remove().(ssa.Node)
-
-		if c, ok := e.(*ssa.Call); ok && config.isSanitizer(c) {
-			a.sanitizers = append(a.sanitizers, &sanitizer.Sanitizer{Call: c})
-			continue
-		}
-
-		if e.Referrers() == nil {
-			continue
-		}
-
-		for _, r := range *e.Referrers() {
-			if _, ok := a.marked[r.(ssa.Node)]; ok {
-				continue
-			}
-			a.marked[r.(ssa.Node)] = true
-
-			// Need to stay within the scope of the function under analysis.
-			if call, ok := r.(*ssa.Call); ok && !config.isPropagator(call) {
-				continue
-			}
-
-			// Do not follow innocuous field access (e.g. Cluster.Zone)
-			if addr, ok := r.(*ssa.FieldAddr); ok && !config.isSourceFieldAddr(addr) {
-				continue
-			}
-
-			q.Add(r)
-		}
-	}
-}
-
-// 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 (a *source) isSanitizedAt(call ssa.Instruction) bool {
-	for _, s := range a.sanitizers {
-		if s.Dominates(call) {
-			return true
-		}
-	}
-
-	return false
-}
-
 // varargs represents a variable length argument.
 // Concretely, it abstract over the fact that the varargs internally are represented by an ssa.Slice
 // which contains the underlying values of for vararg members.
@@ -388,17 +311,17 @@ func (a *source) isSanitizedAt(call ssa.Instruction) bool {
 // get the underlying values of the vararg members is important for this analyzer.
 type varargs struct {
 	slice   *ssa.Slice
-	sources []*source
+	sources []*source.Source
 }
 
 // newVarargs constructs varargs. SSA represents varargs as an ssa.Slice.
-func newVarargs(s *ssa.Slice, sources []*source) *varargs {
+func newVarargs(s *ssa.Slice, sources []*source.Source) *varargs {
 	a, ok := s.X.(*ssa.Alloc)
 	if !ok || a.Comment != "varargs" {
 		return nil
 	}
 	var (
-		referredSources []*source
+		referredSources []*source.Source
 	)
 
 	for _, r := range *a.Referrers() {
@@ -417,7 +340,7 @@ func newVarargs(s *ssa.Slice, sources []*source) *varargs {
 		store := (*idx.Referrers())[0].(*ssa.Store)
 
 		for _, s := range sources {
-			if s.hasPathTo(store.Val.(ssa.Node)) {
+			if s.HasPathTo(store.Val.(ssa.Node)) {
 				referredSources = append(referredSources, s)
 				break
 			}
@@ -475,7 +398,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 				switch v := instr.(type) {
 				case *ssa.Call:
 					switch {
-					case conf.isPropagator(v):
+					case conf.IsPropagator(v):
 						// Handling the case where sources are propagated to io.Writer
 						// (ex. proto.MarshalText(&buf, c)
 						// In such cases, "buf" becomes a source, and not the return value of the propagator.
@@ -485,10 +408,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
 						//  specify the position of the propagated source.
 						// TODO  Consider turning propagators that take io.Writer into sinks.
 						if a := conf.sendsToIOWriter(v); a != nil {
-							sources = append(sources, newSource(a, conf))
+							sources = append(sources, source.New(a, conf))
 						} else {
 							//log.V(2).Infof("Adding source: %v %T", v.Value(), v.Value())
-							sources = append(sources, newSource(v, conf))
+							sources = append(sources, source.New(v, conf))
 						}
 
 					case conf.isSink(v):
@@ -498,7 +421,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 							if varargs, ok := lastArg.(*ssa.Slice); ok {
 								if sinkVarargs := newVarargs(varargs, sources); sinkVarargs != nil {
 									for _, s := range sinkVarargs.sources {
-										if !s.isSanitizedAt(v) {
+										if !s.IsSanitizedAt(v) {
 											report(pass, s, v)
 										}
 									}
@@ -539,11 +462,11 @@ func readConfig(path string) (*config, error) {
 	return readConfigCached, readConfigCachedErr
 }
 
-func identifySources(conf *config, ssaInput *buildssa.SSA) map[*ssa.Function][]*source {
-	sourceMap := make(map[*ssa.Function][]*source)
+func identifySources(conf *config, ssaInput *buildssa.SSA) map[*ssa.Function][]*source.Source {
+	sourceMap := make(map[*ssa.Function][]*source.Source)
 
 	for _, fn := range ssaInput.SrcFuncs {
-		var sources []*source
+		var sources []*source.Source
 		sources = append(sources, sourcesFromParams(fn, conf)...)
 		sources = append(sources, sourcesFromClosure(fn, conf)...)
 		sources = append(sources, sourcesFromBlocks(fn, conf)...)
@@ -555,13 +478,13 @@ func identifySources(conf *config, ssaInput *buildssa.SSA) map[*ssa.Function][]*
 	return sourceMap
 }
 
-func sourcesFromParams(fn *ssa.Function, conf *config) []*source {
-	var sources []*source
+func sourcesFromParams(fn *ssa.Function, conf *config) []*source.Source {
+	var sources []*source.Source
 	for _, p := range fn.Params {
 		switch t := p.Type().(type) {
 		case *types.Pointer:
 			if n, ok := t.Elem().(*types.Named); ok && conf.isSource(n) {
-				sources = append(sources, newSource(p, conf))
+				sources = append(sources, source.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
@@ -570,23 +493,23 @@ func sourcesFromParams(fn *ssa.Function, conf *config) []*source {
 	return sources
 }
 
-func sourcesFromClosure(fn *ssa.Function, conf *config) []*source {
-	var sources []*source
+func sourcesFromClosure(fn *ssa.Function, conf *config) []*source.Source {
+	var sources []*source.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.isSource(s) {
-				sources = append(sources, newSource(p, conf))
+				sources = append(sources, source.New(p, conf))
 			}
 		}
 	}
 	return sources
 }
 
-func sourcesFromBlocks(fn *ssa.Function, conf *config) []*source {
-	var sources []*source
+func sourcesFromBlocks(fn *ssa.Function, conf *config) []*source.Source {
+	var sources []*source.Source
 	for _, b := range fn.Blocks {
 		if b == fn.Recover {
 			// TODO Handle calls to log in a recovery block.
@@ -598,14 +521,14 @@ func sourcesFromBlocks(fn *ssa.Function, conf *config) []*source {
 			// Looking for sources of PII allocated within the body of a function.
 			case *ssa.Alloc:
 				if conf.isSource(utils.Dereference(v.Type())) {
-					sources = append(sources, newSource(v, conf))
+					sources = append(sources, source.New(v, conf))
 				}
 
 				// 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, newSource(v, conf))
+					sources = append(sources, source.New(v, conf))
 				}
 			}
 		}
@@ -613,10 +536,10 @@ func sourcesFromBlocks(fn *ssa.Function, conf *config) []*source {
 	return sources
 }
 
-func report(pass *analysis.Pass, source *source, sink ssa.Node) {
+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.Node().Pos()))
 	pass.Reportf(sink.Pos(), b.String())
 }
 
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
new file mode 100644
index 0000000..e0a876b
--- /dev/null
+++ b/internal/pkg/source/source.go
@@ -0,0 +1,138 @@
+// 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 source contains the logic related to the concept of the source which may be tainted.
+package source
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/eapache/queue"
+	"github.com/google/go-flow-levee/internal/pkg/sanitizer"
+	"golang.org/x/tools/go/ssa"
+)
+
+type classifier interface {
+	IsSanitizer(*ssa.Call) bool
+	IsPropagator(*ssa.Call) bool
+	IsSourceFieldAddr(*ssa.FieldAddr) bool
+}
+
+// Source represents a Source in an SSA call tree.
+// It is based on ssa.Node, with the added functionality of computing the recursive graph of
+// its referrers.
+// Source.sanitized notes sanitizer calls that sanitize this Source
+type Source struct {
+	node       ssa.Node
+	marked     map[ssa.Node]bool
+	sanitizers []*sanitizer.Sanitizer
+	config     classifier
+}
+
+// Node returns the underlying ssa.Node of the Source.
+func (a *Source) Node() ssa.Node {
+	return a.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,
+	}
+	a.bfs()
+	return a
+}
+
+// bfs performs Breadth-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, bfs does not continue through that Node.
+func (a *Source) bfs() {
+	q := queue.New()
+	q.Add(a.node)
+	a.marked[a.node] = true
+
+	for q.Length() > 0 {
+		e := q.Remove().(ssa.Node)
+
+		if e.Referrers() == nil {
+			continue
+		}
+
+		for _, r := range *e.Referrers() {
+			if _, ok := a.marked[r.(ssa.Node)]; ok {
+				continue
+			}
+
+			if c, ok := r.(*ssa.Call); ok && a.config.IsSanitizer(c) {
+				a.sanitizers = append(a.sanitizers, &sanitizer.Sanitizer{Call: c})
+				continue
+			}
+
+			// Need to stay within the scope of the function under analysis.
+			if call, ok := r.(*ssa.Call); ok && !a.config.IsPropagator(call) {
+				continue
+			}
+
+			// Do not follow innocuous field access.
+			if addr, ok := r.(*ssa.FieldAddr); ok && !a.config.IsSourceFieldAddr(addr) {
+				continue
+			}
+
+			a.marked[r.(ssa.Node)] = true
+			q.Add(r)
+		}
+	}
+}
+
+// HasPathTo returns true when a Node is part of declaration-use graph.
+func (a *Source) HasPathTo(n ssa.Node) bool {
+	return a.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) {
+			return true
+		}
+	}
+
+	return false
+}
+
+// String implements Stringer interface.
+func (a *Source) String() string {
+	var b strings.Builder
+	fmt.Fprintf(&b, "%v\n", a.node.String())
+
+	fmt.Fprint(&b, "Connections:\n")
+	for k := range a.marked {
+		fmt.Fprintf(&b, "\t%v : %T\n", k.String(), k)
+	}
+
+	fmt.Fprint(&b, "Referrers:\n")
+	for _, k := range *a.node.Referrers() {
+		fmt.Fprintf(&b, "\t%v : %T\n", k.String(), k)
+	}
+
+	fmt.Fprint(&b, "Sanitizers:\n")
+	for _, k := range a.sanitizers {
+		fmt.Fprintf(&b, "\t%v : %T\n", k.Call, k)
+	}
+
+	return b.String()
+}


Test patch:
diff --git a/internal/pkg/source/source_test.go b/internal/pkg/source/source_test.go
new file mode 100644
index 0000000..1b7b381
--- /dev/null
+++ b/internal/pkg/source/source_test.go
@@ -0,0 +1,160 @@
+// 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 source
+
+import (
+	"github.com/google/go-flow-levee/internal/pkg/utils"
+	"reflect"
+	"regexp"
+	"testing"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/passes/buildssa"
+	"golang.org/x/tools/go/ssa"
+)
+
+type testConfig struct {
+	propagatorsPattern string
+	fieldsPattern      string
+	sanitizerPattern   string
+}
+
+func (c *testConfig) IsSanitizer(call *ssa.Call) bool {
+	match, _ := regexp.MatchString(c.sanitizerPattern, call.String())
+	return match
+}
+
+func (c *testConfig) IsPropagator(call *ssa.Call) bool {
+	match, _ := regexp.MatchString(c.propagatorsPattern, call.String())
+	return match
+}
+
+func (c *testConfig) IsSourceFieldAddr(field *ssa.FieldAddr) bool {
+	match, _ := regexp.MatchString(c.fieldsPattern, utils.FieldName(field))
+	return match
+}
+
+type analyzerResult struct {
+	allocations []*ssa.Alloc
+	calls       []*ssa.Call
+	fieldAddr   []*ssa.FieldAddr
+	store       []*ssa.Store
+}
+
+var testAnalyzer = &analysis.Analyzer{
+	Name:       "source",
+	Run:        run,
+	Doc:        "test harness for the logic related to sources",
+	Requires:   []*analysis.Analyzer{buildssa.Analyzer},
+	ResultType: reflect.TypeOf(analyzerResult{}),
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	in := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+	var result analyzerResult
+	for _, fn := range in.SrcFuncs {
+		for _, b := range fn.Blocks {
+			for _, i := range b.Instrs {
+				switch v := i.(type) {
+				case *ssa.Alloc:
+					result.allocations = append(result.allocations, v)
+				case *ssa.Call:
+					result.calls = append(result.calls, v)
+				case *ssa.FieldAddr:
+					result.fieldAddr = append(result.fieldAddr, v)
+				case *ssa.Store:
+					result.store = append(result.store, v)
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+func TestSource(t *testing.T) {
+	dir := analysistest.TestData()
+	config := &testConfig{
+		propagatorsPattern: "propagator",
+		sanitizerPattern:   "sanitizer",
+		fieldsPattern:      "name",
+	}
+
+	testCases := []struct {
+		pattern                      string
+		config                       *testConfig
+		wantCallConnectionIdx        []int
+		wantFieldAccessConnectionIdx []int
+		wantStoreConnectionIdx       []int
+	}{
+		{
+			pattern:                      "allocation",
+			config:                       config,
+			wantStoreConnectionIdx:       []int{0},
+			wantFieldAccessConnectionIdx: []int{0},
+		},
+		{
+			pattern:                      "propagation",
+			config:                       config,
+			wantCallConnectionIdx:        []int{0},
+			wantFieldAccessConnectionIdx: []int{0},
+		},
+		{
+			pattern:                      "sanitization",
+			config:                       config,
+			wantFieldAccessConnectionIdx: []int{0},
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.pattern, func(t *testing.T) {
+			r := analysistest.Run(t, dir, testAnalyzer, tt.pattern)
+			if len(r) != 1 {
+				t.Fatalf("Got len(result) == %d, want 1", len(r))
+			}
+
+			a, ok := r[0].Result.(analyzerResult)
+			if !ok {
+				t.Fatalf("Got result of type %T, wanted analyzerResult", a)
+			}
+
+			if len(a.allocations) == 0 {
+				t.Fatal("Got 0 allocations want at least one")
+			}
+
+			src := New(a.allocations[0], tt.config)
+			t.Logf("Testing source:\n%v", src)
+
+			for i := 0; i < len(tt.wantCallConnectionIdx); i++ {
+				if !src.HasPathTo(a.calls[i]) {
+					t.Errorf("Expected\n%v to have a path to %v", src, a.calls[i])
+				}
+			}
+
+			for i := 0; i < len(tt.wantStoreConnectionIdx); i++ {
+				if !src.HasPathTo(a.store[i]) {
+					t.Errorf("Expected\n%v to have a path to %v", src, a.store[i])
+				}
+			}
+
+			for i := 0; i < len(tt.wantFieldAccessConnectionIdx); i++ {
+				if !src.HasPathTo(a.fieldAddr[i]) {
+					t.Fatalf("Expected\n%v to have a path to %v", src, a.fieldAddr[i])
+				}
+			}
+		})
+	}
+}
diff --git a/internal/pkg/source/testdata/src/allocation/test.go b/internal/pkg/source/testdata/src/allocation/test.go
new file mode 100644
index 0000000..78d026c
--- /dev/null
+++ b/internal/pkg/source/testdata/src/allocation/test.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 allocation
+
+import (
+	"fmt"
+)
+
+type foo struct {
+	name string
+}
+
+func f1() {
+	f := &foo{name: "bar"}
+
+	// Since f will exit ths scope of f1, log will not be added to the graph of f.
+	// Expected graph:
+	// new foo (complit) : *ssa.Alloc
+	// &t0.name [#0] : *ssa.FieldAddr
+	// *t1 = "bar":string : *ssa.Store
+
+	log(f)
+}
+
+func log(in *foo) {
+	fmt.Printf("Logging foo: %v", in)
+}
diff --git a/internal/pkg/source/testdata/src/propagation/test.go b/internal/pkg/source/testdata/src/propagation/test.go
new file mode 100644
index 0000000..46e1895
--- /dev/null
+++ b/internal/pkg/source/testdata/src/propagation/test.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 propagation
+
+import "fmt"
+
+type foo struct {
+	name string
+}
+
+func f1() {
+	f := &foo{name: "bar"}
+	transformedFoo := propagator(f)
+	fmt.Println(transformedFoo)
+}
+
+func propagator(in *foo) string {
+	return fmt.Sprintf("Transformed foo: %v", in)
+}
diff --git a/internal/pkg/source/testdata/src/sanitization/test.go b/internal/pkg/source/testdata/src/sanitization/test.go
new file mode 100644
index 0000000..b791dec
--- /dev/null
+++ b/internal/pkg/source/testdata/src/sanitization/test.go
@@ -0,0 +1,32 @@
+// 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 sanitization
+
+import "fmt"
+
+type foo struct {
+	name     string
+	password string
+}
+
+func f1() {
+	f := &foo{name: "n", password: "p"}
+	sanitizer(f)
+	fmt.Println(f)
+}
+
+func sanitizer(in *foo) {
+	in.password = "redacted"
+}