Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(os): support for Arch Linux #2010

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Deadlyelder
Copy link

@Deadlyelder Deadlyelder commented Aug 20, 2024

What did you implement:

Implements support for Arch Linux.

Fixes # (issue)

Addresses #527 and fixes #2009

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Tested on an instance of Arch Linux hosted on network for similar functionalities as other supported OS on vuls.

Checklist:

You don't have to satisfy all of the following.

  • Write tests
  • Write documentation
  • Check that there aren't other open pull requests for the same issue/feature
  • Format your source code by make fmt
  • Pass the test by make test
  • Enable "Allow edits from maintainers" for this PR
  • Update the messages below

Is this ready for review?: YES

@MaineK00n
Copy link
Collaborator

@Deadlyelder @ziyagenc
Thank you for the PR, I will provide a detailed review after August 29th.

@MaineK00n
Copy link
Collaborator

MaineK00n commented Sep 3, 2024

Since the fork repository created by the organization does not allow the maintainer to add commits, I have written the following patch.
Please consider applying the patch.

Also, when sending PRs in the future, please write unit tests.

commit 2ba1adb1557c3cbf1a81bfce14096d2358dd4e8b
Author: MaineK00n <[email protected]>
Date:   Sat Aug 31 15:52:05 2024 +0900

    refactor(arch): reviewer corrections

:100644 100644 1e5ccea 7a0d2e1 M	detector/detector.go
:100644 100644 46ce7a3 ae75d8f M	go.mod
:100644 100644 d1f77a1 ec676b3 M	go.sum
:100644 100644 adef4d1 ed094cb M	gost/arch.go
:000000 100644 0000000 27aca8f A	gost/arch_test.go
:100644 100644 3bee991 3c33eca M	models/vulninfos.go
:100644 000000 17e9f2a 0000000 D	oval/arch.go
:100644 100644 5b6ae37 cded582 M	oval/util.go
:100644 100644 62ae527 6a71cc0 M	scanner/arch.go
:000000 100644 0000000 135b497 A	scanner/arch_test.go
:100644 100644 56a5892 82ae7d9 M	scanner/scanner.go

diff --git a/detector/detector.go b/detector/detector.go
index 1e5ccea..7a0d2e1 100644
--- a/detector/detector.go
+++ b/detector/detector.go
@@ -373,9 +373,7 @@ func isPkgCvesDetactable(r *models.ScanResult) bool {
 	case constant.FreeBSD, constant.MacOSX, constant.MacOSXServer, constant.MacOS, constant.MacOSServer, constant.ServerTypePseudo:
 		logging.Log.Infof("%s type. Skip OVAL and gost detection", r.Family)
 		return false
-	case constant.Arch:
-		return true
-	case constant.Windows:
+	case constant.Arch, constant.Windows:
 		return true
 	default:
 		if r.ScannedVia == "trivy" {
@@ -536,11 +534,11 @@ func detectPkgsCvesWithOval(cnf config.GovalDictConf, r *models.ScanResult, logO
 	}()
 
 	switch r.Family {
-	case constant.Arch, constant.Debian, constant.Raspbian, constant.Ubuntu:
+	case constant.Debian, constant.Raspbian, constant.Ubuntu:
 		logging.Log.Infof("Skip OVAL and Scan with gost alone.")
 		logging.Log.Infof("%s: %d CVEs are detected with OVAL", r.FormatServerName(), 0)
 		return nil
-	case constant.Windows, constant.MacOSX, constant.MacOSXServer, constant.MacOS, constant.MacOSServer, constant.FreeBSD, constant.ServerTypePseudo:
+	case constant.Arch, constant.Windows, constant.MacOSX, constant.MacOSXServer, constant.MacOS, constant.MacOSServer, constant.FreeBSD, constant.ServerTypePseudo:
 		return nil
 	default:
 		logging.Log.Debugf("Check if oval fetched: %s %s", r.Family, r.Release)
diff --git a/go.mod b/go.mod
index 46ce7a3..ae75d8f 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
 	github.com/BurntSushi/toml v1.4.0
 	github.com/CycloneDX/cyclonedx-go v0.9.0
+	github.com/MaineK00n/go-pacman-version v0.0.0-20210916231937-19e87b7d7184
 	github.com/Ullaakut/nmap/v2 v2.2.2
 	github.com/aquasecurity/trivy v0.54.1
 	github.com/aquasecurity/trivy-db v0.0.0-20240718084044-d23a6ca8ba04
@@ -55,7 +56,7 @@ require (
 	github.com/vulsio/go-exploitdb v0.4.7-0.20240318122115-ccb3abc151a1
 	github.com/vulsio/go-kev v0.1.4-0.20240318121733-b3386e67d3fb
 	github.com/vulsio/go-msfdb v0.2.4-0.20240318121704-8bfc812656dc
-	github.com/vulsio/gost v0.4.6-0.20240501065222-d47d2e716bfa
+	github.com/vulsio/gost v0.4.6-0.20240830085319-0786e102a856
 	github.com/vulsio/goval-dictionary v0.9.6-0.20240625074017-1da5dfb8b28a
 	go.etcd.io/bbolt v1.3.10
 	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
@@ -130,7 +131,7 @@ require (
 	github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c // indirect
 	github.com/blang/semver v3.5.1+incompatible // indirect
 	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
-	github.com/briandowns/spinner v1.23.0 // indirect
+	github.com/briandowns/spinner v1.23.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/chai2010/gettext-go v1.0.2 // indirect
diff --git a/go.sum b/go.sum
index d1f77a1..ec676b3 100644
--- a/go.sum
+++ b/go.sum
@@ -255,6 +255,8 @@ github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw
 github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o=
 github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A=
 github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw=
+github.com/MaineK00n/go-pacman-version v0.0.0-20210916231937-19e87b7d7184 h1:enu2psM1AcUsNx36T+X13lcy2kmFFV4kwCMmL7i4yiQ=
+github.com/MaineK00n/go-pacman-version v0.0.0-20210916231937-19e87b7d7184/go.mod h1:iMNOZ59Aouwx++SN7zGEi8yB9JTd+ZwYufdnC02mjd4=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -426,8 +428,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwN
 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
 github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
-github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
-github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
+github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650=
+github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
@@ -1382,8 +1384,8 @@ github.com/vulsio/go-kev v0.1.4-0.20240318121733-b3386e67d3fb h1:j03zKKkR+WWaPoP
 github.com/vulsio/go-kev v0.1.4-0.20240318121733-b3386e67d3fb/go.mod h1:AjLUC5oGYi3dWakVE6WuuHoC+xL/f8YN8CFC45oTE9c=
 github.com/vulsio/go-msfdb v0.2.4-0.20240318121704-8bfc812656dc h1:nf62vF8T3yAmmwu7xMycqIvTVincv/sH7FyeeWWodxs=
 github.com/vulsio/go-msfdb v0.2.4-0.20240318121704-8bfc812656dc/go.mod h1:X7NqckQva6ok3GaWRYFAEvd72xzWFeGKOm9YOCWeIhc=
-github.com/vulsio/gost v0.4.6-0.20240501065222-d47d2e716bfa h1:AmXiFpp2kFuoCgGw/yBl+RGuanSbPg7cV78dvIrbJ/k=
-github.com/vulsio/gost v0.4.6-0.20240501065222-d47d2e716bfa/go.mod h1:fWe/YGX+XpPYIjrIvvl15/x/6GXj+pqbn8BHwnE3X/g=
+github.com/vulsio/gost v0.4.6-0.20240830085319-0786e102a856 h1:BHS1viDR2LyDK5zfntQYyCr4ZHzTHSa68976Ks6pvlA=
+github.com/vulsio/gost v0.4.6-0.20240830085319-0786e102a856/go.mod h1:CHZaikJjPZhNGtiVI5BUXbZ1WAD9o1QWujd3Ch+Wj/0=
 github.com/vulsio/goval-dictionary v0.9.6-0.20240625074017-1da5dfb8b28a h1:8X9wH7AocxgrM52PYtjBZ2Xd/axrzCHonWwhQZSgQaM=
 github.com/vulsio/goval-dictionary v0.9.6-0.20240625074017-1da5dfb8b28a/go.mod h1:Qkcs63pRa/ZuOrQO0xPIhR/M6WVKOQEV60fkRJFkM60=
 github.com/xanzy/go-gitlab v0.102.0 h1:ExHuJ1OTQ2yt25zBMMj0G96ChBirGYv8U7HyUiYkZ+4=
diff --git a/gost/arch.go b/gost/arch.go
index adef4d1..ed094cb 100644
--- a/gost/arch.go
+++ b/gost/arch.go
@@ -5,15 +5,16 @@ package gost
 
 import (
 	"encoding/json"
-	"io"
-	"net/http"
-	"time"
+	"fmt"
 
-	"github.com/future-architect/vuls/logging"
-	"github.com/future-architect/vuls/models"
-	"github.com/hashicorp/go-version"
+	version "github.com/MaineK00n/go-pacman-version"
 	"golang.org/x/exp/maps"
 	"golang.org/x/xerrors"
+
+	"github.com/future-architect/vuls/logging"
+	"github.com/future-architect/vuls/models"
+	"github.com/future-architect/vuls/util"
+	gostmodels "github.com/vulsio/gost/models"
 )
 
 // Arch is Gost client
@@ -21,144 +22,156 @@ type Arch struct {
 	Base
 }
 
-// ArchIssue is struct of Arch Linux security issue
-type ArchIssue struct {
-	Name string
-
-	// Contains list of package names
-	Packages []string
-	Status   string
-	Severity string
-	Type     string
-
-	// Vulnerable version.
-	Affected string
-
-	// Fixed version. May be empty.
-	Fixed  string
-	Ticket string
-
-	// Contains list of CVEs
-	Issues     []string
-	Advisories []string
-}
-
-func (arch Arch) FetchAllIssues() ([]ArchIssue, error) {
-	client := &http.Client{Timeout: 2 * 60 * time.Second}
-	r, err := client.Get("https://security.archlinux.org/issues/all.json")
+func (arch Arch) DetectCVEs(r *models.ScanResult, _ bool) (nCVEs int, err error) {
+	fixedCVEs, err := arch.detectCVEsWithFixState(r, true)
 	if err != nil {
-		return nil, xerrors.Errorf("Failed to fetch files. err: %w", err)
+		return 0, xerrors.Errorf("Failed to detect fixed CVEs. err: %w", err)
 	}
-	defer r.Body.Close()
 
-	body, err := io.ReadAll(r.Body)
+	unfixedCVEs, err := arch.detectCVEsWithFixState(r, false)
 	if err != nil {
-		return nil, xerrors.Errorf("Failed to read response body. err: %w", err)
-	}
-
-	var archIssues []ArchIssue
-	if err := json.Unmarshal(body, &archIssues); err != nil {
-		return nil, xerrors.Errorf("Failed to unmarshal. err: %w", err)
+		return 0, xerrors.Errorf("Failed to detect unfixed CVEs. err: %w", err)
 	}
 
-	return archIssues, nil
+	return len(unique(append(fixedCVEs, unfixedCVEs...))), nil
 }
 
-func (arch Arch) DetectCVEs(r *models.ScanResult, _ bool) (nCVEs int, err error) {
-	detects := map[string]cveContent{}
-
-	archIssues, _ := arch.FetchAllIssues()
-	for _, issue := range archIssues {
-		for _, pkgName := range issue.Packages {
-			if _, ok := r.Packages[pkgName]; ok {
-				pkgVer := r.Packages[pkgName].Version + "-" + r.Packages[pkgName].Release
-				for _, content := range arch.detect(issue, pkgName, pkgVer) {
-					c, ok := detects[content.cveContent.CveID]
-					if ok {
-						content.fixStatuses = append(content.fixStatuses, c.fixStatuses...)
-					}
-					detects[content.cveContent.CveID] = content
+func (arch Arch) detectCVEsWithFixState(r *models.ScanResult, fixed bool) ([]string, error) {
+	detects := map[string]gostmodels.ArchADV{}
+	if arch.driver == nil {
+		urlPrefix, err := util.URLPathJoin(arch.baseURL, "arch", "pkgs")
+		if err != nil {
+			return nil, xerrors.Errorf("Failed to join URLPath. err: %w", err)
+		}
+		s := "fixed-advs"
+		if !fixed {
+			s = "unfixed-advs"
+		}
+		responses, err := getCvesWithFixStateViaHTTP(r, urlPrefix, s)
+		if err != nil {
+			return nil, xerrors.Errorf("Failed to get Advisories via HTTP. err: %w", err)
+		}
+
+		for _, res := range responses {
+			if res.request.isSrcPack {
+				continue
+			}
+
+			var as map[string]gostmodels.ArchADV
+			if err := json.Unmarshal([]byte(res.json), &as); err != nil {
+				return nil, xerrors.Errorf("Failed to unmarshal json. err: %w", err)
+			}
+			for _, content := range arch.detect(as, r.Packages[res.request.packName]) {
+				if base, ok := detects[content.Name]; ok {
+					content.Packages = append(content.Packages, base.Packages...)
+				}
+				detects[content.Name] = content
+			}
+		}
+	} else {
+		for _, p := range r.Packages {
+			as, err := func() (map[string]gostmodels.ArchADV, error) {
+				if !fixed {
+					return arch.driver.GetUnfixedAdvsArch(p.Name)
+				}
+				return arch.driver.GetFixedAdvsArch(p.Name)
+			}()
+			if err != nil {
+				return nil, xerrors.Errorf("Failed to get Advisories. package: %s, err: %w", p.Name, err)
+			}
+			for _, content := range arch.detect(as, p) {
+				if base, ok := detects[content.Name]; ok {
+					content.Packages = append(content.Packages, base.Packages...)
 				}
+				detects[content.Name] = content
 			}
 		}
 	}
 
+	cs := make(map[string]struct{})
 	for _, content := range detects {
-		v, ok := r.ScannedCves[content.cveContent.CveID]
-		if ok {
-			if v.CveContents == nil {
-				v.CveContents = models.NewCveContents(content.cveContent)
+		for _, i := range content.Issues {
+			v, ok := r.ScannedCves[i.Issue]
+			if ok {
+				v.DistroAdvisories.AppendIfMissing(&models.DistroAdvisory{
+					AdvisoryID: content.Name,
+					Severity:   content.Severity,
+				})
 			} else {
-				v.CveContents[models.ArchLinuxSecurityTracker] = []models.CveContent{content.cveContent}
+				v = models.VulnInfo{
+					CveID: i.Issue,
+					CveContents: models.NewCveContents(models.CveContent{
+						Type:       models.ArchLinuxSecurityTracker,
+						CveID:      i.Issue,
+						SourceLink: fmt.Sprintf("https://security.archlinux.org/%s", i.Issue),
+					}),
+					Confidences: models.Confidences{models.ArchLinuxSecurityTrackerMatch},
+					DistroAdvisories: models.DistroAdvisories{{
+						AdvisoryID: content.Name,
+						Severity:   content.Severity,
+					}},
+				}
 			}
-			v.Confidences.AppendIfMissing(models.ArchLinuxSecurityTrackerMatch)
-		} else {
-			v = models.VulnInfo{
-				CveID:       content.cveContent.CveID,
-				CveContents: models.NewCveContents(content.cveContent),
-				Confidences: models.Confidences{models.ArchLinuxSecurityTrackerMatch},
+
+			for _, s := range content.Packages {
+				v.AffectedPackages = v.AffectedPackages.Store(models.PackageFixStatus{
+					Name:        s.Name,
+					NotFixedYet: !fixed,
+					FixState:    content.Status,
+					FixedIn: func() string {
+						if content.Fixed != nil {
+							return *content.Fixed
+						}
+						return ""
+					}(),
+				})
 			}
-		}
 
-		for _, s := range content.fixStatuses {
-			v.AffectedPackages = v.AffectedPackages.Store(s)
+			r.ScannedCves[i.Issue] = v
+
+			cs[i.Issue] = struct{}{}
 		}
-		r.ScannedCves[content.cveContent.CveID] = v
 	}
-
-	return len(unique(maps.Keys(detects))), nil
+	return maps.Keys(cs), nil
 }
 
-func (arch Arch) detect(issue ArchIssue, pkgName, verStr string) []cveContent {
-	var contents []cveContent
-
-	for _, cveId := range issue.Issues {
-		c := cveContent{
-			cveContent: models.CveContent{
-				Type:  models.ArchLinuxSecurityTracker,
-				CveID: cveId,
-			},
-		}
-
-		vera, err := version.NewVersion(verStr)
-		if err != nil {
-			logging.Log.Debugf("Failed to parse version. version: %s, err: %v", verStr, err)
-			continue
-		}
-
-		if issue.Fixed != "" {
-			verb, err := version.NewVersion(issue.Fixed)
-			if err != nil {
-				logging.Log.Debugf("Failed to parse version. version: %s, err: %v", issue.Fixed, err)
-				continue
+func (arch Arch) detect(advs map[string]gostmodels.ArchADV, pkg models.Package) []gostmodels.ArchADV {
+	var as []gostmodels.ArchADV
+	for _, a := range advs {
+		switch a.Status {
+		case "Fixed":
+			if a.Fixed == nil {
+				logging.Log.Debugf("Failed to get fixed version in %s", a.Name)
+				break
 			}
 
-			if vera.LessThan(verb) {
-				c.fixStatuses = append(c.fixStatuses,
-					models.PackageFixStatus{
-						Name:    pkgName,
-						FixedIn: issue.Fixed,
-					})
-			}
-		} else {
-			verb, err := version.NewVersion(issue.Affected)
+			affected, err := arch.isGostDefAffected(fmt.Sprintf("%s-%s", pkg.Version, pkg.Release), *a.Fixed)
 			if err != nil {
-				logging.Log.Debugf("Failed to parse version. version: %s, err: %v", issue.Affected, err)
-				continue
+				logging.Log.Debugf("Failed to parse versions: %s, Ver: %s, Gost: %s", err, fmt.Sprintf("%s-%s", pkg.Version, pkg.Release), a.Affected)
+				break
 			}
-
-			if vera.LessThanOrEqual(verb) {
-				c.fixStatuses = append(c.fixStatuses,
-					models.PackageFixStatus{
-						Name: pkgName,
-					})
+			if affected {
+				a.Packages = []gostmodels.ArchPackage{{Name: pkg.Name}}
+				as = append(as, a)
 			}
-		}
-
-		if len(c.fixStatuses) > 0 {
-			contents = append(contents, c)
+		case "Vulnerable":
+			a.Packages = []gostmodels.ArchPackage{{Name: pkg.Name}}
+			as = append(as, a)
+		default:
+			logging.Log.Debugf("Failed to check vulnerable Advisory. err: unknown status: %s", a.Status)
 		}
 	}
+	return as
+}
 
-	return contents
+func (arch Arch) isGostDefAffected(installedVersion, gostVersion string) (bool, error) {
+	vera, err := version.NewVersion(installedVersion)
+	if err != nil {
+		return false, xerrors.Errorf("Failed to parse version. version: %s, err: %w", installedVersion, err)
+	}
+	verb, err := version.NewVersion(gostVersion)
+	if err != nil {
+		return false, xerrors.Errorf("Failed to parse version. version: %s, err: %w", gostVersion, err)
+	}
+	return vera.LessThan(verb), nil
 }
diff --git a/gost/arch_test.go b/gost/arch_test.go
new file mode 100644
index 0000000..27aca8f
--- /dev/null
+++ b/gost/arch_test.go
@@ -0,0 +1,40 @@
+//go:build !scanner
+// +build !scanner
+
+package gost
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/future-architect/vuls/models"
+	gostmodels "github.com/vulsio/gost/models"
+)
+
+func TestArch_detect(t *testing.T) {
+	type fields struct {
+		Base Base
+	}
+	type args struct {
+		advs map[string]gostmodels.ArchADV
+		pkg  models.Package
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   []gostmodels.ArchADV
+	}{
+		// TODO: Add test cases.
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			arch := Arch{
+				Base: tt.fields.Base,
+			}
+			if got := arch.detect(tt.args.advs, tt.args.pkg); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Arch.detect() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/models/vulninfos.go b/models/vulninfos.go
index 3bee991..3c33eca 100644
--- a/models/vulninfos.go
+++ b/models/vulninfos.go
@@ -1047,7 +1047,7 @@ var (
 	// RedHatAPIMatch ranking how confident the CVE-ID was detected correctly
 	RedHatAPIMatch = Confidence{100, RedHatAPIStr, 0}
 
-	// DebianSecurityTrackerMatch ranking how confident the CVE-ID was detected correctly
+	// ArchLinuxSecurityTrackerMatch ranking how confident the CVE-ID was detected correctly
 	ArchLinuxSecurityTrackerMatch = Confidence{100, ArchLinuxSecurityTrackerMatchStr, 0}
 
 	// DebianSecurityTrackerMatch ranking how confident the CVE-ID was detected correctly
diff --git a/oval/arch.go b/oval/arch.go
deleted file mode 100644
index 17e9f2a..0000000
--- a/oval/arch.go
+++ /dev/null
@@ -1,31 +0,0 @@
-//go:build !scanner
-// +build !scanner
-
-package oval
-
-import (
-	"github.com/future-architect/vuls/constant"
-	"github.com/future-architect/vuls/models"
-	ovaldb "github.com/vulsio/goval-dictionary/db"
-)
-
-// Arch is the interface for Arch OVAL.
-type Arch struct {
-	Base
-}
-
-// NewArch creates OVAL client for Arch
-func NewArch(driver ovaldb.DB, baseURL string) Arch {
-	return Arch{
-		Base{
-			driver:  driver,
-			baseURL: baseURL,
-			family:  constant.Arch,
-		},
-	}
-}
-
-// FillWithOval returns scan result after updating CVE info by OVAL
-func (o Arch) FillWithOval(_ *models.ScanResult) (nCVEs int, err error) {
-	return 0, nil
-}
diff --git a/oval/util.go b/oval/util.go
index 5b6ae37..cded582 100644
--- a/oval/util.go
+++ b/oval/util.go
@@ -603,8 +603,6 @@ func NewOVALClient(family string, cnf config.GovalDictConf, o logging.LogOpts) (
 	}
 
 	switch family {
-	case constant.Arch:
-		return NewArch(driver, cnf.GetURL()), nil
 	case constant.Debian, constant.Raspbian:
 		return NewDebian(driver, cnf.GetURL()), nil
 	case constant.Ubuntu:
@@ -633,7 +631,7 @@ func NewOVALClient(family string, cnf config.GovalDictConf, o logging.LogOpts) (
 		return NewAmazon(driver, cnf.GetURL()), nil
 	case constant.Fedora:
 		return NewFedora(driver, cnf.GetURL()), nil
-	case constant.FreeBSD, constant.Windows:
+	case constant.Arch, constant.FreeBSD, constant.Windows:
 		return NewPseudo(family), nil
 	case constant.ServerTypePseudo:
 		return NewPseudo(family), nil
@@ -649,8 +647,6 @@ func NewOVALClient(family string, cnf config.GovalDictConf, o logging.LogOpts) (
 // For example, CentOS/Alma/Rocky uses Red Hat's OVAL, so return 'redhat'
 func GetFamilyInOval(familyInScanResult string) (string, error) {
 	switch familyInScanResult {
-	case constant.Arch:
-		return constant.Arch, nil
 	case constant.Debian, constant.Raspbian:
 		return constant.Debian, nil
 	case constant.Ubuntu:
@@ -673,7 +669,7 @@ func GetFamilyInOval(familyInScanResult string) (string, error) {
 		return constant.Alpine, nil
 	case constant.Amazon:
 		return constant.Amazon, nil
-	case constant.FreeBSD, constant.Windows:
+	case constant.Arch, constant.FreeBSD, constant.Windows:
 		return "", nil
 	case constant.ServerTypePseudo:
 		return "", nil
diff --git a/scanner/arch.go b/scanner/arch.go
index 62ae527..6a71cc0 100644
--- a/scanner/arch.go
+++ b/scanner/arch.go
@@ -2,14 +2,17 @@ package scanner
 
 import (
 	"bufio"
+	"fmt"
+	"net/textproto"
 	"strings"
 
+	"golang.org/x/xerrors"
+
 	"github.com/future-architect/vuls/config"
 	"github.com/future-architect/vuls/constant"
 	"github.com/future-architect/vuls/logging"
 	"github.com/future-architect/vuls/models"
 	"github.com/future-architect/vuls/util"
-	"golang.org/x/xerrors"
 )
 
 // inherit OsTypeInterface
@@ -71,6 +74,12 @@ func (o *arch) checkIfSudoNoPasswd() error {
 		"stat /proc/1/exe",
 		"ls -l /proc/1/exe",
 		"cat /proc/1/maps",
+		"lsof -i -P -n",
+		"pacman -Qi",
+		"pacman -Qu",
+	}
+	if !o.getServerInfo().Mode.IsOffline() {
+		cmds = append(cmds, "pacman -Sy")
 	}
 
 	for _, cmd := range cmds {
@@ -87,28 +96,27 @@ func (o *arch) checkIfSudoNoPasswd() error {
 }
 
 func (o *arch) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
-	packs := models.Packages{}
-	pkgInfos := strings.Split(stdout, "\n\n")
-	for _, pkgInfo := range pkgInfos {
+	packs := make(models.Packages)
+	for _, pkgInfo := range strings.Split(stdout, "\n\n") {
 		if len(strings.TrimSpace(pkgInfo)) == 0 {
 			continue
 		}
 
-		lines := strings.Split(pkgInfo, "\n")
+		header, err := textproto.NewReader(bufio.NewReader(strings.NewReader(fmt.Sprintf("%s\n\n", pkgInfo)))).ReadMIMEHeader()
+		if err != nil {
+			return nil, nil, xerrors.Errorf("Failed to read package info. err: %w", err)
+		}
+
 		var name, version, release, arch string
-		for _, line := range lines {
-			columns := strings.Split(line, ":")
-			leftColumn := strings.TrimSpace(columns[0])
-			rightColumn := strings.TrimSpace(strings.Join(columns[1:], ":"))
-			switch leftColumn {
+		for k := range header {
+			switch strings.TrimSpace(k) {
 			case "Name":
-				name = rightColumn
+				name = header.Get(k)
 			case "Version":
-				values := strings.Split(rightColumn, "-")
-				version = values[0]
-				release = values[1]
+				version, release, _ = strings.Cut(header.Get(k), "-")
 			case "Architecture":
-				arch = rightColumn
+				arch = header.Get(k)
+			default:
 			}
 		}
 
@@ -154,12 +162,21 @@ func (o *arch) scanPackages() error {
 		Version: version,
 	}
 
-	installed, err := o.scanInstalledPackages()
+	o.Packages, err = o.scanInstalledPackages()
 	if err != nil {
 		o.log.Errorf("Failed to scan installed packages: %s", err)
 		return err
 	}
 
+	if !o.getServerInfo().Mode.IsOffline() {
+		if err := o.updatePackageDB(); err != nil {
+			err = xerrors.Errorf("Failed to update package DB: %w", err)
+			o.log.Warnf("err: %+v", err)
+			o.warns = append(o.warns, err)
+			// Only warning this error
+		}
+	}
+
 	updatable, err := o.scanUpdatablePackages()
 	if err != nil {
 		err = xerrors.Errorf("Failed to scan updatable packages: %w", err)
@@ -167,10 +184,8 @@ func (o *arch) scanPackages() error {
 		o.warns = append(o.warns, err)
 		// Only warning this error
 	} else {
-		installed.MergeNewVersion(updatable)
+		o.Packages.MergeNewVersion(updatable)
 	}
-
-	o.Packages = installed
 	return nil
 }
 
@@ -180,60 +195,53 @@ func (o *arch) scanInstalledPackages() (models.Packages, error) {
 	if !r.isSuccess() {
 		return nil, xerrors.Errorf("Failed to SSH: %s", r)
 	}
-	pkgs, _, _ := o.parseInstalledPackages(r.Stdout)
+	pkgs, _, err := o.parseInstalledPackages(r.Stdout)
+	if err != nil {
+		return nil, xerrors.Errorf("Failed to parse installed packages. err: %w", err)
+	}
 
 	return pkgs, nil
 }
 
-func (o *arch) scanUpdatablePackages() (models.Packages, error) {
-	listOutdateCmd := `TMPPATH="${TMPDIR:-/tmp}/vuls"
-DBPATH="$(pacman-conf DBPath)"
-
-mkdir -p "$TMPPATH"
-ln -s "$DBPATH/local" "$TMPPATH" &>/dev/null
-fakeroot -- pacman -Sy --dbpath "$TMPPATH" --logfile /dev/null &>/dev/null
-pacman -Qu --dbpath "$TMPPATH" 2>/dev/null
-`
-	cmd := util.PrependProxyEnv(listOutdateCmd)
+func (o *arch) updatePackageDB() error {
+	cmd := util.PrependProxyEnv("pacman -Sy")
 	r := o.exec(cmd, noSudo)
 	if !r.isSuccess() {
-		return nil, xerrors.Errorf("Failed to SSH: %s", r)
+		return xerrors.Errorf("Failed to SSH: %s", r)
 	}
-	pkgs, _ := o.parseOutdatedPackages(r.Stdout)
-
-	unlinkCmd := `TMPPATH="${TMPDIR:-/tmp}/vuls"
-rm -r "$TMPPATH"`
+	return nil
+}
 
-	cmd = util.PrependProxyEnv(unlinkCmd)
-	r = o.exec(cmd, noSudo)
+func (o *arch) scanUpdatablePackages() (models.Packages, error) {
+	cmd := util.PrependProxyEnv("pacman -Qu")
+	r := o.exec(cmd, noSudo)
 	if !r.isSuccess() {
-		err := xerrors.Errorf("Failed to SSH: %s", r)
-		o.log.Warnf("err: %+v", err)
-		o.warns = append(o.warns, err)
-		// Only warning this error
+		return nil, xerrors.Errorf("Failed to SSH: %s", r)
+	}
+	pkgs, err := o.parseOutdatedPackages(r.Stdout)
+	if err != nil {
+		return nil, xerrors.Errorf("Failed to parse outdated packages. err: %w", err)
 	}
 
 	return pkgs, nil
 }
 
 func (o *arch) parseOutdatedPackages(stdout string) (models.Packages, error) {
-	packs := models.Packages{}
+	packs := make(models.Packages)
 	scanner := bufio.NewScanner(strings.NewReader(stdout))
 	for scanner.Scan() {
 		line := scanner.Text()
-		if !strings.Contains(line, "->") {
-			continue
+		lhs, rhs, ok := strings.Cut(line, "->")
+		if !ok {
+			return nil, xerrors.Errorf("unexpected `pacman -Qu` line format. expected: %q, actual: %q", "<package name> <install version> -> <updatable version>", line)
 		}
-		ss := strings.Fields(line)
-		name := ss[0]
-		fullVersionInfo := ss[3]
-
-		versionAndRelease := strings.Split(fullVersionInfo, "-")
 
+		name := strings.Fields(lhs)[0]
+		version, release, _ := strings.Cut(strings.TrimSpace(rhs), "-")
 		packs[name] = models.Package{
 			Name:       name,
-			NewVersion: versionAndRelease[0],
-			NewRelease: versionAndRelease[1],
+			NewVersion: version,
+			NewRelease: release,
 		}
 	}
 	return packs, nil
diff --git a/scanner/arch_test.go b/scanner/arch_test.go
new file mode 100644
index 0000000..135b497
--- /dev/null
+++ b/scanner/arch_test.go
@@ -0,0 +1,184 @@
+package scanner
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/future-architect/vuls/config"
+	"github.com/future-architect/vuls/models"
+)
+
+func Test_arch_parseInstalledPackages(t *testing.T) {
+	type args struct {
+		stdout string
+	}
+	tests := []struct {
+		name     string
+		args     args
+		wantBins models.Packages
+		wantSrcs models.SrcPackages
+		wantErr  bool
+	}{
+		{
+			name: "happy",
+			args: args{
+				stdout: `Name            : acl
+Version         : 2.3.2-1
+Description     : Access control list utilities, libraries and headers
+Architecture    : x86_64
+URL             : https://savannah.nongnu.org/projects/acl
+Licenses        : LGPL
+Groups          : None
+Provides        : xfsacl  libacl.so=1-64
+Depends On      : glibc
+Optional Deps   : None
+Required By     : coreutils  gettext  libarchive  sed  shadow  systemd  tar
+Optional For    : None
+Conflicts With  : xfsacl
+Replaces        : xfsacl
+Installed Size  : 329.98 KiB
+Packager        : Christian Hesse <[email protected]>
+Build Date      : Wed Jan 24 08:57:20 2024
+Install Date    : Sun Aug 25 00:03:46 2024
+Install Reason  : Installed as a dependency for another package
+Install Script  : No
+Validated By    : Signature
+
+Name            : archlinux-keyring
+Version         : 20240709-1
+Description     : Arch Linux PGP keyring
+Architecture    : any
+URL             : https://gitlab.archlinux.org/archlinux/archlinux-keyring/
+Licenses        : GPL-3.0-or-later
+Groups          : None
+Provides        : None
+Depends On      : pacman
+Optional Deps   : None
+Required By     : base
+Optional For    : None
+Conflicts With  : None
+Replaces        : None
+Installed Size  : 1709.20 KiB
+Packager        : Christian Hesse <[email protected]>
+Build Date      : Tue Jul 9 21:31:37 2024
+Install Date    : Sun Aug 25 00:03:47 2024
+Install Reason  : Installed as a dependency for another package
+Install Script  : Yes
+Validated By    : Signature
+
+Name            : openssl
+Version         : 3.3.1-1
+Description     : The Open Source toolkit for Secure Sockets Layer and Transport Layer Security
+Architecture    : x86_64
+URL             : https://www.openssl.org
+Licenses        : Apache-2.0
+Groups          : None
+Provides        : libcrypto.so=3-64  libssl.so=3-64
+Depends On      : glibc
+Optional Deps   : ca-certificates [installed]
+                  perl
+Required By     : coreutils  cryptsetup  curl  kmod  krb5  libarchive  libevent  libsasl  libssh2  openssh  systemd  tpm2-tss
+Optional For    : None
+Conflicts With  : None
+Replaces        : openssl-perl  openssl-doc
+Installed Size  : 10.95 MiB
+Packager        : Pierre Schmitz <[email protected]>
+Build Date      : Tue Jun  4 19:29:08 2024
+Install Date    : Sun Aug 25 00:03:46 2024
+Install Reason  : Installed as a dependency for another package
+Install Script  : No
+Validated By    : Signature
+
+`,
+			},
+			wantBins: models.Packages{
+				"acl": {
+					Name:    "acl",
+					Version: "2.3.2",
+					Release: "1",
+					Arch:    "x86_64",
+				},
+				"archlinux-keyring": {
+					Name:    "archlinux-keyring",
+					Version: "20240709",
+					Release: "1",
+					Arch:    "any",
+				},
+				"openssl": {
+					Name:    "openssl",
+					Version: "3.3.1",
+					Release: "1",
+					Arch:    "x86_64",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			o := newArch(config.ServerInfo{})
+			gotBins, gotSrcs, err := o.parseInstalledPackages(tt.args.stdout)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("arch.parseInstalledPackages() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(gotBins, tt.wantBins) {
+				t.Errorf("arch.parseInstalledPackages() gotBins = %v, wantBins %v", gotBins, tt.wantBins)
+			}
+			if !reflect.DeepEqual(gotSrcs, tt.wantSrcs) {
+				t.Errorf("arch.parseInstalledPackages() gotSrcs = %v, wantSrcs %v", gotSrcs, tt.wantSrcs)
+			}
+		})
+	}
+}
+
+func Test_arch_parseOutdatedPackages(t *testing.T) {
+	type args struct {
+		stdout string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    models.Packages
+		wantErr bool
+	}{
+		{
+			name: "happy",
+			args: args{
+				stdout: `bash 5.2.032-1 -> 5.2.032-2
+ca-certificates-mozilla 3.103-1 -> 3.104-1
+device-mapper 2.03.25-2 -> 2.03.26-1
+`,
+			},
+			want: models.Packages{
+				"bash": {
+					Name:       "bash",
+					NewVersion: "5.2.032",
+					NewRelease: "2",
+				},
+				"ca-certificates-mozilla": {
+					Name:       "ca-certificates-mozilla",
+					NewVersion: "3.104",
+					NewRelease: "1",
+				},
+				"device-mapper": {
+					Name:       "device-mapper",
+					NewVersion: "2.03.26",
+					NewRelease: "1",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			o := newArch(config.ServerInfo{})
+			got, err := o.parseOutdatedPackages(tt.args.stdout)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("arch.parseOutdatedPackages() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("arch.parseOutdatedPackages() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 56a5892..82ae7d9 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -763,11 +763,6 @@ func (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
 		}
 	}
 
-	if itsMe, osType := detectArch(c); itsMe {
-		logging.Log.Debugf("Arch based Linux. Host: %s:%s", c.Host, c.Port)
-		return osType
-	}
-
 	if itsMe, osType := detectWindows(c); itsMe {
 		logging.Log.Debugf("Windows. Host: %s:%s", c.Host, c.Port)
 		return osType
@@ -798,6 +793,11 @@ func (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
 		return osType
 	}
 
+	if itsMe, osType := detectArch(c); itsMe {
+		logging.Log.Debugf("Arch based Linux. Host: %s:%s", c.Host, c.Port)
+		return osType
+	}
+
 	if itsMe, osType := detectMacOS(c); itsMe {
 		logging.Log.Debugf("MacOS. Host: %s:%s", c.Host, c.Port)
 		return osType
FROM archlinux

RUN pacman -Syu --noconfirm && \
    pacman -S --noconfirm openssh
RUN mkdir -p /var/run/sshd

RUN sed -i 's/#\?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

COPY .ssh/id_rsa.pub /root/authorized_keys
RUN mkdir -p ~/.ssh && \
    mv ~/authorized_keys ~/.ssh/authorized_keys && \
    chmod 0600 ~/.ssh/authorized_keys

RUN ssh-keygen -A

EXPOSE 22

## AVG-2851
RUN pacman -U --noconfirm https://archive.archlinux.org/packages/x/xz/xz-5.6.0-1-x86_64.pkg.tar.zst

## AVG-2847
RUN pacman -S --noconfirm minizip

CMD ["/usr/sbin/sshd", "-D"]
$ vuls scan
...
[Sep  4 02:18:41]  INFO [localhost] Validating config...
[Sep  4 02:18:41]  INFO [localhost] Detecting Server/Container OS... 
[Sep  4 02:18:41]  INFO [localhost] Detecting OS of servers... 
[Sep  4 02:18:42]  INFO [localhost] (1/1) Detected: docker: arch 
[Sep  4 02:18:42]  INFO [localhost] Detecting OS of containers... 
[Sep  4 02:18:42]  INFO [localhost] Checking Scan Modes... 
[Sep  4 02:18:42]  INFO [localhost] Detecting Platforms... 
[Sep  4 02:18:44]  INFO [localhost] (1/1) docker is running on other
[Sep  4 02:18:44]  INFO [docker] Scanning OS pkg in fast mode


Scan Summary
================
docker	arch	122 installed, 3 updatable





To view the detail, vuls tui is useful.
To send a report, run vuls report -h.

$ vuls report
...
[Sep  4 02:20:44]  INFO [localhost] docker: 3 CVEs are detected with gost
[Sep  4 02:20:44]  INFO [localhost] docker: 0 CVEs are detected with CPE
[Sep  4 02:20:44]  INFO [localhost] docker: 0 PoC are detected
[Sep  4 02:20:44]  INFO [localhost] docker: 0 exploits are detected
[Sep  4 02:20:44]  INFO [localhost] docker: Known Exploited Vulnerabilities are detected for 0 CVEs
[Sep  4 02:20:44]  INFO [localhost] docker: Cyber Threat Intelligences are detected for 0 CVEs
[Sep  4 02:20:44]  INFO [localhost] docker: total 3 CVEs detected
[Sep  4 02:20:44]  INFO [localhost] docker: 0 CVEs filtered by --confidence-over=80
docker (arch)
=============
Total: 3 (Critical:2 High:0 Medium:1 Low:0 ?:0)
1/3 Fixed, 0 poc, 0 exploits, cisa: 0, uscert: 0, jpcert: 0 alerts
122 installed

+----------------+------+--------+-----+-----------+---------+----------+
|     CVE-ID     | CVSS | ATTACK | POC |   ALERT   |  FIXED  | PACKAGES |
+----------------+------+--------+-----+-----------+---------+----------+
| CVE-2023-45853 | 10.0 |  AV:L  |     |           | unfixed | minizip  |
+----------------+------+--------+-----+-----------+---------+----------+
| CVE-2024-3094  | 10.0 |  AV:N  |     |           |   fixed | xz       |
+----------------+------+--------+-----+-----------+---------+----------+
| CVE-2022-2068  |  6.9 |  AV:L  |     |           | unfixed | openssl  |
+----------------+------+--------+-----+-----------+---------+----------+

@MaineK00n MaineK00n changed the title [Feature] Support for Arch Linux feat(os): support for Arch Linux Sep 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Arch Linux support
3 participants