Repository: google/go-flow-levee
Pull request #73 (2020-08-28 14:28:01)

Avoid future json errors
This PR aims to avoid typos such as #72 from happening again by validating the JSON files in the repository using `jq`.

While the existing `go test ./...` check already validates that all of the configs used in testing are properly formed, any other JSON files are not currently being validated.

- [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/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++
 		}


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/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
 }