go-devops/devkit/vulncheck_test.go
Snider e20083d51b feat(devkit): Phase 4 — vulnerability scanning, complexity analysis, coverage trending
- govulncheck JSON output parsing with structured VulnFinding types (13 tests)
- Cyclomatic complexity analysis via go/ast with configurable threshold (21 tests)
- Coverage snapshot persistence and regression detection with CoverageStore (19 tests)

53 new tests, 68 total devkit tests. All pass with -race.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 06:28:37 +00:00

189 lines
7.6 KiB
Go

// LEK-1 | lthn.ai | EUPL-1.2
package devkit
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const sampleVulnJSON = `{"config":{"module_path":"example.com/mymod","go_version":"go1.22.0"}}
{"progress":{"message":"Scanning your code..."}}
{"osv":{"id":"GO-2024-0001","aliases":["CVE-2024-1234","GHSA-abcd-1234"],"summary":"Buffer overflow in net/http","affected":[{"package":{"name":"stdlib","ecosystem":"Go"},"ranges":[{"events":[{"fixed":"1.22.1"}]}]}]}}
{"osv":{"id":"GO-2024-0002","aliases":["CVE-2024-5678"],"summary":"Path traversal in archive/zip","affected":[{"package":{"name":"stdlib","ecosystem":"Go"},"ranges":[{"events":[{"fixed":"1.21.9"}]}]}]}}
{"finding":{"osv":"GO-2024-0001","trace":[{"module":"example.com/mymod","package":"example.com/mymod/server","function":"HandleRequest"},{"module":"stdlib","package":"net/http","function":"ReadRequest","version":"go1.22.0"}]}}
{"finding":{"osv":"GO-2024-0002","trace":[{"module":"stdlib","package":"archive/zip","function":"OpenReader","version":"go1.22.0"}]}}
`
func TestParseVulnCheckJSON_Good(t *testing.T) {
result, err := ParseVulnCheckJSON(sampleVulnJSON, "")
require.NoError(t, err)
assert.Equal(t, "example.com/mymod", result.Module)
assert.Len(t, result.Findings, 2)
// First finding: GO-2024-0001
f0 := result.Findings[0]
assert.Equal(t, "GO-2024-0001", f0.ID)
assert.Equal(t, "net/http", f0.Package)
assert.Equal(t, "ReadRequest", f0.CalledFunction)
assert.Equal(t, "Buffer overflow in net/http", f0.Description)
assert.Contains(t, f0.Aliases, "CVE-2024-1234")
assert.Contains(t, f0.Aliases, "GHSA-abcd-1234")
assert.Equal(t, "go1.22.0", f0.FixedVersion) // from trace version
// Second finding: GO-2024-0002
f1 := result.Findings[1]
assert.Equal(t, "GO-2024-0002", f1.ID)
assert.Equal(t, "archive/zip", f1.Package)
assert.Equal(t, "OpenReader", f1.CalledFunction)
assert.Equal(t, "Path traversal in archive/zip", f1.Description)
assert.Contains(t, f1.Aliases, "CVE-2024-5678")
}
func TestParseVulnCheckJSON_EmptyOutput_Good(t *testing.T) {
result, err := ParseVulnCheckJSON("", "")
require.NoError(t, err)
assert.Empty(t, result.Findings)
assert.Empty(t, result.Module)
}
func TestParseVulnCheckJSON_ConfigOnly_Good(t *testing.T) {
input := `{"config":{"module_path":"example.com/clean","go_version":"go1.23.0"}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Equal(t, "example.com/clean", result.Module)
assert.Empty(t, result.Findings)
}
func TestParseVulnCheckJSON_MalformedLines_Bad(t *testing.T) {
input := `not valid json
{"config":{"module_path":"example.com/mod"}}
also broken {{{
{"osv":{"id":"GO-2024-0099","summary":"Test vuln","aliases":[],"affected":[]}}
{"finding":{"osv":"GO-2024-0099","trace":[{"module":"stdlib","package":"crypto/tls","function":"Dial"}]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Equal(t, "example.com/mod", result.Module)
assert.Len(t, result.Findings, 1)
assert.Equal(t, "GO-2024-0099", result.Findings[0].ID)
assert.Equal(t, "Dial", result.Findings[0].CalledFunction)
}
func TestParseVulnCheckJSON_FindingWithoutOSV_Bad(t *testing.T) {
// Finding references an OSV ID that was never emitted — should still parse.
input := `{"finding":{"osv":"GO-2024-UNKNOWN","trace":[{"module":"example.com/mod","package":"example.com/mod/pkg","function":"DoStuff"}]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Len(t, result.Findings, 1)
f := result.Findings[0]
assert.Equal(t, "GO-2024-UNKNOWN", f.ID)
assert.Equal(t, "example.com/mod/pkg", f.Package)
assert.Equal(t, "DoStuff", f.CalledFunction)
assert.Empty(t, f.Description) // No OSV entry to enrich from
assert.Empty(t, f.Aliases)
}
func TestParseVulnCheckJSON_NoTrace_Bad(t *testing.T) {
input := `{"osv":{"id":"GO-2024-0050","summary":"Empty trace test","aliases":["CVE-2024-0050"],"affected":[]}}
{"finding":{"osv":"GO-2024-0050","trace":[]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Len(t, result.Findings, 1)
f := result.Findings[0]
assert.Equal(t, "GO-2024-0050", f.ID)
assert.Equal(t, "Empty trace test", f.Description)
assert.Empty(t, f.Package)
assert.Empty(t, f.CalledFunction)
}
func TestParseVulnCheckJSON_MultipleFindings_Good(t *testing.T) {
input := `{"osv":{"id":"GO-2024-0010","summary":"Vuln A","aliases":["CVE-A"],"affected":[{"ranges":[{"events":[{"fixed":"1.20.5"}]}]}]}}
{"osv":{"id":"GO-2024-0011","summary":"Vuln B","aliases":["CVE-B"],"affected":[]}}
{"osv":{"id":"GO-2024-0012","summary":"Vuln C","aliases":["CVE-C"],"affected":[{"ranges":[{"events":[{"fixed":"1.21.0"}]}]}]}}
{"finding":{"osv":"GO-2024-0010","trace":[{"package":"net/http","function":"Serve"}]}}
{"finding":{"osv":"GO-2024-0011","trace":[{"package":"encoding/xml","function":"Unmarshal"}]}}
{"finding":{"osv":"GO-2024-0012","trace":[{"package":"os/exec","function":"Command"}]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Len(t, result.Findings, 3)
assert.Equal(t, "Vuln A", result.Findings[0].Description)
assert.Equal(t, "1.20.5", result.Findings[0].FixedVersion)
assert.Equal(t, "Vuln B", result.Findings[1].Description)
assert.Equal(t, "Vuln C", result.Findings[2].Description)
assert.Equal(t, "1.21.0", result.Findings[2].FixedVersion)
}
func TestParseVulnCheckJSON_FixedVersionFromOSV_Good(t *testing.T) {
// When trace has no version, fixed version should come from OSV affected ranges.
input := `{"osv":{"id":"GO-2024-0077","summary":"Test","aliases":[],"affected":[{"ranges":[{"events":[{"fixed":"0.9.1"}]}]}]}}
{"finding":{"osv":"GO-2024-0077","trace":[{"package":"example.com/lib","function":"Process"}]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
require.Len(t, result.Findings, 1)
assert.Equal(t, "0.9.1", result.Findings[0].FixedVersion)
}
func TestVulnCheck_NotInstalled_Ugly(t *testing.T) {
setupMockCmdExit(t, "govulncheck-nonexistent", "", "", 1)
// Don't mock govulncheck — ensure it handles missing binary gracefully
// We'll rely on the binary not being in the test temp PATH.
tk := New(t.TempDir())
// Remove PATH to simulate govulncheck not found
t.Setenv("PATH", t.TempDir())
_, err := tk.VulnCheck("./...")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not installed or not available")
}
func TestVulnCheck_WithMock_Good(t *testing.T) {
// Mock govulncheck to return our sample JSON
setupMockCmd(t, "govulncheck", sampleVulnJSON)
tk := New(t.TempDir())
result, err := tk.VulnCheck("./...")
require.NoError(t, err)
assert.Equal(t, "example.com/mymod", result.Module)
assert.Len(t, result.Findings, 2)
}
func TestVulnCheck_DefaultModulePath_Good(t *testing.T) {
setupMockCmd(t, "govulncheck", `{"config":{"module_path":"default/mod"}}`)
tk := New(t.TempDir())
result, err := tk.VulnCheck("")
require.NoError(t, err)
assert.Equal(t, "default/mod", result.Module)
}
func TestParseVulnCheckJSON_ProgressOnly_Good(t *testing.T) {
input := `{"progress":{"message":"Scanning..."}}
{"progress":{"message":"Done"}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
assert.Empty(t, result.Findings)
}
func TestParseVulnCheckJSON_ModulePathFromTrace_Good(t *testing.T) {
input := `{"finding":{"osv":"GO-2024-0099","trace":[{"module":"example.com/vulnerable","package":"example.com/vulnerable/pkg","function":"Bad","version":"v1.2.3"}]}}
`
result, err := ParseVulnCheckJSON(input, "")
require.NoError(t, err)
require.Len(t, result.Findings, 1)
assert.Equal(t, "example.com/vulnerable", result.Findings[0].ModulePath)
assert.Equal(t, "v1.2.3", result.Findings[0].FixedVersion)
}
// LEK-1 | lthn.ai | EUPL-1.2