vuln-list-update/alpine/alpine_test.go
Teppei Fukuda 95bdc43aeb
fix(alpine): handle security fixes (#35)
* test(alpine): add a failing test

* fix(alpine): handle security fixes
2020-06-29 20:54:35 +03:00

397 lines
13 KiB
Go

package alpine_test
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"testing"
"golang.org/x/xerrors"
"github.com/aquasecurity/vuln-list-update/alpine"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockGitConfig struct {
mock.Mock
}
func (mgc *MockGitConfig) CloneOrPull(a string, b string) (map[string]struct{}, error) {
args := mgc.Called(a, b)
return args.Get(0).(map[string]struct{}), args.Error(1)
}
func (mgc *MockGitConfig) RemoteBranch(a string) ([]string, error) {
args := mgc.Called(a)
return args.Get(0).([]string), args.Error(1)
}
func (mgc *MockGitConfig) Checkout(a string, b string) error {
args := mgc.Called(a, b)
return args.Error(0)
}
func TestParsePkgVerRel(t *testing.T) {
vectors := []struct {
file string // Test input file
pkgVer string
pkgRel string
secFixes map[string][]string
}{
{
file: "testdata/aports/main/freeradius/APKBUILD",
pkgVer: "3.0.19",
pkgRel: "0",
},
{
file: "testdata/aports/main/wireshark/APKBUILD",
pkgVer: "2.6.8",
pkgRel: "1",
},
}
for _, v := range vectors {
t.Run(path.Base(v.file), func(t *testing.T) {
content, err := ioutil.ReadFile(v.file)
if err != nil {
t.Fatalf("ReadAll() error: %v", err)
}
pkgVer, pkgRel, err := alpine.ParsePkgVerRel(&alpine.Config{}, string(content))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pkgVer != v.pkgVer {
t.Errorf("pkgVer: got %s, want %s", pkgVer, v.pkgVer)
}
if pkgRel != v.pkgRel {
t.Errorf("pkgRel: got %s, want %s", pkgRel, v.pkgRel)
}
})
}
}
func TestParseSecFixes(t *testing.T) {
vectors := []struct {
file string // Test input file
pkgVer string
pkgRel string
secFixes map[string][]string
}{
{
file: "testdata/aports/main/freeradius/APKBUILD",
pkgVer: "3.0.19",
pkgRel: "0",
secFixes: map[string][]string{
"3.0.19-r0": {"CVE-2019-11234", "CVE-2019-11235"},
},
},
{
file: "testdata/aports/main/wireshark/APKBUILD",
pkgVer: "2.6.8",
pkgRel: "1",
secFixes: map[string][]string{
"2.6.8-r0": {"CVE-2019-10894", "CVE-2019-10895", "CVE-2019-10896", "CVE-2019-10899", "CVE-2019-10901", "CVE-2019-10903"},
"2.6.7-r0": {"CVE-2019-9208", "CVE-2019-9209", "CVE-2019-9214"},
"2.6.6-r0": {"CVE-2019-5717", "CVE-2019-5718", "CVE-2019-5719", "CVE-2019-5721"},
},
},
{
file: "testdata/aports/main/libssh2/APKBUILD",
pkgVer: "1.9.0",
pkgRel: "1",
secFixes: map[string][]string{
"1.9.0-r1": {"CVE-2019-17498"},
"1.9.0-r0": {"CVE-2019-13115"},
},
},
}
for _, v := range vectors {
t.Run(path.Base(v.file), func(t *testing.T) {
content, err := ioutil.ReadFile(v.file)
if err != nil {
t.Fatalf("ReadAll() error: %v", err)
}
secFixes, err := alpine.ParseSecFixes(&alpine.Config{}, string(content))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(secFixes, v.secFixes) {
t.Errorf("secFixes: got %v, want %v", secFixes, v.secFixes)
}
})
}
}
func TestShouldOverwrite(t *testing.T) {
testCases := []struct {
name string
currentVersion string
issuedAdvisory interface{}
expctedOverwrite bool
}{
{
name: "issued advisory should overwrite existing one with valid version",
currentVersion: "1.0.0",
issuedAdvisory: alpine.Advisory{
IssueID: 0,
VulnerabilityID: "CVE-2100-0001",
Release: "1.0",
Package: "testpackage",
Repository: "main",
FixedVersion: "1.2.0",
Description: "for testing only",
},
expctedOverwrite: true,
},
{
name: "issued advisory should NOT overwrite existing one with valid version",
currentVersion: "1.0.0",
issuedAdvisory: alpine.Advisory{
IssueID: 0,
VulnerabilityID: "CVE-2100-0001",
Release: "1.0",
Package: "testpackage",
Repository: "main",
FixedVersion: "0.9.0",
Description: "for testing only",
},
expctedOverwrite: false,
},
{
name: "invalid advisory json",
currentVersion: "1.0.0",
issuedAdvisory: []byte(`badjsonhere`),
expctedOverwrite: true,
},
{
name: "empty fixed version",
currentVersion: "1.0.0",
issuedAdvisory: alpine.Advisory{
Subject: "non empty subject",
},
expctedOverwrite: true,
},
{
name: "invalid old advisory version",
currentVersion: "1.0.0",
issuedAdvisory: alpine.Advisory{
Subject: "non empty subject",
Package: "test",
FixedVersion: "invalid",
},
expctedOverwrite: false,
},
{
name: "invalid current advisory version",
currentVersion: "invalid",
issuedAdvisory: alpine.Advisory{
Subject: "non empty subject",
Package: "test",
FixedVersion: "1.0.0",
},
expctedOverwrite: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f, _ := ioutil.TempFile("", "TestShouldOverwrite_happy_sad")
defer os.Remove(f.Name())
b, _ := json.Marshal(tc.issuedAdvisory)
_, _ = f.Write(b)
assert.Equal(t, tc.expctedOverwrite, alpine.ShouldOverwrite(&alpine.Config{}, f.Name(), tc.currentVersion), tc.name)
assert.NoError(t, f.Close())
})
}
}
func TestWalkApkBuild(t *testing.T) {
advisories, err := alpine.WalkApkBuild(&alpine.Config{}, "testdata/aports", "1.0.0")
assert.NoError(t, err)
assert.ElementsMatch(t, []alpine.Advisory{
{FixedVersion: "1.2.15-r11", VulnerabilityID: "CVE-2019-7572", Release: "1.0.0", Package: "sdl", Repository: "main"},
{FixedVersion: "1.2.15-r11", VulnerabilityID: "CVE-2019-7574", Release: "1.0.0", Package: "sdl", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10894", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10895", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10896", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10899", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10901", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.8-r0", VulnerabilityID: "CVE-2019-10903", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.7-r0", VulnerabilityID: "CVE-2019-9208", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.7-r0", VulnerabilityID: "CVE-2019-9209", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.7-r0", VulnerabilityID: "CVE-2019-9214", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.6-r0", VulnerabilityID: "CVE-2019-5717", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.6-r0", VulnerabilityID: "CVE-2019-5718", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.6-r0", VulnerabilityID: "CVE-2019-5719", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "2.6.6-r0", VulnerabilityID: "CVE-2019-5721", Release: "1.0.0", Package: "wireshark", Repository: "main"},
{FixedVersion: "3.0.19-r0", VulnerabilityID: "CVE-2019-11234", Release: "1.0.0", Package: "freeradius", Repository: "main"},
{FixedVersion: "3.0.19-r0", VulnerabilityID: "CVE-2019-11235", Release: "1.0.0", Package: "freeradius", Repository: "main"},
{FixedVersion: "1.9.0-r0", VulnerabilityID: "CVE-2019-13115", Release: "1.0.0", Package: "libssh2", Repository: "main"},
{FixedVersion: "1.9.0-r1", VulnerabilityID: "CVE-2019-17498", Release: "1.0.0", Package: "libssh2", Repository: "main"},
{FixedVersion: "1.7.3-r0", VulnerabilityID: "CVE-2019-9917", Release: "1.0.0", Package: "znc", Repository: "community"},
{FixedVersion: "1.7.1-r0", VulnerabilityID: "CVE-2018-14055", Release: "1.0.0", Package: "znc", Repository: "community"},
{FixedVersion: "1.7.1-r0", VulnerabilityID: "CVE-2018-14056", Release: "1.0.0", Package: "znc", Repository: "community"},
},
advisories)
}
func TestBuildAdvisories(t *testing.T) {
secFixes := map[string][]string{
"2.6.8-r0": {"CVE-2019-10894"},
"2.6.7-r1": {"CVE_2019-2426 XSA-201"}, // typo
"2.6.5-r0": {"CVE_2019-5910 (+ some extra in parens)"},
}
assert.ElementsMatch(t, []alpine.Advisory{
{IssueID: 0, VulnerabilityID: "CVE-2019-10894", Release: "1.0.0", Package: "testpkg", Repository: "testrepo", FixedVersion: "2.6.8-r0", Subject: "", Description: ""},
{IssueID: 0, VulnerabilityID: "CVE-2019-2426", Release: "1.0.0", Package: "testpkg", Repository: "testrepo", FixedVersion: "2.6.7-r1", Subject: "", Description: ""},
{IssueID: 0, VulnerabilityID: "XSA-201", Release: "1.0.0", Package: "testpkg", Repository: "testrepo", FixedVersion: "2.6.7-r1", Subject: "", Description: ""},
{IssueID: 0, VulnerabilityID: "CVE-2019-5910", Release: "1.0.0", Package: "testpkg", Repository: "testrepo", FixedVersion: "2.6.5-r0", Subject: "", Description: ""}},
alpine.BuildAdvisories(&alpine.Config{}, secFixes, "1.0.0", "testpkg", "testrepo"))
}
func TestConfig_Update(t *testing.T) {
type cloneOrPull struct {
returnArg map[string]struct{}
err error
}
type remoteBranch struct {
returnArg []string
err error
}
testCases := []struct {
name string
remoteBranch remoteBranch // mock value
cloneOrPull cloneOrPull // mock value
checkout map[string]error // mock value
wantErr error
}{
{
name: "happy path",
remoteBranch: remoteBranch{
returnArg: []string{"origin/branch1-stable", "origin/branch2", "origin/branch3"},
},
checkout: map[string]error{mock.Anything: nil},
wantErr: nil,
},
{
name: "invalid branch name",
remoteBranch: remoteBranch{returnArg: []string{"badbranch-stable"}},
checkout: map[string]error{mock.Anything: nil},
wantErr: nil,
},
{
name: "git fails to show remote branches",
remoteBranch: remoteBranch{
returnArg: nil, err: errors.New("failed to show remote branch"),
},
checkout: map[string]error{mock.Anything: nil},
wantErr: xerrors.Errorf("failed to show branches: %w", errors.New("failed to show remote branch")),
},
{
name: "git clone fails",
cloneOrPull: cloneOrPull{
returnArg: nil, err: errors.New("failed clone operation"),
},
checkout: map[string]error{mock.Anything: nil},
wantErr: xerrors.Errorf("failed to clone alpine repository: %w", errors.New("failed clone operation")),
},
{
name: "git fails to checkout branch",
remoteBranch: remoteBranch{
returnArg: []string{"origin/branch1-stable", "origin/branch2", "origin/branch3"},
},
checkout: map[string]error{mock.Anything: errors.New("failed to checkout branch")},
wantErr: xerrors.Errorf("git failed to checkout branch: %w", errors.New("failed to checkout branch")),
},
{
name: "git checkout of a particular branch fails",
remoteBranch: remoteBranch{
returnArg: []string{"origin/branch1-stable", "origin/branch2", "origin/branch3"},
},
checkout: map[string]error{
"master": errors.New("failed to checkout master"),
"origin/branch1-stable": errors.New("failed to checkout branch1-stable"),
},
wantErr: xerrors.Errorf("git failed to checkout branch: %w", errors.New("failed to checkout branch1-stable")),
},
}
cacheDir := "testdata"
repoDir := filepath.Join(cacheDir, "aports")
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
vulnListDir, err := ioutil.TempDir("", "TestUpdate")
assert.NoError(t, err)
defer os.RemoveAll(vulnListDir)
mockGitConfig := new(MockGitConfig)
// setup expectations with a placeholder in the argument list
mockGitConfig.On("RemoteBranch", repoDir).Return(
tc.remoteBranch.returnArg, tc.remoteBranch.err)
mockGitConfig.On("CloneOrPull", mock.Anything, repoDir).Return(
tc.cloneOrPull.returnArg, tc.cloneOrPull.err)
for arg, returnErr := range tc.checkout {
mockGitConfig.On("Checkout", repoDir, arg).Return(returnErr)
}
ac := alpine.Config{
GitClient: mockGitConfig,
CacheDir: cacheDir,
VulnListDir: vulnListDir,
}
fmt.Println(vulnListDir)
err = ac.Update()
if tc.wantErr != nil {
assert.EqualError(t, err, tc.wantErr.Error())
} else {
assert.NoError(t, err)
err = filepath.Walk(vulnListDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
paths := strings.Split(path, string(os.PathSeparator))
assert.True(t, len(paths) > 3)
golden := filepath.Join("testdata", "goldens",
paths[len(paths)-3], paths[len(paths)-2], paths[len(paths)-1],
)
got, _ := ioutil.ReadFile(path)
want, _ := ioutil.ReadFile(golden + ".golden")
assert.Equal(t, string(want), string(got), "Alpine result json")
return nil
})
assert.NoError(t, err)
}
})
}
}