Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion config/tomlloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,16 @@ func setDefaultIfEmpty(server *ServerInfo) error {
return nil
}

func toCpeURI(cpename string) (string, error) {
func toCpeURI(cpename string) (uri string, err error) {
// go-cpe's BindToURI / UnbindURI panic on some malformed inputs (e.g.
// embedded backslash-escapes); convert that into an error so a single
// bad CPE in user config can never bring the process down.
defer func() {
if r := recover(); r != nil {
uri = ""
err = xerrors.Errorf("Failed to parse CPE %q: %v", cpename, r)
}
}()
if strings.HasPrefix(cpename, "cpe:2.3:") {
wfn, err := naming.UnbindFS(cpename)
if err != nil {
Expand Down
40 changes: 40 additions & 0 deletions config/tomlloader_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config

import (
"strings"
"testing"
)

// FuzzToCpeURI drives random CPE strings through toCpeURI. Invariants:
// - never panic,
// - if err == nil, the returned URI starts with "cpe:/" (CPE 2.2 form).
func FuzzToCpeURI(f *testing.F) {
seeds := []string{
`cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*`,
`cpe:2.3:o:linux:linux_kernel:5.10:*:*:*:*:*:*:*`,
`cpe:/a:vendor:product:1.0`,
`cpe:/o:linux:linux_kernel:5.10`,
`cpe:/`,
`cpe:2.3:`,
`cpe:2.3`,
`cpe:2.3:a:`,
`cpe:/a:`,
``,
`not-a-cpe`,
`cpe:2.3:a:v:p:1\:2:*:*:*:*:*:*:*`,
`cpe:/a:vendor:product:1\:2`,
}
for _, s := range seeds {
f.Add(s)
}

f.Fuzz(func(t *testing.T, cpename string) {
got, err := toCpeURI(cpename)
if err != nil {
return
}
if !strings.HasPrefix(got, "cpe:/") {
t.Fatalf("toCpeURI(%q) = %q: missing cpe:/ prefix", cpename, got)
}
})
}
6 changes: 5 additions & 1 deletion contrib/snmp2cpe/pkg/cpe/cpe.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ func Convert(result snmp.Result) []string {
} else {
h, v, ok := strings.Cut(result.SysDescr0, " version ")
if ok {
vfields := strings.Fields(v)
if len(vfields) == 0 {
break
}
Comment on lines +71 to +74
cpes = append(cpes,
fmt.Sprintf("cpe:2.3:h:juniper:%s:-:*:*:*:*:*:*:*", strings.ToLower(h)),
fmt.Sprintf("cpe:2.3:o:juniper:screenos:%s:*:*:*:*:*:*:*", strings.ToLower(strings.Fields(v)[0])),
fmt.Sprintf("cpe:2.3:o:juniper:screenos:%s:*:*:*:*:*:*:*", strings.ToLower(vfields[0])),
)
}
}
Expand Down
52 changes: 52 additions & 0 deletions contrib/snmp2cpe/pkg/cpe/cpe_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cpe

import (
"testing"

"github.com/future-architect/vuls/contrib/snmp2cpe/pkg/snmp"
)

// FuzzConvert drives random snmp.Result values through Convert. Strings are
// fed for SysDescr0, EntPhysicalMfgName, EntPhysicalName and
// EntPhysicalSoftwareRev. Invariant: never panic.
func FuzzConvert(f *testing.F) {
seeds := []struct{ desc, mfg, name, rev string }{
{"Cisco IOS Software, Version 12.4", "Cisco", "C2960", "12.4"},
{"Cisco NX-OS, Version 7.0", "Cisco", "Nexus", "7.0"},
{"Juniper Networks, Inc. mx240, kernel JUNOS 15.1F6", "Juniper Networks", "MX240", "15.1F6"},
{"Arista Networks EOS version 4.20.7M running on an Arista Networks DCS-7050S", "Arista Networks", "DCS-7050S", "4.20.7M"},
{"FortiGate-60E v5.6.4,build1575", "Fortinet", "FGT_60E", "FortiGate-60E v5.6.4,build1575"},
{"YAMAHA RTX1210 Rev.14.01.42", "YAMAHA", "RTX1210", "14.01.42"},
{"NEC IX Series IX2215 (magellan-sec) Software, Version 10.10.10", "NEC", "IX2215", "10.10.10"},
{"Palo Alto Networks PA-220", "Palo Alto Networks", "PA-220", "10.0.0"},
{"", "", "", ""},
{"Cisco", "", "", ""},
{"Cisco IOS Software, Version", "", "", ""},
{"Juniper Networks, Inc. ", "", "", ""},
{"Arista Networks EOS version running on an ", "", "", ""},
{"Arista Networks EOS version x running on an ", "", "", ""},
{"FortiGate version", "", "", ""},
{"YAMAHA RTX", "", "", ""},
{"NEC", "", "", ""},
{"foo version ", "Juniper Networks", "", ""},
{"abc kernel JUNOS ", "Juniper Networks", "", ""},
{"Juniper Networks, Inc. qfx", "Juniper Networks", "", ""},
}
for _, s := range seeds {
f.Add(s.desc, s.mfg, s.name, s.rev)
}

f.Fuzz(func(_ *testing.T, desc, mfg, ename, rev string) {
r := snmp.Result{
SysDescr0: desc,
EntPhysicalTables: map[int]snmp.EntPhysicalTable{
1: {
EntPhysicalMfgName: mfg,
EntPhysicalName: ename,
EntPhysicalSoftwareRev: rev,
},
},
}
_ = Convert(r)
})
}
55 changes: 55 additions & 0 deletions detector/library_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build !scanner

package detector

import (
"strings"
"testing"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)

// FuzzCanonicalMavenPURL drives random "groupId:artifactId" / version pairs
// through canonicalMavenPURL. It enforces:
// - the function never panics,
// - the empty-input guard ("" group, "" artifact, no ':') always returns "",
// - any non-empty result is a syntactically plausible Maven purl.
func FuzzCanonicalMavenPURL(f *testing.F) {
seeds := []struct{ name, version string }{
{"org.apache.logging.log4j:log4j-core", "2.14.1"},
{"org.springframework.boot:spring-boot-starter-jdbc", "2.5.0"},
{"antlr:antlr", "2.7.7"},
{"com.example:example", "1.0+build.1"},
{"log4j-core", "2.14.1"},
{":log4j-core", "2.14.1"},
{"org.apache.logging.log4j:", "2.14.1"},
{"", "2.14.1"},
{"a:b", ""},
{"a:b:c", "1"},
{"a/b:c", "1.0"},
{"a:b@c", "1.0"},
}
for _, s := range seeds {
f.Add(s.name, s.version)
}

f.Fuzz(func(t *testing.T, name, version string) {
got := canonicalMavenPURL(ftypes.Package{Name: name, Version: version})

group, artifact, hasColon := strings.Cut(name, ":")
if !hasColon || group == "" || artifact == "" {
if got != "" {
t.Fatalf("expected empty for non-canonical name %q, got %q", name, got)
}
return
}

if got == "" {
// purl.New rejected the input — acceptable, no further checks.
return
}
if !strings.HasPrefix(got, "pkg:maven/") {
t.Fatalf("canonicalMavenPURL(%q,%q) = %q: missing pkg:maven/ prefix", name, version, got)
}
})
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ module github.com/future-architect/vuls

go 1.26

// stats/opentelemetry was absorbed into google.golang.org/grpc itself; the
// standalone module is still pulled in transitively (helm.sh/helm/v3 →
// distribution/distribution/v3@v3.0.0) and fails the build with an
// "ambiguous import" error. Exclude the standalone module so the absorbed
// sub-package is the only one resolved.
exclude google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a

require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/BurntSushi/toml v1.6.0
Expand Down
44 changes: 44 additions & 0 deletions gost/redhat_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package gost

import (
"strings"
"testing"
)

// FuzzParseCwe drives random RedHat-CWE-encoded strings through
// (RedHat).parseCwe. Invariants:
// - never panic,
// - no entry contains the splitter characters '(', ')' or "->",
// - no empty entry.
func FuzzParseCwe(f *testing.F) {
seeds := []string{
"CWE-79",
"CWE-79->CWE-89",
"(CWE-79)",
"(CWE-79->CWE-89)",
"CWE-79->(CWE-89)",
"",
"->",
"()",
"((()))",
"->->->",
"a(b)c->d",
}
for _, s := range seeds {
f.Add(s)
}

f.Fuzz(func(t *testing.T, str string) {
got := RedHat{}.parseCwe(str)
for _, c := range got {
if c == "" {
t.Fatalf("parseCwe(%q) emitted empty entry: %v", str, got)
}
for _, splitter := range []string{"(", ")", "->"} {
if strings.Contains(c, splitter) {
t.Fatalf("parseCwe(%q) entry %q still contains splitter %q", str, c, splitter)
}
}
}
})
}
57 changes: 57 additions & 0 deletions scanner/alpine_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package scanner

import (
"testing"

"github.com/future-architect/vuls/config"
)

// FuzzParseApkIndex drives random APKINDEX-like blocks through
// (alpine).parseApkIndex. Invariants:
// - never panic,
// - if err == nil, every entry in the returned map has matching key/Name
// and a non-empty Version.
func FuzzParseApkIndex(f *testing.F) {
seeds := []string{
"P:alpine-baselayout\nV:3.6.5-r0\nA:x86_64\no:alpine-baselayout\n",
"P:foo\nV:1.0\nA:noarch\n\nP:bar\nV:2.0\no:foo\n",
"P:foo\nV:1.0\n",
"",
"\n",
"\n\n",
"P:\nV:\n",
"P:foo\n",
"V:1.0\n",
"only-no-colon-line\n",
":\n",
"P:a\nV:b\nP:c\nV:d\n",
"P:a\nV:b\n\nP:c\nV:d\n",
}
for _, s := range seeds {
f.Add(s)
}

f.Fuzz(func(t *testing.T, stdout string) {
o := newAlpine(config.ServerInfo{})
bins, srcs, err := o.parseApkIndex(stdout)
if err != nil {
return
}
for key, pkg := range bins {
if pkg.Name != key {
t.Fatalf("parseApkIndex(%q): bin map key %q != Name %q", stdout, key, pkg.Name)
}
if pkg.Version == "" {
t.Fatalf("parseApkIndex(%q): bin %q has empty Version", stdout, key)
}
}
for key, sp := range srcs {
if sp.Name != key {
t.Fatalf("parseApkIndex(%q): src map key %q != Name %q", stdout, key, sp.Name)
}
if sp.Version == "" {
t.Fatalf("parseApkIndex(%q): src %q has empty Version", stdout, key)
}
}
})
}
48 changes: 48 additions & 0 deletions scanner/debian_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package scanner

import (
"strings"
"testing"
)

// FuzzParseScannedPackagesLine drives random comma-separated lines through
// (debian).parseScannedPackagesLine. Invariants:
// - never panic,
// - if err == nil, name is non-empty and matches the first comma-separated
// field with any ":arch" suffix stripped.
func FuzzParseScannedPackagesLine(f *testing.F) {
seeds := []string{
"linux-base,ii ,4.9,linux-base,4.9",
"linux-libc-dev:amd64,ii ,6.1.90-1,linux,6.1.90-1",
"linux-image-6.1.0-18-amd64,ii ,6.1.76-1,linux-signed-amd64,6.1.76+1",
"package,ii , , , ",
",,,,",
"a,b,c,d,e",
"a:x,b,c,d,e",
"a:x:y,b,c,d,e",
"",
"only,three,fields",
"a,b,c,d,e,f",
"name:arch,status,version,srcname srcver,srcversion",
}
for _, s := range seeds {
f.Add(s)
}

f.Fuzz(func(t *testing.T, line string) {
o := &debian{}
name, _, _, _, _, err := o.parseScannedPackagesLine(line)
if err != nil {
return
}

ss := strings.Split(line, ",")
expected := ss[0]
if i := strings.IndexRune(expected, ':'); i >= 0 {
expected = expected[:i]
}
if name != expected {
t.Fatalf("parseScannedPackagesLine(%q): name=%q, want %q", line, name, expected)
}
})
}
19 changes: 16 additions & 3 deletions scanner/freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ func (o *bsd) parsePkgVersion(stdout string) models.Packages {
Version: ver,
}
case "<":
// Expected format: "<name>-<ver> < needs updating (index has <new>)"
// — 7 whitespace-separated fields. A truncated line that survives
// the earlier len < 2 guard would otherwise panic on fields[6].
if len(fields) < 7 {
continue
}
candidate := strings.TrimSuffix(fields[6], ")")
packs[name] = models.Package{
Name: name,
Expand Down Expand Up @@ -331,11 +337,18 @@ func (o *bsd) parseBlock(block string) (packName string, cveIDs []string, vulnID
lines := strings.SplitSeq(block, "\n")
for l := range lines {
if strings.HasSuffix(l, " is vulnerable:") {
packVer := strings.Fields(l)[0]
splitted := strings.Split(packVer, "-")
fields := strings.Fields(l)
if len(fields) == 0 {
continue
}
splitted := strings.Split(fields[0], "-")
packName = strings.Join(splitted[:len(splitted)-1], "-")
} else if strings.HasPrefix(l, "CVE:") {
cveIDs = append(cveIDs, strings.Fields(l)[1])
fields := strings.Fields(l)
if len(fields) < 2 {
continue
}
cveIDs = append(cveIDs, fields[1])
} else if strings.HasPrefix(l, "WWW:") {
splitted := strings.Split(l, "/")
vulnID = strings.TrimSuffix(splitted[len(splitted)-1], ".html")
Expand Down
Loading
Loading