---
title: 'Static Analysis and Security Scanning'
source: 'https://academia.sh/en/courses/go-production/static-analysis-and-security-scanning'
course: 'Production Go: Testing, Tooling and Performance'
language: en
updated: '2026-09-14T10:59:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the difference between two sources that the compiler cannot see but an analyzer reports through an exit code, with a single measurement'
    - 'Counts how many of three patterns the compiler rejects none of are caught by the analyzer'
    - 'Bounds analysis coming out clean as not meaning flawless, by running an uncaught pattern'
---

# Static Analysis and Security Scanning

Of the same two sources, one is clean, the other flawed; the compiler accepts both the same way, but an analyzer reports the difference between them through an exit code. Of three patterns, the compiler catches none, the analyzer catches two. The limiting measurement shows, by running it, that coming out clean from analysis does not mean flawless — the third, uncaught pattern genuinely crashes when run. A final measurement compares a dependency's declared version against a modeled threshold list.

The tool measured in the previous lesson read the source only at the level of names and types;
it never looked at what was inside the bodies at all. This lesson descends into exactly that gap:
a tool that reads the body itself, searching for specific patterns. But this tool has its own
limit too — reporting what it found does not mean that what it did not search for does not exist.

## Two Sources, One Analyzer

**Static analysis** is a process that searches a source, without ever running it, only by reading
its text, for specific fault patterns. The language's own distribution's analysis tool does this
job with a single command and reports only an **exit code** — in this lesson we never read the
text of the lines it found, only whether the result is clean or flawed.

The measurement below sets up two sources: one prints an integer with the correct format
verb, the other gives a string to the same format verb — a mismatch between type and format.

```go
// helpers.go — the helpers this lesson shares: setting up a temporary package and calling the tool
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// show prints a fact, who states it, and its evidence on a single line.
func show(info, speaker, evidence string) {
	fmt.Printf("%-40s %-7s %s\n", info, speaker, evidence)
}

// setupModule writes the given files into a new temporary module; the path is never printed.
func setupModule(files map[string]string) string {
	dir, err := os.MkdirTemp("", "analysis")
	if err != nil {
		return ""
	}
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module analysistrial\n\ngo 1.24\n"), 0o644)
	for name, content := range files {
		os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)
	}
	return dir
}

// runTool runs the language's own command, returning the output and whether it succeeded; output is never printed.
func runTool(dir string, arg ...string) (string, bool) {
	cmd := exec.Command("go", arg...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	output, err := cmd.CombinedOutput()
	return string(output), err == nil
}

// compiles attempts to build the package in the given directory.
func compiles(dir string) bool {
	_, ok := runTool(dir, "build", "-o", os.DevNull, ".")
	return ok
}

// analysisClean says whether the package in the given directory passes static analysis clean.
func analysisClean(dir string) bool {
	_, ok := runTool(dir, "vet", "./...")
	return ok
}
```

```go
// measurement1.go — same two sources, one clean one flawed, both passed through a single analyzer
package main

import "fmt"

func main() {
	clean := setupModule(map[string]string{
		"prog.go": "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Printf(\"value=%d\\n\", 7)\n}\n",
	})
	flawed := setupModule(map[string]string{
		"prog.go": "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Printf(\"value=%d\\n\", \"text\")\n}\n",
	})

	fmt.Println("-- two sources, one analyzer --")
	show("clean source compiled", "tool", fmt.Sprint(compiles(clean)))
	show("flawed source compiled", "tool", fmt.Sprint(compiles(flawed)))
	show("clean source analysis", "tool", fmt.Sprint("clean=", analysisClean(clean)))
	show("flawed source analysis", "tool", fmt.Sprint("clean=", analysisClean(flawed)))
}
```

```
-- two sources, one analyzer --
clean source compiled                    tool    true
flawed source compiled                   tool    true
clean source analysis                    tool    clean=true
flawed source analysis                   tool    clean=false
```

**BD15.** Both sources compile — the type mismatch is not a compiler-level error, because the
relationship between a format string and its arguments is not the subject of the compiler's type
checking. The analyzer, by contrast, does distinguish these two lines: in one the format verb and
the argument type match, in the other they do not. This is another form of the "tool" knowledge
seen in previous lessons — the analyzer searches for a pattern without running the source, only
by reading it, and reports what it found through a single exit code.

## The Pattern the Compiler Does Not Catch

The single pattern above is only a beginning. The measurement below tries three separate patterns
together: a format mismatch, a copied lock value, and an ignored error. All three compile; the
question asked is how many are caught by the analyzer.

```go
// measurement2.go — three patterns the compiler rejects none of, splitting apart as they pass through the analyzer
package main

import "fmt"

const patternFormat = "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Printf(\"value=%d\\n\", \"text\")\n}\n"

const patternLock = "package main\n\nimport \"sync\"\n\ntype Counter struct {\n\tlock sync.Mutex\n\tvalue int\n}\n\nfunc copyIt(c Counter) Counter {\n\treturn c\n}\n\nfunc main() {\n\tcopyIt(Counter{})\n}\n"

const patternIgnoredError = "package main\n\nimport \"fmt\"\n\ntype Record struct{ Name string }\n\nfunc find(key string) (*Record, error) {\n\tif key == \"\" {\n\t\treturn nil, fmt.Errorf(\"empty key\")\n\t}\n\treturn &Record{Name: key}, nil\n}\n\nfunc main() {\n\tr, _ := find(\"\")\n\tfmt.Println(r.Name)\n}\n"

func main() {
	patterns := []struct{ name, source string }{
		{"format mismatch", patternFormat},
		{"lock copy", patternLock},
		{"ignored error", patternIgnoredError},
	}

	fmt.Println("-- three patterns the compiler does not catch --")
	compiledCount, cleanCount := 0, 0
	for _, p := range patterns {
		dir := setupModule(map[string]string{"prog.go": p.source})
		c := compiles(dir)
		cl := analysisClean(dir)
		if c {
			compiledCount++
		}
		if cl {
			cleanCount++
		}
		show(p.name+" compiled", "tool", fmt.Sprint(c))
		show(p.name+" analysis clean", "tool", fmt.Sprint(cl))
	}
	show("compiled count of three patterns", "tool", fmt.Sprint(compiledCount))
	show("analysis-passed count of three patterns", "tool", fmt.Sprint(cleanCount))
	show("pattern count caught by analyzer", "tool", fmt.Sprint(len(patterns)-cleanCount))
}
```

```
-- three patterns the compiler does not catch --
format mismatch compiled                 tool    true
format mismatch analysis clean           tool    false
lock copy compiled                       tool    true
lock copy analysis clean                 tool    false
ignored error compiled                   tool    true
ignored error analysis clean             tool    true
compiled count of three patterns         tool    3
analysis-passed count of three patterns  tool    1
pattern count caught by analyzer         tool    2
```

**BD16.** All three patterns compile — the number of patterns caught by the compiler is zero.
These three patterns sit entirely outside the layer the compiler works in, because all three are
**type-valid** code: a wrongly formatted print call, a struct copied by value with a lock field
inside, an ignored error value — none of these is a type mismatch or a syntax error. The analyzer
catches two of the three patterns: the format mismatch and the lock copy. The third — the ignored
error — comes out **clean** from analysis. This third pattern is the subject of the next section.

The two caught patterns share a common property: both fit a pattern that sits **outside** the
language's own type system, but is still mechanically recognizable. In the format mismatch, the
pattern is whether every `%` mark in a format string matches the type of the argument supplied;
in the lock copy, the pattern is whether a type documented by the `sync` package (any struct
containing a lock) is carried by pointer rather than by value. The analyzer enforces both these
rules by parsing the source and searching for a specific shape in the syntax tree — much like the
signature comparator of the previous lesson, except what is searched here is not a signature but
a usage pattern. The third pattern's failure to be caught is not because the analyzer fails to
match **either** of these two rules — it is because a rule searching for an ignored error is not
in the list at all.

## Limiting Measurement: Coming Out Clean Does Not Mean Flawless

The third pattern above is a call that ignores a function's returned error and uses the result
directly. The analyzer found this clean; the compiler did not reject it either. The measurement
below genuinely runs this pattern — this time not by calling a subprocess, but by running the
same logic inside this very measurement, because the panic here will be caught with `recover` and
read only as a boolean.

```go
// measurement3.go — limiting measurement: coming out clean does not mean flawless
package main

import "fmt"

const patternIgnoredError = "package main\n\nimport \"fmt\"\n\ntype Record struct{ Name string }\n\nfunc find(key string) (*Record, error) {\n\tif key == \"\" {\n\t\treturn nil, fmt.Errorf(\"empty key\")\n\t}\n\treturn &Record{Name: key}, nil\n}\n\nfunc main() {\n\tr, _ := find(\"\")\n\tfmt.Println(r.Name)\n}\n"

// Record and find are a genuine, runnable counterpart of the source above: they carry the exact
// same logic that the analyzer read as text.
type Record struct{ Name string }

func find(key string) (*Record, error) {
	if key == "" {
		return nil, fmt.Errorf("empty key")
	}
	return &Record{Name: key}, nil
}

// attempt runs the call that ignores the error and catches the panic; the stack trace is never printed.
func attempt() (panicked bool) {
	defer func() {
		if r := recover(); r != nil {
			panicked = true
		}
	}()
	r, _ := find("")
	_ = r.Name
	return false
}

func main() {
	dir := setupModule(map[string]string{"prog.go": patternIgnoredError})

	fmt.Println("-- limiting measurement: coming out clean does not mean flawless --")
	show("analysis result", "tool", fmt.Sprint("clean=", analysisClean(dir)))
	show("panicked when run", "run", fmt.Sprint(attempt()))
}
```

```
-- limiting measurement: coming out clean does not mean flawless --
analysis result                          tool    clean=true
panicked when run                        run     true
```

**BD17.** The analyzer's answer (`clean=true`) and the run's answer (`panicked=true`) say two
separate things about the same source, and both are correct. The analyzer says "I found none of
the patterns I search for" — this does not include a rule searching for an ignored error. The run
says "this source, run with this input, crashes." These two answers do not contradict each other;
both are bounded by the tool's **scope**. The `clean=true` line therefore cannot be read as "this
source has no flaw" — it can only be read as "none of the patterns the analyzer knows about are
present in this source." This is the same distinction the course already established for coverage
ratio: a tool's silence does not mean the thing it **does not search for** does not exist.

The practical counterpart of this limit is that an analyzer's rule list is not **fixed**, but it
is always **finite**. As new patterns are noticed, new rules can be added to the list — the two
rules used specifically in this lesson (format mismatch, lock copy) were added this way too, entering
the list only after a real fault was seen repeatedly in real programs. But no matter how large the
list grows, there is always a pattern not yet added; this lesson chose a single example to make
this gap concrete, but the chosen example itself is not what matters — what matters is that an
analyzer's **clean** answer always means "clean according to the rules known so far," carrying no
claim of absolute correctness.

## Known Vulnerability Scan: A Modeled List

The patterns the analyzer searches for are bounded by the language's own rules. A dependency's
known **vulnerability** is a separate kind of information — it comes not from the code's own
shape, but from **which version** the dependency was fixed in. A **known vulnerability scan**
compares a dependency's declared version against the version the fix arrived in. The measurement
below never connects to a real vulnerability list at all; the comparison is **modeled** in the
source with a small threshold table.

```go
// measurement4.go — known vulnerability scan, a tool built with a modeled threshold list
package main

import "fmt"

// Version carries a three-part version number (same pattern as M09/K01 go-packages/02).
type Version struct{ Major, Minor, Patch int }

func (v Version) text() string { return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) }

func (v Version) lessThan(other Version) bool {
	if v.Major != other.Major {
		return v.Major < other.Major
	}
	if v.Minor != other.Minor {
		return v.Minor < other.Minor
	}
	return v.Patch < other.Patch
}

// safeThreshold is a MODELED table holding the known lowest safe version for each module; not a
// real vulnerability database, only a small list this measurement itself sets up.
var safeThreshold = map[string]Version{
	"libraryX": {1, 4, 0},
	"libraryY": {2, 0, 0},
}

type Dependency struct {
	Module   string
	Declared Version
}

// hasVulnerability checks whether the declared version falls below the modeled threshold.
func hasVulnerability(d Dependency) bool {
	threshold, exists := safeThreshold[d.Module]
	if !exists {
		return false
	}
	return d.Declared.lessThan(threshold)
}

func main() {
	dependencies := []Dependency{
		{Module: "libraryX", Declared: Version{1, 2, 0}},
		{Module: "libraryY", Declared: Version{2, 3, 0}},
	}

	fmt.Println("-- known vulnerability scan: a modeled list --")
	vulnerableCount := 0
	for _, d := range dependencies {
		v := hasVulnerability(d)
		if v {
			vulnerableCount++
		}
		show(d.Module+" declared="+d.Declared.text(), "tool", fmt.Sprint("vulnerable=", v))
	}
	show("vulnerable count of two dependencies", "tool", fmt.Sprint(vulnerableCount))
}
```

```
-- known vulnerability scan: a modeled list --
libraryX declared=1.2.0                  tool    vulnerable=true
libraryY declared=2.3.0                  tool    vulnerable=false
vulnerable count of two dependencies     tool    1
```

**BD18.** The declared version for `libraryX` (`1.2.0`) falls below the modeled threshold
(`1.4.0`) — vulnerable. The declared version for `libraryY` (`2.3.0`) exceeds its threshold
(`2.0.0`) — not vulnerable. This comparison carries the same limit as the previous two
measurements: the threshold table itself **is a model this measurement built**, it does not come
from a real record, and if a module is not in the table at all, `hasVulnerability` silently
returns `false` — a missing table means the scan finds nothing, exactly like the analyzer never
seeing a pattern it does not search for. The **Go Fundamentals** course's minimal version
selection measurement established which version gets selected; this measurement asks, through a
separate, modeled comparison, whether that selected version falls below a known threshold.

## Summary

- Static analysis works by searching for specific patterns without ever running the source; it
  reports the difference between two sources the compiler accepts only through an exit code.
- Of the three patterns the compiler rejects none of, two were caught by the analyzer; one — the
  ignored error — came out clean.
- Limiting measurement: the pattern that came out clean crashed when genuinely run; the
  analyzer's silence means not flawlessness, only that the pattern searched for was not found.
- A known vulnerability scan compares a dependency's declared version against a modeled
  threshold; if the table is missing an entry, the scan finds nothing at all.
- The measure used throughout this lesson stayed the same: a tool's silence shows that what it
  searched for was not found, not that the thing itself does **not exist**.

## Next Step

The analyzer measured in this lesson stayed within the language's own boundaries — the patterns
it searched for always belonged to Go's own syntax and types. The course's closing lesson crosses
this boundary: when a Go program calls another language's library, one more boundary opens, one
the analyzer cannot see through either. The next question is which tool, with which number, the
cost of crossing the language's own boundary becomes visible through.
