Change ScanResult.Packages structure to Map
This commit is contained in:
4
cache/bolt_test.go
vendored
4
cache/bolt_test.go
vendored
@@ -37,8 +37,8 @@ var meta = Meta{
|
||||
Family: "ubuntu",
|
||||
Release: "16.04",
|
||||
},
|
||||
Packs: []models.Package{
|
||||
{
|
||||
Packs: models.Packages{
|
||||
"apt": {
|
||||
Name: "apt",
|
||||
Version: "1",
|
||||
},
|
||||
|
||||
12
cache/db.go
vendored
12
cache/db.go
vendored
@@ -45,16 +45,6 @@ type Cache interface {
|
||||
type Meta struct {
|
||||
Name string
|
||||
Distro config.Distro
|
||||
Packs []models.Package
|
||||
Packs models.Packages
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// FindPack search a Package
|
||||
func (m Meta) FindPack(name string) (pack models.Package, found bool) {
|
||||
for _, p := range m.Packs {
|
||||
if name == p.Name {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return pack, false
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/howeyc/gopass"
|
||||
)
|
||||
|
||||
func getPasswd(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
pass, err := gopass.GetPasswdMasked()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to read password")
|
||||
}
|
||||
if 0 < len(pass) {
|
||||
return string(pass[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/future-architect/vuls/report"
|
||||
"github.com/future-architect/vuls/util"
|
||||
"github.com/howeyc/gopass"
|
||||
)
|
||||
|
||||
// jsonDirPattern is file name pattern of JSON directory
|
||||
@@ -190,11 +191,14 @@ func diff(curResults, preResults models.ScanResults) (diffed models.ScanResults,
|
||||
new, updated := getDiffCves(previous, current)
|
||||
current.ScannedCves = append(new, updated...)
|
||||
|
||||
current.Packages = models.Packages{}
|
||||
packages := models.Packages{}
|
||||
for _, s := range current.ScannedCves {
|
||||
current.Packages = append(current.Packages, s.Packages...)
|
||||
for _, pack := range s.Packages {
|
||||
p := current.Packages[pack.Name]
|
||||
packages[pack.Name] = p
|
||||
}
|
||||
}
|
||||
current.Packages = current.Packages.UniqByName()
|
||||
current.Packages = packages
|
||||
}
|
||||
|
||||
diffed = append(diffed, current)
|
||||
@@ -320,3 +324,17 @@ func needToRefreshCve(r models.ScanResult) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getPasswd(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
pass, err := gopass.GetPasswdMasked()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to read password")
|
||||
}
|
||||
if 0 < len(pass) {
|
||||
return string(pass[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2012-6702",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"libexpat1": {
|
||||
Name: "libexpat1",
|
||||
Version: "2.1.0-7",
|
||||
Release: "",
|
||||
@@ -216,7 +216,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2014-9761",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"libc-bin": {
|
||||
Name: "libc-bin",
|
||||
Version: "2.21-0ubuntu5",
|
||||
Release: "",
|
||||
@@ -229,7 +229,7 @@ func TestDiff(t *testing.T) {
|
||||
CpeNames: []string{},
|
||||
},
|
||||
},
|
||||
Packages: []models.Package{},
|
||||
Packages: models.Packages{},
|
||||
Errors: []string{},
|
||||
Optional: [][]interface{}{},
|
||||
},
|
||||
@@ -244,7 +244,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2012-6702",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"libexpat1": {
|
||||
Name: "libexpat1",
|
||||
Version: "2.1.0-7",
|
||||
Release: "",
|
||||
@@ -259,7 +259,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2014-9761",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"libc-bin": {
|
||||
Name: "libc-bin",
|
||||
Version: "2.21-0ubuntu5",
|
||||
Release: "",
|
||||
@@ -272,7 +272,7 @@ func TestDiff(t *testing.T) {
|
||||
CpeNames: []string{},
|
||||
},
|
||||
},
|
||||
Packages: []models.Package{},
|
||||
Packages: models.Packages{},
|
||||
Errors: []string{},
|
||||
Optional: [][]interface{}{},
|
||||
},
|
||||
@@ -282,7 +282,7 @@ func TestDiff(t *testing.T) {
|
||||
ServerName: "u16",
|
||||
Family: "ubuntu",
|
||||
Release: "16.04",
|
||||
Packages: []models.Package{},
|
||||
Packages: models.Packages{},
|
||||
Errors: []string{},
|
||||
Optional: [][]interface{}{},
|
||||
},
|
||||
@@ -298,7 +298,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2016-6662",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"mysql-libs": {
|
||||
Name: "mysql-libs",
|
||||
Version: "5.1.73",
|
||||
Release: "7.el6",
|
||||
@@ -331,7 +331,7 @@ func TestDiff(t *testing.T) {
|
||||
{
|
||||
CveID: "CVE-2016-6662",
|
||||
Packages: models.Packages{
|
||||
{
|
||||
"mysql-libs": {
|
||||
Name: "mysql-libs",
|
||||
Version: "5.1.73",
|
||||
Release: "7.el6",
|
||||
@@ -345,7 +345,7 @@ func TestDiff(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Packages: models.Packages{
|
||||
models.Package{
|
||||
"mysql-libs": {
|
||||
Name: "mysql-libs",
|
||||
Version: "5.1.73",
|
||||
Release: "7.el6",
|
||||
@@ -368,14 +368,14 @@ func TestDiff(t *testing.T) {
|
||||
if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) {
|
||||
h := pp.Sprint(actual.ScannedCves)
|
||||
x := pp.Sprint(tt.out.ScannedCves)
|
||||
t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, h, x)
|
||||
t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x)
|
||||
}
|
||||
|
||||
for j := range tt.out.Packages {
|
||||
if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) {
|
||||
h := pp.Sprint(tt.out.Packages[j])
|
||||
x := pp.Sprint(actual.Packages[j])
|
||||
t.Errorf("[%d] actual: \n %s \n expected: \n %s", i, x, h)
|
||||
t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ type CveContents map[CveContentType]CveContent
|
||||
|
||||
// NewCveContents create CveContents
|
||||
func NewCveContents(conts ...CveContent) CveContents {
|
||||
m := make(map[CveContentType]CveContent)
|
||||
m := map[CveContentType]CveContent{}
|
||||
for _, cont := range conts {
|
||||
m[cont.Type] = cont
|
||||
}
|
||||
@@ -547,69 +547,51 @@ type Reference struct {
|
||||
Link string
|
||||
}
|
||||
|
||||
// Packages is slice of Package
|
||||
type Packages []Package
|
||||
// Packages is Map of Package
|
||||
// { "package-name": Package }
|
||||
type Packages map[string]Package
|
||||
|
||||
// Exists returns true if exists the name
|
||||
func (ps Packages) Exists(name string) bool {
|
||||
for _, p := range ps {
|
||||
if p.Name == name {
|
||||
return true
|
||||
}
|
||||
// NewPackages create Packages
|
||||
func NewPackages(packs ...Package) Packages {
|
||||
m := Packages{}
|
||||
for _, pack := range packs {
|
||||
m[pack.Name] = pack
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UniqByName be uniq by name.
|
||||
func (ps Packages) UniqByName() (distincted Packages) {
|
||||
set := make(map[string]Package)
|
||||
for _, p := range ps {
|
||||
set[p.Name] = p
|
||||
}
|
||||
for _, v := range set {
|
||||
distincted = append(distincted, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FindByName search Package by name
|
||||
func (ps Packages) FindByName(name string) (result Package, found bool) {
|
||||
for _, p := range ps {
|
||||
if p.Name == name {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Package{}, false
|
||||
return m
|
||||
}
|
||||
|
||||
// MergeNewVersion merges candidate version information to the receiver struct
|
||||
func (ps Packages) MergeNewVersion(as Packages) {
|
||||
for _, a := range as {
|
||||
for i, p := range ps {
|
||||
if p.Name == a.Name {
|
||||
ps[i].NewVersion = a.NewVersion
|
||||
ps[i].NewRelease = a.NewRelease
|
||||
}
|
||||
if pack, ok := ps[a.Name]; ok {
|
||||
pack.NewVersion = a.NewVersion
|
||||
pack.NewRelease = a.NewRelease
|
||||
ps[a.Name] = pack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps Packages) countUpdatablePacks() int {
|
||||
count := 0
|
||||
set := make(map[string]bool)
|
||||
for _, p := range ps {
|
||||
if len(p.NewVersion) != 0 && !set[p.Name] {
|
||||
count++
|
||||
set[p.Name] = true
|
||||
}
|
||||
// Merge returns merged map (immutable)
|
||||
func (ps Packages) Merge(other Packages) Packages {
|
||||
merged := map[string]Package{}
|
||||
for k, v := range ps {
|
||||
merged[k] = v
|
||||
}
|
||||
return count
|
||||
for k, v := range other {
|
||||
merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// FormatUpdatablePacksSummary returns a summary of updatable packages
|
||||
func (ps Packages) FormatUpdatablePacksSummary() string {
|
||||
return fmt.Sprintf("%d updatable packages",
|
||||
ps.countUpdatablePacks())
|
||||
nUpdatable := 0
|
||||
for _, p := range ps {
|
||||
if p.NewVersion != "" {
|
||||
nUpdatable++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%d updatable packages", nUpdatable)
|
||||
}
|
||||
|
||||
// Package has installed packages.
|
||||
|
||||
@@ -19,52 +19,11 @@ package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k0kubun/pp"
|
||||
)
|
||||
|
||||
func TestPackagesUniqByName(t *testing.T) {
|
||||
var test = struct {
|
||||
in Packages
|
||||
out Packages
|
||||
}{
|
||||
Packages{
|
||||
{
|
||||
Name: "hoge",
|
||||
},
|
||||
{
|
||||
Name: "fuga",
|
||||
},
|
||||
{
|
||||
Name: "hoge",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
{
|
||||
Name: "hoge",
|
||||
},
|
||||
{
|
||||
Name: "fuga",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual := test.in.UniqByName()
|
||||
sort.Slice(actual, func(i, j int) bool {
|
||||
return actual[i].Name < actual[j].Name
|
||||
})
|
||||
sort.Slice(test.out, func(i, j int) bool {
|
||||
return test.out[i].Name < test.out[j].Name
|
||||
})
|
||||
for i, ePack := range test.out {
|
||||
if actual[i].Name != ePack.Name {
|
||||
t.Errorf("expected %#v, actual %#v", ePack.Name, actual[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNewVersion(t *testing.T) {
|
||||
var test = struct {
|
||||
a Packages
|
||||
@@ -72,19 +31,19 @@ func TestMergeNewVersion(t *testing.T) {
|
||||
expected Packages
|
||||
}{
|
||||
Packages{
|
||||
{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
},
|
||||
},
|
||||
Packages{
|
||||
{
|
||||
"hoge": {
|
||||
Name: "hoge",
|
||||
NewVersion: "1.0.0",
|
||||
NewRelease: "release1",
|
||||
|
||||
14
oval/oval.go
14
oval/oval.go
@@ -11,15 +11,11 @@ type Client interface {
|
||||
}
|
||||
|
||||
func getPackages(r *models.ScanResult, d *ovalmodels.Definition) models.Packages {
|
||||
var packages models.Packages
|
||||
for _, pack := range d.AffectedPacks {
|
||||
for _, p := range r.Packages {
|
||||
if pack.Name == p.Name {
|
||||
p.Changelog = models.Changelog{}
|
||||
packages = append(packages, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
packages := models.Packages{}
|
||||
for _, affectedPack := range d.AffectedPacks {
|
||||
pack, _ := r.Packages[affectedPack.Name]
|
||||
// pack.Changelog = models.Changelog{}
|
||||
packages[affectedPack.Name] = pack
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini
|
||||
ovalContent := *o.convertToModel(cve.CveID, definition)
|
||||
vinfo, ok := r.ScannedCves.Get(cve.CveID)
|
||||
if !ok {
|
||||
util.Log.Infof("%s is newly detected by OVAL", definition.Debian.CveID)
|
||||
util.Log.Infof("%s is newly detected by OVAL", cve.CveID)
|
||||
vinfo = models.VulnInfo{
|
||||
CveID: cve.CveID,
|
||||
Confidence: models.OvalMatch,
|
||||
@@ -70,9 +70,9 @@ func (o Redhat) fillOvalInfo(r *models.ScanResult, definition *ovalmodels.Defini
|
||||
}
|
||||
} else {
|
||||
if _, ok := vinfo.CveContents.Get(models.RedHat); !ok {
|
||||
util.Log.Infof("%s is also detected by OVAL", definition.Debian.CveID)
|
||||
util.Log.Infof("%s is also detected by OVAL", cve.CveID)
|
||||
} else {
|
||||
util.Log.Infof("%s will be updated by OVAL", definition.Debian.CveID)
|
||||
util.Log.Infof("%s will be updated by OVAL", cve.CveID)
|
||||
}
|
||||
if vinfo.Confidence.Score < models.OvalMatch.Score {
|
||||
vinfo.Confidence = models.OvalMatch
|
||||
|
||||
@@ -181,7 +181,10 @@ func (o *debian) scanPackages() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable models.Packages, err error) {
|
||||
func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) {
|
||||
installed := models.Packages{}
|
||||
upgradable := models.Packages{}
|
||||
|
||||
r := o.exec("dpkg-query -W", noSudo)
|
||||
if !r.isSuccess() {
|
||||
return nil, nil, fmt.Errorf("Failed to SSH: %s", r)
|
||||
@@ -198,10 +201,10 @@ func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable
|
||||
return nil, nil, fmt.Errorf(
|
||||
"Debian: Failed to parse package line: %s", line)
|
||||
}
|
||||
installed = append(installed, models.Package{
|
||||
installed[name] = models.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,20 +215,20 @@ func (o *debian) scanInstalledPackages() (installed models.Packages, upgradable
|
||||
for _, name := range upgradableNames {
|
||||
for _, pack := range installed {
|
||||
if pack.Name == name {
|
||||
upgradable = append(upgradable, pack)
|
||||
upgradable[name] = pack
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the candidate versions of upgradable packages
|
||||
upgradable, err = o.fillCandidateVersion(upgradable)
|
||||
err = o.fillCandidateVersion(upgradable)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err)
|
||||
}
|
||||
installed.MergeNewVersion(upgradable)
|
||||
|
||||
return
|
||||
return installed, upgradable, nil
|
||||
}
|
||||
|
||||
var packageLinePattern = regexp.MustCompile(`^([^\t']+)\t(.+)$`)
|
||||
@@ -254,7 +257,7 @@ func (o *debian) aptGetUpdate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *debian) scanUnsecurePackages(upgradable []models.Package) ([]models.VulnInfo, error) {
|
||||
func (o *debian) scanUnsecurePackages(upgradable models.Packages) ([]models.VulnInfo, error) {
|
||||
|
||||
o.aptGetUpdate()
|
||||
|
||||
@@ -315,28 +318,28 @@ func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) {
|
||||
return &cached, nil
|
||||
}
|
||||
|
||||
func (o *debian) fillCandidateVersion(before models.Packages) (filled []models.Package, err error) {
|
||||
func (o *debian) fillCandidateVersion(packages models.Packages) (err error) {
|
||||
names := []string{}
|
||||
for _, p := range before {
|
||||
names = append(names, p.Name)
|
||||
for name := range packages {
|
||||
names = append(names, name)
|
||||
}
|
||||
cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " "))
|
||||
r := o.exec(cmd, noSudo)
|
||||
if !r.isSuccess() {
|
||||
return nil, fmt.Errorf("Failed to SSH: %s", r)
|
||||
return fmt.Errorf("Failed to SSH: %s", r)
|
||||
}
|
||||
packChangelog := o.splitAptCachePolicy(r.Stdout)
|
||||
for k, v := range packChangelog {
|
||||
ver, err := o.parseAptCachePolicy(v, k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse %s", err)
|
||||
return fmt.Errorf("Failed to parse %s", err)
|
||||
}
|
||||
p, found := before.FindByName(k)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Not found: %s", k)
|
||||
pack, ok := packages[k]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", k)
|
||||
}
|
||||
p.NewVersion = ver.Candidate
|
||||
filled = append(filled, p)
|
||||
pack.NewVersion = ver.Candidate
|
||||
packages[k] = pack
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -394,7 +397,7 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er
|
||||
return
|
||||
}
|
||||
|
||||
func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Meta) (models.VulnInfos, error) {
|
||||
func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) {
|
||||
resChan := make(chan struct {
|
||||
models.Package
|
||||
DetectedCveIDs
|
||||
@@ -412,7 +415,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met
|
||||
}()
|
||||
|
||||
timeout := time.After(30 * 60 * time.Second)
|
||||
concurrency := 10
|
||||
concurrency := 1
|
||||
tasks := util.GenWorkers(concurrency)
|
||||
for range upgradablePacks {
|
||||
tasks <- func() {
|
||||
@@ -446,18 +449,23 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met
|
||||
}
|
||||
|
||||
// { DetectedCveID{} : [package] }
|
||||
cvePackages := make(map[DetectedCveID][]models.Package)
|
||||
cvePackages := make(map[DetectedCveID]models.Packages)
|
||||
errs := []error{}
|
||||
for i := 0; i < len(upgradablePacks); i++ {
|
||||
select {
|
||||
case pair := <-resChan:
|
||||
pack := pair.Package
|
||||
cveIDs := pair.DetectedCveIDs
|
||||
for _, cveID := range cveIDs {
|
||||
cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack)
|
||||
cves := pair.DetectedCveIDs
|
||||
for _, cve := range cves {
|
||||
packs, ok := cvePackages[cve]
|
||||
if ok {
|
||||
packs[cve.CveID] = pair.Package
|
||||
} else {
|
||||
packs = models.Packages{}
|
||||
}
|
||||
cvePackages[cve] = packs
|
||||
}
|
||||
o.log.Infof("(%d/%d) Scanned %s-%s : %s",
|
||||
i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cveIDs)
|
||||
i+1, len(upgradablePacks), pair.Name, pair.Package.Version, cves)
|
||||
case err := <-errChan:
|
||||
errs = append(errs, err)
|
||||
case <-timeout:
|
||||
@@ -492,7 +500,7 @@ func (o *debian) scanVulnInfos(upgradablePacks []models.Package, meta *cache.Met
|
||||
}
|
||||
|
||||
func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string {
|
||||
cachedPack, found := meta.FindPack(pack.Name)
|
||||
cachedPack, found := meta.Packs[pack.Name]
|
||||
if !found {
|
||||
o.log.Debugf("Not found: %s", pack.Name)
|
||||
return ""
|
||||
@@ -602,14 +610,12 @@ func (o *debian) getCveIDsFromChangelog(
|
||||
// Only logging the error.
|
||||
o.log.Error(err)
|
||||
|
||||
for i, p := range o.Packages {
|
||||
if p.Name == name {
|
||||
o.Packages[i].Changelog = models.Changelog{
|
||||
Contents: "",
|
||||
Method: models.FailedToFindVersionInChangelog,
|
||||
}
|
||||
}
|
||||
pack := o.Packages[name]
|
||||
pack.Changelog = models.Changelog{
|
||||
Contents: "",
|
||||
Method: models.FailedToFindVersionInChangelog,
|
||||
}
|
||||
o.Packages[name] = pack
|
||||
|
||||
// If the version is not in changelog, return entire changelog to put into cache
|
||||
return []DetectedCveID{}, models.Changelog{
|
||||
@@ -666,11 +672,9 @@ func (o *debian) parseChangelog(changelog, name, ver string, confidence models.C
|
||||
Method: string(confidence.DetectionMethod),
|
||||
}
|
||||
|
||||
for i, p := range o.Packages {
|
||||
if p.Name == name {
|
||||
o.Packages[i].Changelog = clog
|
||||
}
|
||||
}
|
||||
pack := o.Packages[name]
|
||||
pack.Changelog = clog
|
||||
o.Packages[name] = pack
|
||||
|
||||
cves := []DetectedCveID{}
|
||||
for _, id := range cveIDs {
|
||||
@@ -729,14 +733,3 @@ func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, err
|
||||
}
|
||||
return ver, fmt.Errorf("Unknown Format: %s", stdout)
|
||||
}
|
||||
|
||||
func appendPackIfMissing(slice []models.Package, s models.Package) []models.Package {
|
||||
for _, ele := range slice {
|
||||
if ele.Name == s.Name &&
|
||||
ele.Version == s.Version &&
|
||||
ele.Release == s.Release {
|
||||
return slice
|
||||
}
|
||||
}
|
||||
return append(slice, s)
|
||||
}
|
||||
|
||||
@@ -624,7 +624,9 @@ func TestGetChangelogCache(t *testing.T) {
|
||||
Family: "ubuntu",
|
||||
Release: "16.04",
|
||||
},
|
||||
Packs: []models.Package{pack},
|
||||
Packs: models.Packages{
|
||||
"apt": pack,
|
||||
},
|
||||
}
|
||||
|
||||
const path = "/tmp/vuls-test-cache-11111111.db"
|
||||
|
||||
@@ -71,7 +71,7 @@ func (o *bsd) checkDependencies() error {
|
||||
|
||||
func (o *bsd) scanPackages() error {
|
||||
var err error
|
||||
var packs []models.Package
|
||||
var packs models.Packages
|
||||
if packs, err = o.scanInstalledPackages(); err != nil {
|
||||
o.log.Errorf("Failed to scan installed packages")
|
||||
return err
|
||||
@@ -87,7 +87,7 @@ func (o *bsd) scanPackages() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *bsd) scanInstalledPackages() ([]models.Package, error) {
|
||||
func (o *bsd) scanInstalledPackages() (models.Packages, error) {
|
||||
cmd := util.PrependProxyEnv("pkg version -v")
|
||||
r := o.exec(cmd, noSudo)
|
||||
if !r.isSuccess() {
|
||||
@@ -121,7 +121,7 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) {
|
||||
if len(cveIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
pack, found := o.Packages.FindByName(name)
|
||||
pack, found := o.Packages[name]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Vulnerable package: %s is not found", name)
|
||||
}
|
||||
@@ -143,9 +143,9 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) {
|
||||
}
|
||||
|
||||
for k := range cveIDAdtMap {
|
||||
packs := []models.Package{}
|
||||
packs := models.Packages{}
|
||||
for _, r := range cveIDAdtMap[k] {
|
||||
packs = append(packs, r.pack)
|
||||
packs[r.pack.Name] = r.pack
|
||||
}
|
||||
|
||||
disAdvs := []models.DistroAdvisory{}
|
||||
@@ -165,7 +165,8 @@ func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (o *bsd) parsePkgVersion(stdout string) (packs []models.Package) {
|
||||
func (o *bsd) parsePkgVersion(stdout string) models.Packages {
|
||||
packs := models.Packages{}
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, l := range lines {
|
||||
fields := strings.Fields(l)
|
||||
@@ -180,20 +181,20 @@ func (o *bsd) parsePkgVersion(stdout string) (packs []models.Package) {
|
||||
|
||||
switch fields[1] {
|
||||
case "?", "=":
|
||||
packs = append(packs, models.Package{
|
||||
packs[name] = models.Package{
|
||||
Name: name,
|
||||
Version: ver,
|
||||
})
|
||||
}
|
||||
case "<":
|
||||
candidate := strings.TrimSuffix(fields[6], ")")
|
||||
packs = append(packs, models.Package{
|
||||
packs[name] = models.Package{
|
||||
Name: name,
|
||||
Version: ver,
|
||||
NewVersion: candidate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return packs
|
||||
}
|
||||
|
||||
type vulnIDCveIDs struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestParsePkgVersion(t *testing.T) {
|
||||
var tests = []struct {
|
||||
in string
|
||||
expected []models.Package
|
||||
expected models.Packages
|
||||
}{
|
||||
{
|
||||
`Updating FreeBSD repository catalogue...
|
||||
@@ -23,22 +23,22 @@ gettext-0.18.3.1 < needs updating (remote has 0.19.7)
|
||||
tcl84-8.4.20_2,1 = up-to-date with remote
|
||||
teTeX-base-3.0_25 ? orphaned: print/teTeX-base`,
|
||||
|
||||
[]models.Package{
|
||||
{
|
||||
models.Packages{
|
||||
"bash": {
|
||||
Name: "bash",
|
||||
Version: "4.2.45",
|
||||
NewVersion: "4.3.42_1",
|
||||
},
|
||||
{
|
||||
"gettext": {
|
||||
Name: "gettext",
|
||||
Version: "0.18.3.1",
|
||||
NewVersion: "0.19.7",
|
||||
},
|
||||
{
|
||||
"tcl84": {
|
||||
Name: "tcl84",
|
||||
Version: "8.4.20_2,1",
|
||||
},
|
||||
{
|
||||
"teTeX-base": {
|
||||
Name: "teTeX-base",
|
||||
Version: "3.0_25",
|
||||
},
|
||||
|
||||
@@ -231,7 +231,7 @@ func (o *redhat) scanPackages() error {
|
||||
o.log.Errorf("Failed to scan installed packages")
|
||||
return err
|
||||
}
|
||||
o.setPackages(packs)
|
||||
o.setPackages(models.NewPackages(packs...))
|
||||
|
||||
var vinfos []models.VulnInfo
|
||||
if vinfos, err = o.scanVulnInfos(); err != nil {
|
||||
@@ -242,7 +242,7 @@ func (o *redhat) scanPackages() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err error) {
|
||||
func (o *redhat) scanInstalledPackages() (installed []models.Package, err error) {
|
||||
cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'"
|
||||
r := o.exec(cmd, noSudo)
|
||||
if r.isSuccess() {
|
||||
@@ -255,13 +255,13 @@ func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err
|
||||
if pack, err = o.parseScannedPackagesLine(line); err != nil {
|
||||
return
|
||||
}
|
||||
installedPackages = append(installedPackages, pack)
|
||||
installed = append(installed, pack)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return installedPackages, fmt.Errorf(
|
||||
return nil, fmt.Errorf(
|
||||
"Scan packages failed. status: %d, stdout: %s, stderr: %s",
|
||||
r.ExitStatus, r.Stdout, r.Stderr)
|
||||
}
|
||||
@@ -341,22 +341,23 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er
|
||||
}
|
||||
|
||||
for name, clog := range rpm2changelog {
|
||||
for i, p := range o.Packages {
|
||||
n := fmt.Sprintf("%s-%s-%s",
|
||||
p.Name, p.NewVersion, p.NewRelease)
|
||||
for _, p := range o.Packages {
|
||||
n := fmt.Sprintf("%s-%s-%s", p.Name, p.NewVersion, p.NewRelease)
|
||||
if name == n {
|
||||
o.Packages[i].Changelog = models.Changelog{
|
||||
p.Changelog = models.Changelog{
|
||||
Contents: *clog,
|
||||
Method: models.ChangelogExactMatchStr,
|
||||
}
|
||||
o.Packages[p.Name] = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results []PackageCveIDs
|
||||
for i, pack := range packages {
|
||||
changelog := o.getChangelogCVELines(rpm2changelog, pack)
|
||||
i := 0
|
||||
for name := range packages {
|
||||
changelog := o.getChangelogCVELines(rpm2changelog, packages[name])
|
||||
|
||||
// Collect unique set of CVE-ID in each changelog
|
||||
uniqueCveIDMap := make(map[string]bool)
|
||||
@@ -374,7 +375,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er
|
||||
cveIDs = append(cveIDs, k)
|
||||
}
|
||||
p := PackageCveIDs{
|
||||
Package: pack,
|
||||
Package: packages[name],
|
||||
CveIDs: cveIDs,
|
||||
}
|
||||
results = append(results, p)
|
||||
@@ -388,6 +389,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er
|
||||
p.Package.NewVersion,
|
||||
p.Package.NewRelease,
|
||||
p.CveIDs)
|
||||
i++
|
||||
}
|
||||
|
||||
// transform datastructure
|
||||
@@ -415,7 +417,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er
|
||||
// Amazon, RHEL do not use this method, so VendorAdvisory do not set.
|
||||
vinfos = append(vinfos, models.VulnInfo{
|
||||
CveID: k,
|
||||
Packages: v,
|
||||
Packages: models.NewPackages(v...),
|
||||
Confidence: models.ChangelogExactMatch,
|
||||
})
|
||||
}
|
||||
@@ -423,7 +425,8 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er
|
||||
}
|
||||
|
||||
// parseYumCheckUpdateLines parse yum check-update to get package name, candidate version
|
||||
func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Packages, err error) {
|
||||
func (o *redhat) parseYumCheckUpdateLines(stdout string) (models.Packages, error) {
|
||||
results := models.Packages{}
|
||||
needToParse := false
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
@@ -443,20 +446,20 @@ func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Package
|
||||
return results, err
|
||||
}
|
||||
|
||||
installed, found := o.Packages.FindByName(candidate.Name)
|
||||
installed, found := o.Packages[candidate.Name]
|
||||
if !found {
|
||||
o.log.Warnf("Not found the package in rpm -qa. candidate: %s-%s-%s",
|
||||
candidate.Name, candidate.Version, candidate.Release)
|
||||
results = append(results, candidate)
|
||||
results[candidate.Name] = candidate
|
||||
continue
|
||||
}
|
||||
installed.NewVersion = candidate.NewVersion
|
||||
installed.NewRelease = candidate.NewRelease
|
||||
installed.Repository = candidate.Repository
|
||||
results = append(results, installed)
|
||||
results[installed.Name] = installed
|
||||
}
|
||||
}
|
||||
return
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (o *redhat) parseYumCheckUpdateLine(line string) (models.Package, error) {
|
||||
@@ -686,16 +689,16 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos,
|
||||
// set candidate version info
|
||||
o.Packages.MergeNewVersion(updatable)
|
||||
|
||||
dict := map[string][]models.Package{}
|
||||
dict := make(map[string]models.Packages)
|
||||
for _, advIDPackNames := range advIDPackNamesList {
|
||||
packages := models.Packages{}
|
||||
for _, packName := range advIDPackNames.PackNames {
|
||||
pack, found := updatable.FindByName(packName)
|
||||
pack, found := updatable[packName]
|
||||
if !found {
|
||||
return nil, fmt.Errorf(
|
||||
"Package not found. pack: %#v", packName)
|
||||
}
|
||||
packages = append(packages, pack)
|
||||
packages[pack.Name] = pack
|
||||
continue
|
||||
}
|
||||
dict[advIDPackNames.AdvisoryID] = packages
|
||||
@@ -729,7 +732,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos,
|
||||
vinfos[i].DistroAdvisories = advAppended
|
||||
|
||||
packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
|
||||
vinfos[i].Packages = append(vinfos[i].Packages, packs...)
|
||||
vinfos[i].Packages = vinfos[i].Packages.Merge(packs)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -440,11 +440,13 @@ Description : kernel-uek
|
||||
for _, tt := range tests {
|
||||
actual, _ := r.parseYumUpdateinfo(tt.in)
|
||||
for i, advisoryCveIDs := range actual {
|
||||
if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
|
||||
e := pp.Sprintf("%v", tt.out[i])
|
||||
a := pp.Sprintf("%v", advisoryCveIDs)
|
||||
if tt.out[i].DistroAdvisory != advisoryCveIDs.DistroAdvisory {
|
||||
t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s",
|
||||
i, e, a)
|
||||
i, tt.out[i].DistroAdvisory, advisoryCveIDs.DistroAdvisory)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.out[i].CveIDs, advisoryCveIDs.CveIDs) {
|
||||
t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s",
|
||||
i, tt.out[i].CveIDs, advisoryCveIDs.CveIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,14 +564,14 @@ Description : The Berkeley Internet Name Domain (BIND) is an implementation of
|
||||
}
|
||||
for _, tt := range tests {
|
||||
actual, _ := r.parseYumUpdateinfo(tt.in)
|
||||
for j, advisoryCveIDs := range actual {
|
||||
sort.Strings(tt.out[j].CveIDs)
|
||||
for i, advisoryCveIDs := range actual {
|
||||
sort.Strings(tt.out[i].CveIDs)
|
||||
sort.Strings(advisoryCveIDs.CveIDs)
|
||||
if !reflect.DeepEqual(tt.out[j], advisoryCveIDs) {
|
||||
e := pp.Sprintf("%v", tt.out[j])
|
||||
if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
|
||||
e := pp.Sprintf("%v", tt.out[i])
|
||||
a := pp.Sprintf("%v", advisoryCveIDs)
|
||||
t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s",
|
||||
j, e, a)
|
||||
i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,46 +690,46 @@ bind-utils.x86_64 30:9.3.6-25.P1.el5_11.8 updates
|
||||
pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
`
|
||||
|
||||
r.Packages = []models.Package{
|
||||
{
|
||||
r.setPackages(models.NewPackages(
|
||||
models.Package{
|
||||
Name: "audit-libs",
|
||||
Version: "2.3.6",
|
||||
Release: "4.el6",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "bash",
|
||||
Version: "4.1.1",
|
||||
Release: "33",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "python-libs",
|
||||
Version: "2.6.0",
|
||||
Release: "1.1-0",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "python-ordereddict",
|
||||
Version: "1.0",
|
||||
Release: "1",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "bind-utils",
|
||||
Version: "1.0",
|
||||
Release: "1",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "pytalloc",
|
||||
Version: "2.0.1",
|
||||
Release: "0",
|
||||
},
|
||||
}
|
||||
))
|
||||
var tests = []struct {
|
||||
in string
|
||||
out models.Packages
|
||||
}{
|
||||
{
|
||||
stdout,
|
||||
models.Packages{
|
||||
{
|
||||
models.NewPackages(
|
||||
models.Package{
|
||||
Name: "audit-libs",
|
||||
Version: "2.3.6",
|
||||
Release: "4.el6",
|
||||
@@ -735,7 +737,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "5.el6",
|
||||
Repository: "base",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "bash",
|
||||
Version: "4.1.1",
|
||||
Release: "33",
|
||||
@@ -743,7 +745,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "33.el6_7.1",
|
||||
Repository: "updates",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "python-libs",
|
||||
Version: "2.6.0",
|
||||
Release: "1.1-0",
|
||||
@@ -751,7 +753,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "64.el6",
|
||||
Repository: "rhui-REGION-rhel-server-releases",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "python-ordereddict",
|
||||
Version: "1.0",
|
||||
Release: "1",
|
||||
@@ -759,7 +761,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "3.el6ev",
|
||||
Repository: "installed",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "bind-utils",
|
||||
Version: "1.0",
|
||||
Release: "1",
|
||||
@@ -767,7 +769,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "25.P1.el5_11.8",
|
||||
Repository: "updates",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "pytalloc",
|
||||
Version: "2.0.1",
|
||||
Release: "0",
|
||||
@@ -775,7 +777,7 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
NewRelease: "2.el6",
|
||||
Repository: "@CentOS 6.5/6.5",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -785,11 +787,11 @@ pytalloc.x86_64 2.0.7-2.el6 @CentOS 6.5/6.5
|
||||
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
|
||||
return
|
||||
}
|
||||
for i, ePack := range tt.out {
|
||||
if !reflect.DeepEqual(ePack, packages[i]) {
|
||||
for name, ePack := range tt.out {
|
||||
if !reflect.DeepEqual(ePack, packages[name]) {
|
||||
e := pp.Sprintf("%v", ePack)
|
||||
a := pp.Sprintf("%v", packages[i])
|
||||
t.Errorf("[%d] expected %s, actual %s", i, e, a)
|
||||
a := pp.Sprintf("%v", packages[name])
|
||||
t.Errorf("expected %s, actual %s", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,31 +807,31 @@ bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main
|
||||
java-1.7.0-openjdk.x86_64 1.7.0.95-2.6.4.0.65.amzn1 amzn-main
|
||||
if-not-architecture 100-200 amzn-main
|
||||
`
|
||||
r.Packages = []models.Package{
|
||||
{
|
||||
r.Packages = models.NewPackages(
|
||||
models.Package{
|
||||
Name: "bind-libs",
|
||||
Version: "9.8.0",
|
||||
Release: "0.33.rc1.45.amzn1",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "java-1.7.0-openjdk",
|
||||
Version: "1.7.0.0",
|
||||
Release: "2.6.4.0.0.amzn1",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "if-not-architecture",
|
||||
Version: "10",
|
||||
Release: "20",
|
||||
},
|
||||
}
|
||||
)
|
||||
var tests = []struct {
|
||||
in string
|
||||
out models.Packages
|
||||
}{
|
||||
{
|
||||
stdout,
|
||||
models.Packages{
|
||||
{
|
||||
models.NewPackages(
|
||||
models.Package{
|
||||
Name: "bind-libs",
|
||||
Version: "9.8.0",
|
||||
Release: "0.33.rc1.45.amzn1",
|
||||
@@ -837,7 +839,7 @@ if-not-architecture 100-200 amzn-main
|
||||
NewRelease: "0.37.rc1.45.amzn1",
|
||||
Repository: "amzn-main",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "java-1.7.0-openjdk",
|
||||
Version: "1.7.0.0",
|
||||
Release: "2.6.4.0.0.amzn1",
|
||||
@@ -845,7 +847,7 @@ if-not-architecture 100-200 amzn-main
|
||||
NewRelease: "2.6.4.0.65.amzn1",
|
||||
Repository: "amzn-main",
|
||||
},
|
||||
{
|
||||
models.Package{
|
||||
Name: "if-not-architecture",
|
||||
Version: "10",
|
||||
Release: "20",
|
||||
@@ -853,7 +855,7 @@ if-not-architecture 100-200 amzn-main
|
||||
NewRelease: "200",
|
||||
Repository: "amzn-main",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -863,11 +865,11 @@ if-not-architecture 100-200 amzn-main
|
||||
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
|
||||
return
|
||||
}
|
||||
for i, ePack := range tt.out {
|
||||
if !reflect.DeepEqual(ePack, packages[i]) {
|
||||
for name, ePack := range tt.out {
|
||||
if !reflect.DeepEqual(ePack, packages[name]) {
|
||||
e := pp.Sprintf("%v", ePack)
|
||||
a := pp.Sprintf("%v", packages[i])
|
||||
t.Errorf("[%d] expected %s, actual %s", i, e, a)
|
||||
a := pp.Sprintf("%v", packages[name])
|
||||
t.Errorf("[%s] expected %s, actual %s", name, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user