From 078a62afc3331206fec1cd9a03637983ec4f9fc8 Mon Sep 17 00:00:00 2001 From: Cosmin Cojocar Date: Sat, 21 Feb 2026 12:35:16 +0100 Subject: [PATCH] Expand analyzer-core test coverage for orchestration, go/analysis adapter logic, and taint integration (#1537) Signed-off-by: Cosmin Cojocar --- analyzer_core_internal_test.go | 109 +++++++++++++++++ goanalysis/analyzer_internal_test.go | 177 +++++++++++++++++++++++++++ taint/analyzer_internal_test.go | 110 +++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 analyzer_core_internal_test.go create mode 100644 goanalysis/analyzer_internal_test.go create mode 100644 taint/analyzer_internal_test.go diff --git a/analyzer_core_internal_test.go b/analyzer_core_internal_test.go new file mode 100644 index 0000000..5b067c8 --- /dev/null +++ b/analyzer_core_internal_test.go @@ -0,0 +1,109 @@ +package gosec + +import ( + "errors" + "go/types" + "io" + "log" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/buildssa" + "golang.org/x/tools/go/packages" + + "github.com/securego/gosec/v2/issue" +) + +func TestCheckAnalyzersShortCircuitsWithoutAnalyzers(t *testing.T) { + t.Parallel() + + a := NewAnalyzer(NewConfig(), false, false, false, 1, log.New(io.Discard, "", 0)) + issues, stats := a.checkAnalyzers(nil, nil) + + if issues != nil { + t.Fatalf("expected nil issues when no analyzers are loaded") + } + if stats == nil { + t.Fatalf("expected non-nil metrics") + } + if stats.NumFound != 0 { + t.Fatalf("unexpected findings count: %d", stats.NumFound) + } +} + +func TestCheckAnalyzersHandlesSSABuildFailure(t *testing.T) { + t.Parallel() + + a := NewAnalyzer(NewConfig(), false, false, false, 1, log.New(io.Discard, "", 0)) + a.analyzerSet.Register(&analysis.Analyzer{Name: "dummy", Run: func(*analysis.Pass) (any, error) { return nil, nil }}, false) + + pkg := &packages.Package{Name: "broken"} + issues, stats := a.checkAnalyzers(pkg, nil) + + if len(issues) != 0 { + t.Fatalf("expected no issues when SSA build fails") + } + if stats == nil || stats.NumFound != 0 { + t.Fatalf("expected empty metrics, got %#v", stats) + } +} + +func TestCheckAnalyzersWithSSAWrapperMergesIssues(t *testing.T) { + t.Parallel() + + a := NewAnalyzer(NewConfig(), false, false, false, 1, log.New(io.Discard, "", 0)) + a.analyzerSet.Register(&analysis.Analyzer{ + Name: "dummy", + Run: func(*analysis.Pass) (any, error) { + return []*issue.Issue{{ + RuleID: "T999", + File: "dummy.go", + Line: "1", + Col: "1", + Severity: issue.High, + Confidence: issue.High, + What: "dummy finding", + }}, nil + }, + }, false) + + a.CheckAnalyzersWithSSA(&packages.Package{Name: "pkg"}, &buildssa.SSA{}) + issues, stats, _ := a.Report() + + if len(issues) != 1 { + t.Fatalf("unexpected issues count: got %d want 1", len(issues)) + } + if stats.NumFound != 1 { + t.Fatalf("unexpected findings count: got %d want 1", stats.NumFound) + } +} + +func TestBuildSSANilPackage(t *testing.T) { + t.Parallel() + + a := NewAnalyzer(NewConfig(), false, false, false, 1, log.New(io.Discard, "", 0)) + _, err := a.buildSSA(nil) + if err == nil { + t.Fatalf("expected error for nil package") + } + if !errors.Is(err, ErrNilPackage) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildSSATypeInfoValidation(t *testing.T) { + t.Parallel() + + a := NewAnalyzer(NewConfig(), false, false, false, 1, log.New(io.Discard, "", 0)) + + if _, err := a.buildSSA(&packages.Package{Name: "missing-types"}); err == nil { + t.Fatalf("expected error for missing types") + } + + pkgMissingInfo := &packages.Package{Name: "missing-typesinfo"} + pkgMissingInfo.Types = types.NewPackage("example.com/p", "p") + _, err := a.buildSSA(pkgMissingInfo) + if err == nil { + t.Fatalf("expected error for missing types info") + } +} diff --git a/goanalysis/analyzer_internal_test.go b/goanalysis/analyzer_internal_test.go new file mode 100644 index 0000000..a1ce03f --- /dev/null +++ b/goanalysis/analyzer_internal_test.go @@ -0,0 +1,177 @@ +// (c) Copyright gosec's authors +// +// 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 +// +// http://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 goanalysis + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/analysis" + + "github.com/securego/gosec/v2/issue" +) + +func TestBuildFilters(t *testing.T) { + t.Parallel() + + newFilter := func(exclude bool, ids ...string) string { + prefix := "include" + if exclude { + prefix = "exclude" + } + return prefix + ":" + ids[0] + } + + filters := buildFilters(" G101 , , G102 ", "G201", newFilter) + if len(filters) != 2 { + t.Fatalf("unexpected filter count: got %d want 2", len(filters)) + } + if filters[0] != "include:G101" { + t.Fatalf("unexpected include filter: %q", filters[0]) + } + if filters[1] != "exclude:G201" { + t.Fatalf("unexpected exclude filter: %q", filters[1]) + } +} + +func TestParseRuleIDs(t *testing.T) { + t.Parallel() + + ids := parseRuleIDs(" G101, ,G102,, G115 ") + if len(ids) != 3 { + t.Fatalf("unexpected ids count: got %d want 3", len(ids)) + } + if ids[0] != "G101" || ids[1] != "G102" || ids[2] != "G115" { + t.Fatalf("unexpected ids: %v", ids) + } +} + +func TestParseScore(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + want issue.Score + }{ + {in: "low", want: issue.Low}, + {in: "Medium", want: issue.Medium}, + {in: "HIGH", want: issue.High}, + } + + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + t.Parallel() + got, err := parseScore(tc.in) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Fatalf("unexpected score: got %v want %v", got, tc.want) + } + }) + } + + if _, err := parseScore("critical"); err == nil { + t.Fatalf("expected error for invalid score") + } +} + +func TestParsePosition(t *testing.T) { + t.Parallel() + + fset := token.NewFileSet() + src := "package p\n\nfunc main() {\n\tprintln(\"x\")\n}\n" + file, err := parser.ParseFile(fset, "/tmp/p.go", src, parser.ParseComments) + if err != nil { + t.Fatalf("failed to parse source: %v", err) + } + + t.Run("uses start line for ranges", func(t *testing.T) { + t.Parallel() + + iss := &issue.Issue{File: "/tmp/p.go", Line: "3-4", Col: "2"} + pos := parsePosition(fset, iss) + if pos == token.NoPos { + t.Fatalf("expected valid position") + } + p := fset.Position(pos) + if p.Line != 3 || p.Column != 2 { + t.Fatalf("unexpected position: line=%d col=%d", p.Line, p.Column) + } + }) + + t.Run("falls back to line start for invalid column", func(t *testing.T) { + t.Parallel() + + iss := &issue.Issue{File: "/tmp/p.go", Line: "3", Col: "bad"} + pos := parsePosition(fset, iss) + p := fset.Position(pos) + if p.Line != 3 || p.Column != 1 { + t.Fatalf("unexpected fallback position: line=%d col=%d", p.Line, p.Column) + } + }) + + t.Run("returns no position for unknown file", func(t *testing.T) { + t.Parallel() + + iss := &issue.Issue{File: "/tmp/unknown.go", Line: "1", Col: "1"} + if got := parsePosition(fset, iss); got != token.NoPos { + t.Fatalf("expected NoPos, got %v", got) + } + }) + + t.Run("returns no position for invalid line", func(t *testing.T) { + t.Parallel() + + iss := &issue.Issue{File: "/tmp/p.go", Line: "99", Col: "1"} + if got := parsePosition(fset, iss); got != token.NoPos { + t.Fatalf("expected NoPos, got %v", got) + } + }) + + _ = file +} + +func TestConvertPassToPackage(t *testing.T) { + t.Parallel() + + fset := token.NewFileSet() + src := "package p\n\nfunc main() {}\n" + astFile, err := parser.ParseFile(fset, "/tmp/main.go", src, 0) + if err != nil { + t.Fatalf("failed to parse source: %v", err) + } + + pass := &analysis.Pass{ + Fset: fset, + Files: []*ast.File{}, + Pkg: types.NewPackage("example.com/p", "p"), + } + pass.Files = append(pass.Files, astFile) + + pkg := convertPassToPackage(pass) + if pkg.Name != "p" { + t.Fatalf("unexpected package name: %q", pkg.Name) + } + if len(pkg.CompiledGoFiles) != 1 { + t.Fatalf("unexpected file count: %d", len(pkg.CompiledGoFiles)) + } + if pkg.CompiledGoFiles[0] != "/tmp/main.go" { + t.Fatalf("unexpected compiled file path: %q", pkg.CompiledGoFiles[0]) + } +} diff --git a/taint/analyzer_internal_test.go b/taint/analyzer_internal_test.go new file mode 100644 index 0000000..595f5f2 --- /dev/null +++ b/taint/analyzer_internal_test.go @@ -0,0 +1,110 @@ +package taint + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/buildssa" + + "github.com/securego/gosec/v2/internal/ssautil" + "github.com/securego/gosec/v2/issue" +) + +func TestMakeAnalyzerRunnerReturnsErrorWithoutSSA(t *testing.T) { + t.Parallel() + + rule := &RuleInfo{ID: "T001", Description: "desc", Severity: "HIGH"} + runner := makeAnalyzerRunner(rule, &Config{}) + + pass := &analysis.Pass{ResultOf: map[*analysis.Analyzer]interface{}{}} + if _, err := runner(pass); err == nil { + t.Fatalf("expected error when SSA result is missing") + } +} + +func TestMakeAnalyzerRunnerReturnsNilWhenNoSourceFunctions(t *testing.T) { + t.Parallel() + + rule := &RuleInfo{ID: "T001", Description: "desc", Severity: "HIGH"} + runner := makeAnalyzerRunner(rule, &Config{}) + + pass := &analysis.Pass{ + ResultOf: map[*analysis.Analyzer]interface{}{ + buildssa.Analyzer: &ssautil.SSAAnalyzerResult{SSA: &buildssa.SSA{}}, + }, + } + + got, err := runner(pass) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != nil { + t.Fatalf("expected nil result when no source functions exist") + } +} + +func TestNewIssuePopulatesFields(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "main.go") + src := "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n" + if err := os.WriteFile(filePath, []byte(src), 0o600); err != nil { + t.Fatalf("failed to write temp source: %v", err) + } + + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, filePath, src, 0) + if err != nil { + t.Fatalf("failed to parse source: %v", err) + } + + iss := newIssue("T001", "taint finding", fset, parsed.Package, issue.High, issue.High) + if iss.RuleID != "T001" { + t.Fatalf("unexpected rule id: %s", iss.RuleID) + } + if iss.File != filePath { + t.Fatalf("unexpected file path: %s", iss.File) + } + if iss.Line != "1" || iss.Col != "1" { + t.Fatalf("unexpected location: line=%s col=%s", iss.Line, iss.Col) + } + if iss.What != "taint finding" { + t.Fatalf("unexpected description: %s", iss.What) + } +} + +func TestIssueCodeSnippetReadsSource(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "snippet.go") + src := "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n" + if err := os.WriteFile(filePath, []byte(src), 0o600); err != nil { + t.Fatalf("failed to write temp source: %v", err) + } + + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, filePath, src, 0) + if err != nil { + t.Fatalf("failed to parse source: %v", err) + } + + snippet := issueCodeSnippet(fset, parsed.Package) + if snippet == "" { + t.Fatalf("expected non-empty snippet") + } +} + +func TestNewIssueReturnsEmptyWhenPositionCannotBeResolved(t *testing.T) { + t.Parallel() + + iss := newIssue("T001", "desc", token.NewFileSet(), token.NoPos, issue.High, issue.High) + if iss.RuleID != "" || iss.File != "" { + t.Fatalf("expected empty issue for unresolved position, got %+v", iss) + } +}