analysis

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package analysis implements Godzilla's taint analysis engine: it walks gIR programs and reports Findings for source-to-sink dataflows described by a rules.RuleSet, alongside two non-dataflow passes (dangerous-call and secrets).

Why the design is shaped this way

One engine serves every language. Frontends lower Go, Python, JavaScript, Java, Ruby, Rust and C/C++ into the same IR, so nothing here may match on a language's callee names: anything language-specific arrives either as a canonical FQN a RULE matches, or as an intrinsic the frontends agree on (builtin.format, builtin.identity, builtin.kwarg, builtin.aggregate/builtin.aggregate_map). When a check seems to need a language check, the fix is almost always a new intrinsic or a rule glob.

The core loop

Engine.Analyze (interproc.go) is an inter-procedural, context-insensitive worklist. Within a function, taint moves by SSA def-use plus the transfer helpers in taint.go; across functions it moves by summary — a tainted argument taints the callee's parameter, and a taint-returning callee taints its caller's call result. callgraph.go supplies CHA for dynamic dispatch, and its reverse edges re-enqueue a callee's callers when the callee becomes taint-returning.

Findings carry a Confidence: intra-procedural is High, cross-function Medium. That is what the LLM reviewer triages on, and it is deliberately independent of severity, which is what the CI gate keys on.

Precision guards

These are the parts most likely to be "simplified" into a regression; each is argued in full at its own definition:

  • Sink-parameter summaries (funcResult.taintsParamSink) report a flow into a dependency's sink wrapper at the USER call site, since the dep-internal finding is scoped out. String-typed parameters only.

  • Framework-agnostic HTTP request sources come from two complementary, name-list-free mechanisms. A framework's own accessor is tainted at the CALL SITE by a rule source glob — a deliberate performance choice, since seeding the framework's context object would force taint through its whole request pipeline. For frameworks layered on net/http, the stdlib request accessors are default propagators, carrying request taint through internal parsing at no false-positive cost.

  • ssrf.go supplies the hostFixed() FACT, not a decision: whether it suppresses anything is the RULE's choice, via `when: 'not hostFixed()'`. The engine must not branch on CWE — that silently denies the reduction to a custom rule tagged anything else, and to open-redirect.

  • guards.go (dominator-based validator suppression) needs a real CFG, which every frontend now emits. linearFn marks branch-free functions in any language, where program order already is dominance.

Files

interproc.go the worklist and call handling; taint.go transfer helpers; flow.go path reconstruction; guards.go dominator guards; callgraph.go CHA; ssrf.go host-fixedness; dangerous.go call-site rules; secrets.go CWE-798; fingerprint.go baseline identity; finding.go the shared Finding type.

Index

Constants

This section is empty.

Variables

Confidences lists every recognized confidence from most to least certain. It is the single authority for confidence ordering: Rank derives from it, and display surfaces (the HTML report) iterate it, so a new confidence level shows up everywhere by being added here once.

Functions

func BuildDefs

func BuildDefs(fn *ir.Function) map[string]*ir.Instruction

BuildDefs maps each SSA result register to the instruction that defines it, so taint transfer can walk value-derivation chains (e.g. from an element address back to its container).

func CompareFindings

func CompareFindings(a, b Finding) int

CompareFindings is THE display order for findings: worst severity first, ties broken by sink location — filename, then line, then column, compared NUMERICALLY (line 9 before line 10) — with unknown (nil) sink positions last within their severity. The CLI's console listing and all three report writers (HTML, JSON, SARIF) sort with this single comparator so their output for the same scan always agrees. Suitable for slices.SortStableFunc.

func Fingerprint

func Fingerprint(f Finding) string

Fingerprint returns a stable identifier for a finding, suitable for baseline matching and diff-aware gating. It deliberately hashes only line-INDEPENDENT attributes — the rule, the repo-relative sink and source file paths, the enclosing function, and the sink callee — and NOT line/column numbers, so editing unrelated code above a finding does not change its fingerprint.

Two distinct findings that share all of those attributes (e.g. two calls to the same sink in one function) collide by design; callers that need to tell them apart consume fingerprints as a multiset (see the triage package's baseline matching), which is stable under add/remove of a single occurrence.

func LogicalArgs

func LogicalArgs(cc *ir.CallCommon) []*ir.Value

LogicalArgs returns a call's arguments in SOURCE-LEVEL order, dropping a method receiver carried as args[0]. Whether args[0] is a receiver is read from the IR the converter supplies, not from the callee-name shape: a statically-resolved method call is a non-invoke call that names its method (MethodName set), and puts the receiver first; an INVOKE keeps the receiver in Call.Value (args are already logical); a free function has no receiver. So logical argument indices line up across every language: index 0 is the first real argument.

func PosString

func PosString(p *ir.Position) string

PosString renders an *ir.Position as "file:line:col", or "<unknown>" when p is nil. Shared by the CLI, the LLM reviewer, and the report writer so they all format positions identically.

func UnwrapKwarg

func UnwrapKwarg(v *ir.Value, defs map[string]*ir.Instruction) (string, *ir.Value)

UnwrapKwarg resolves v through a kwargIntrinsic marker, returning the keyword name and the value it wraps. For anything else it returns ("", v), so callers can apply it unconditionally. defs may be nil, in which case a marker cannot be resolved and v is returned unchanged.

Types

type CallGraph

type CallGraph struct {
	// Funcs indexes every function in the program by its CanonicalName. A function
	// with an empty one gets a unique "__local<N>" fallback key so it is still
	// analyzed intra-procedurally. This is the ONE name->function index, shared
	// with Analyze (see buildFuncIndex).
	Funcs map[string]*ir.Function

	// Edges maps a caller's CanonicalName to a sorted, de-duplicated list
	// of callee names. Every name appearing in Edges is guaranteed to be a
	// key in Funcs -- calls we could not resolve to a known function
	// (stdlib/external code that was never lowered to gIR, or a dynamic
	// dispatch with no known implementation) are simply dropped, so Edges
	// never dangles. A "__local<N>"-keyed function is not addressable by
	// callers, so it never appears as an Edges key.
	Edges map[string][]string

	// Callees maps each DISTINCT callee name this program calls to how many call
	// SITES name it, including the unresolved ones Edges drops (unlowered stdlib,
	// dynamic dispatch). The engine uses the key set to skip a rule whose sink
	// globs match nothing the program calls, and the counts feed the report's
	// source/sink workload figures; both are collected here rather than by a
	// separate walk because this pass already visits every instruction, and a
	// second walk cost more than the skipping saved.
	Callees map[string]int
}

CallGraph is a whole-program call graph over gIR functions. The inter-procedural taint engine (interproc.go) consumes it for its reverse edges (see buildCallers): when a callee is discovered to return taint, every caller that calls it is re-enqueued so the new return summary propagates.

Build one with buildCallGraph, over the shared function/method indexes (buildFuncIndex, buildMethodImpls).

type Confidence

type Confidence string

Confidence expresses how certain the engine is that a finding is a true positive. Intra-procedural source->sink flows are High; flows that cross a function boundary (taint entering through a parameter) are Medium, since the context-insensitive summary merges all call sites and may over-approximate. Lower-confidence findings are the ones the LLM reviewer triages.

const (
	ConfidenceHigh   Confidence = "high"
	ConfidenceMedium Confidence = "medium"
	ConfidenceLow    Confidence = "low"
)

func ParseConfidence

func ParseConfidence(s string, def Confidence) Confidence

ParseConfidence maps a rule's declared `confidence:` spelling onto a Confidence, falling back to def for the empty (unset) string and for anything unrecognized — the loader already rejects a typo (rules.ValidConfidence), so a bad value here means a programmatically-built rule, and defaulting is safer than reporting a finding at a confidence nothing downstream understands. It deliberately returns only the canonical lowercase constants: Rank ranks any other string 0 and the LLM reviewer then never reviews the finding, which would look fine in the HTML report while being permanently un-triageable.

func (Confidence) Rank

func (c Confidence) Rank() int

Rank returns a comparable ordering for a confidence (higher is more certain): low=1, medium=2, high=3. Anything else — including a differently-cased spelling — ranks 0, which consumers treat as un-triageable: the LLM reviewer only reviews findings whose rank is positive and at/below its threshold. That is why ParseConfidence returns only the canonical lowercase constants.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine runs taint analysis over a gIR program for a fixed set of rules. Analysis is inter-procedural; see interproc.go for the orchestration.

func NewEngine

func NewEngine(rs *rules.RuleSet) *Engine

NewEngine builds an Engine that will evaluate every rule in rs.

func (*Engine) Analyze

func (e *Engine) Analyze(prog *ir.Program) []Finding

Analyze runs inter-procedural taint analysis over prog for every rule in the engine's rule set and returns all findings.

Taint flows across call boundaries via context-insensitive function summaries: a tainted argument taints the callee's corresponding parameter, and a function that can return tainted data taints its callers' call results. A worklist re-analyzes functions until this state stabilizes.

func (*Engine) AnalyzeWithStats

func (e *Engine) AnalyzeWithStats(prog *ir.Program) ([]Finding, Stats)

AnalyzeWithStats is Analyze plus what the run observed about the program and about its own cost, for the report's scan diagnostics. The numbers come back as a return value rather than engine state, which would make concurrent or repeated use of one Engine unsafe.

func (*Engine) ScopeSeed

func (e *Engine) ScopeSeed(reportable map[string]bool) *Engine

ScopeSeed restricts the worklist SEED to the given (user-authored) packages: a lowered dependency function is then analyzed DEMAND-DRIVEN, only when taint actually reaches it via a call, instead of the whole dependency closure being walked up front. A nil/empty set seeds every function. Returns e for chaining.

type Finding

type Finding struct {
	RuleID     string
	Severity   rules.Severity
	Confidence Confidence
	CWE        string
	Message    string
	Language   string
	Function   string // enclosing function's CanonicalName
	Package    string // enclosing function's package (for user-code scoping; see internal/scan)
	SourcePos  *ir.Position
	SinkPos    *ir.Position
	SinkCallee string

	// RuleSanitizers and RuleSources are the matched rule's sanitizer/source
	// globs, carried onto the finding so the LLM reviewer can adjudicate using the
	// rulepack's OWN vocabulary (which documented sanitizer neutralizes this sink,
	// what the sources are) instead of second-guessing from generic knowledge
	// (LLM-8). Not serialized in reports.
	RuleSanitizers []string
	RuleSources    []string

	// Steps is the ordered taint path from source to sink (inclusive), when it
	// can be reconstructed intra-procedurally by walking the def-use chain. It
	// powers SARIF codeFlows (which GitHub code scanning renders as a data-flow)
	// and richer triage. Empty when only the endpoints are known (e.g. a flow
	// whose middle crossed a function boundary).
	Steps []*ir.Position

	// Suppressed marks a finding that a downstream triage stage (the LLM
	// reviewer) judged a false positive. A suppressed finding is RETAINED, not
	// discarded: it does not count toward the gate, but it stays visible in
	// reports with SuppressedBy/SuppressionReason so a nondeterministic model can
	// never silently erase a finding. Auditability over silent deletion.
	Suppressed        bool
	SuppressedBy      string // what suppressed it, e.g. "llm-review"
	SuppressionReason string // the reviewer's stated justification

	// ReviewConfirmed marks a finding the LLM reviewer adjudicated as a TRUE
	// positive (kept, not suppressed); ReviewNote carries the reviewer's
	// exploitability/reasoning. This surfaces the value of a review on the
	// findings it KEEPS — a confirmed interprocedural finding is higher-priority
	// triage — instead of only recording the ones it drops (LLM-7).
	ReviewConfirmed bool
	ReviewNote      string
}

Finding is a single reported vulnerability: a tainted value from some Source reaching a Sink without passing through a Sanitizer.

func ScanDangerousCalls

func ScanDangerousCalls(prog *ir.Program, rs *rules.RuleSet) []Finding

ScanDangerousCalls evaluates every `kind: dangerous-call` rule (COV-4) syntactically over the program: any call whose callee matches a rule's Callees glob is a finding, optionally gated on a constant string argument (e.g. MessageDigest.getInstance("MD5")). This is a non-dataflow pass — no taint tracking — for the zero-noise categories (weak crypto/ciphers, insecure randomness) the taint engine cannot express. Findings are High confidence BY DEFAULT (call-site-deterministic); a rule that is heuristic rather than deterministic may declare `confidence:` to lower that, which is what makes its findings eligible for LLM triage. Findings are deduped per (rule, position).

func ScanSecrets

func ScanSecrets(prog *ir.Program, rs *rules.RuleSet) []Finding

ScanSecrets walks a gIR program for hardcoded secrets embedded in string constants, using rs's `kind: secret` rules. This is a non-dataflow, pattern-based analysis (distinct from the taint engine) and complements it in the same Finding stream.

func ScanSecretsInFiles

func ScanSecretsInFiles(root string, rs *rules.RuleSet, isSource func(path string) bool) []Finding

ScanSecretsInFiles walks root for textual CONFIG files that the language frontends never parse — .env, docker-compose.yml, Dockerfile, CI YAML, .npmrc, .properties, Terraform, and the like — and applies the secret patterns line by line, reporting file:line positions. This covers a credential committed to a config file rather than source code, which the gIR-constant scanner (ScanSecrets) cannot see. Source files handled by a frontend are skipped to avoid double-reporting: isSource is the caller's REQUIRED "a language frontend handles this path" predicate (internal/scan derives it from its frontend table, the single source of truth for supported extensions). root may be a file or a directory; a non-existent path yields no findings.

func ScanSecretsInPaths

func ScanSecretsInPaths(paths []string, rs *rules.RuleSet, isSource func(path string) bool) []Finding

ScanSecretsInPaths is ScanSecretsInFiles over an explicit, pre-walked file list — the scan pipeline's cached directory inventory (walkignore.Inventory) — so the config-file secrets pass adds no directory walk of its own. File selection is identical: same scannable-config predicate, same (required) isSource skip, same excluded-path and size policies, applied per file by scanConfigPath.

func (Finding) String

func (f Finding) String() string

String renders a one-line human-readable summary of the finding.

type Stats

type Stats struct {
	// Functions is the call-graph node count — distinct canonical function
	// names, which is a smaller set than the raw IR functions when two collapse
	// onto one name.
	Functions int

	// RulesLive is the rules the engine actually seeded, which is only the
	// DATAFLOW rules: canProduceFinding rejects a rule with no sinks, i.e. every
	// `kind: secret` and `kind: dangerous-call` rule — and those are genuinely
	// evaluated, by ScanSecrets and ScanDangerousCalls. Never present it as the
	// number of rules that ran.
	Rules     int
	RulesLive int

	// SourceSites and SinkSites count the CALL SITES whose callee matches some
	// rule's source or sink glob. They are a workload figure, not a bound on
	// findings: a sink match here ignores `#idx` injection-point pinning and
	// every `when:` guard, and a source match sees only callee-glob matches — the
	// seeding that is not a call at all (addHTTPRequestSource,
	// buildReqSourceHosts, request-object provenance) is invisible to it.
	SourceSites int
	SinkSites   int

	Index      time.Duration // function/method index and call graph
	RuleSelect time.Duration // rule compile, the can-produce-a-finding prefilter, and the counts above
	Taint      time.Duration // the parallel per-rule worklist
}

Stats is what one AnalyzeWithStats run observed about the program and about its own cost. Index, RuleSelect and Taint are disjoint wall spans that partition the run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL