From d5c098d5e2841cefb21e53d2db01085d5cbbf12f Mon Sep 17 00:00:00 2001 From: Ziya Genc Date: Mon, 12 Aug 2024 00:14:35 +0200 Subject: [PATCH 1/3] [ADD] Support scanning Arch Linux packages --- config/os.go | 4 + constant/constant.go | 3 + scanner/arch.go | 182 +++++++++++++++++++++++++++++++++++++++++++ scanner/scanner.go | 7 ++ 4 files changed, 196 insertions(+) create mode 100644 scanner/arch.go diff --git a/config/os.go b/config/os.go index dc59f00cf1..24a26816e0 100644 --- a/config/os.go +++ b/config/os.go @@ -48,6 +48,10 @@ func GetEOL(family, release string) (eol EOL, found bool) { "2027": {StandardSupportUntil: time.Date(2031, 6, 30, 23, 59, 59, 0, time.UTC)}, "2029": {StandardSupportUntil: time.Date(2033, 6, 30, 23, 59, 59, 0, time.UTC)}, }[getAmazonLinuxVersion(release)] + case constant.Arch: + // Arch Linux uses a rolling release model. + // https://wiki.archlinux.org/title/Arch_Linux + eol, found = EOL{Ended: false}, true case constant.RedHat: // https://access.redhat.com/support/policy/updates/errata eol, found = map[string]EOL{ diff --git a/constant/constant.go b/constant/constant.go index 848bf517f4..f08b767fd3 100644 --- a/constant/constant.go +++ b/constant/constant.go @@ -5,6 +5,9 @@ package constant // Define them in the each package. const ( + // Arch is + Arch = "arch" + // RedHat is RedHat = "redhat" diff --git a/scanner/arch.go b/scanner/arch.go new file mode 100644 index 0000000000..62d07a68f6 --- /dev/null +++ b/scanner/arch.go @@ -0,0 +1,182 @@ +package scanner + +import ( + "strings" + + "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 +type arch struct { + base +} + +// NewArch constructor +func newArch(c config.ServerInfo) *arch { + d := &arch{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + } + d.log = logging.NewNormalLogger() + d.setServerInfo(c) + return d +} + +func detectArch(c config.ServerInfo) (bool, osTypeInterface) { + if r := exec(c, "ls /etc/arch-release", noSudo); !r.isSuccess() { + logging.Log.Debugf("Not Arch Linux. %s", r) + return false, nil + } + + arch := newArch(c) + arch.setDistro(constant.Arch, "") + return true, arch +} + +// lsof is needed to find open files. +// We will use it to determine which packages need restart. +func (o *arch) checkDeps() error { + deps := []string{"lsof"} + for _, cmd := range deps { + if r := o.exec(cmd, noSudo); !r.isSuccess() { + o.log.Warnf("%s is not installed", cmd) + o.warns = append(o.warns, r.Error) + } + } + o.log.Infof("Dependencies ... Pass") + return nil +} + +func (o *arch) checkScanMode() error { + return nil +} + +func (o *arch) checkIfSudoNoPasswd() error { + if o.getServerInfo().Mode.IsFast() { + o.log.Infof("sudo ... No need") + return nil + } + + cmds := []string{ + "stat /proc/1/exe", + "ls -l /proc/1/exe", + "cat /proc/1/maps", + } + + for _, cmd := range cmds { + cmd = util.PrependProxyEnv(cmd) + o.log.Infof("Checking... sudo %s", cmd) + if r := o.exec(cmd, sudo); !r.isSuccess() { + o.log.Errorf("sudo error on %s", r) + return xerrors.Errorf("Failed to sudo: %s", r) + } + } + + o.log.Infof("Sudo... Pass") + return nil +} + +func (o *arch) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) { + packs := models.Packages{} + pkgInfos := strings.Split(stdout, "\n\n") + for _, pkgInfo := range pkgInfos { + if len(strings.TrimSpace(pkgInfo)) == 0 { + continue + } + + lines := strings.Split(pkgInfo, "\n") + var name, version, release, arch string + for _, line := range lines { + columns := strings.Split(line, ":") + if len(columns) != 2 { + continue + } + + leftColumn := strings.TrimSpace(columns[0]) + rightColumn := strings.TrimSpace(columns[1]) + switch leftColumn { + case "Name": + name = rightColumn + case "Version": + values := strings.Split(rightColumn, "-") + if len(values) != 2 { + continue + } + version = values[0] + release = values[1] + case "Architecture": + arch = rightColumn + } + } + + packs[name] = models.Package{ + Name: name, + Version: version, + Release: release, + Arch: arch, + } + } + return packs, nil, nil +} + +// TODO: Collect package names that needs reboot +func (o *arch) postScan() error { + return nil +} + +func (o *arch) preCure() error { + if err := o.detectIPAddr(); err != nil { + o.log.Warnf("Failed to detect IP addresses: %s", err) + o.warns = append(o.warns, err) + } + // Ignore this error as it just failed to detect the IP addresses + return nil +} + +func (o *arch) detectIPAddr() (err error) { + o.ServerInfo.IPv4Addrs, o.ServerInfo.IPv6Addrs, err = o.ip() + return err +} + +func (o *arch) scanPackages() error { + o.log.Infof("Scanning OS pkg in %s", o.getServerInfo().Mode) + // collect the running kernel information + release, version, err := o.runningKernel() + if err != nil { + o.log.Errorf("Failed to scan the running kernel version: %s", err) + return err + } + o.Kernel = models.Kernel{ + Release: release, + Version: version, + } + + packs, err := o.scanInstalledPackages() + if err != nil { + o.log.Errorf("Failed to scan installed packages: %s", err) + return err + } + o.Packages = packs + + return nil +} + +func (o *arch) scanInstalledPackages() (models.Packages, error) { + cmd := util.PrependProxyEnv("pacman -Qi") + r := o.exec(cmd, noSudo) + if !r.isSuccess() { + return nil, xerrors.Errorf("Failed to SSH: %s", r) + } + pkgs, _, _ := o.parseInstalledPackages(r.Stdout) + + return pkgs, nil +} diff --git a/scanner/scanner.go b/scanner/scanner.go index eb233a5624..56a589218c 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -264,6 +264,8 @@ func ParseInstalledPkgs(distro config.Distro, kernel models.Kernel, pkgList stri var osType osTypeInterface switch distro.Family { + case constant.Arch: + osType = &arch{base: base} case constant.Debian, constant.Ubuntu, constant.Raspbian: osType = &debian{base: base} case constant.RedHat: @@ -761,6 +763,11 @@ 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 From f9ce01c41bb83f0a9e3081c3a2f83031ee18d83e Mon Sep 17 00:00:00 2001 From: Ziya Genc Date: Mon, 19 Aug 2024 06:32:34 +0200 Subject: [PATCH 2/3] [ADD] Support detecting vulnerable Arch Linux packages --- detector/detector.go | 8 ++- gost/arch.go | 164 ++++++++++++++++++++++++++++++++++++++++++ gost/gost.go | 2 + models/cvecontents.go | 3 + models/vulninfos.go | 6 ++ oval/arch.go | 31 ++++++++ oval/util.go | 4 ++ scanner/arch.go | 79 +++++++++++++++++--- 8 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 gost/arch.go create mode 100644 oval/arch.go diff --git a/detector/detector.go b/detector/detector.go index a2f8aa3c24..1e5cceafd2 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -373,6 +373,8 @@ 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: return true default: @@ -534,7 +536,7 @@ func detectPkgsCvesWithOval(cnf config.GovalDictConf, r *models.ScanResult, logO }() switch r.Family { - case constant.Debian, constant.Raspbian, constant.Ubuntu: + case constant.Arch, 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 @@ -581,7 +583,7 @@ func detectPkgsCvesWithGost(cnf config.GostConf, r *models.ScanResult, logOpts l nCVEs, err := client.DetectCVEs(r, true) if err != nil { switch r.Family { - case constant.Debian, constant.Raspbian, constant.Ubuntu, constant.Windows: + case constant.Arch, constant.Debian, constant.Raspbian, constant.Ubuntu, constant.Windows: return xerrors.Errorf("Failed to detect CVEs with gost: %w", err) default: return xerrors.Errorf("Failed to detect unfixed CVEs with gost: %w", err) @@ -589,7 +591,7 @@ func detectPkgsCvesWithGost(cnf config.GostConf, r *models.ScanResult, logOpts l } switch r.Family { - case constant.Debian, constant.Raspbian, constant.Ubuntu, constant.Windows: + case constant.Arch, constant.Debian, constant.Raspbian, constant.Ubuntu, constant.Windows: logging.Log.Infof("%s: %d CVEs are detected with gost", r.FormatServerName(), nCVEs) default: logging.Log.Infof("%s: %d unfixed CVEs are detected with gost", r.FormatServerName(), nCVEs) diff --git a/gost/arch.go b/gost/arch.go new file mode 100644 index 0000000000..adef4d1c35 --- /dev/null +++ b/gost/arch.go @@ -0,0 +1,164 @@ +//go:build !scanner +// +build !scanner + +package gost + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "github.com/future-architect/vuls/logging" + "github.com/future-architect/vuls/models" + "github.com/hashicorp/go-version" + "golang.org/x/exp/maps" + "golang.org/x/xerrors" +) + +// Arch is Gost client +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") + if err != nil { + return nil, xerrors.Errorf("Failed to fetch files. err: %w", err) + } + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + 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 archIssues, 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 + } + } + } + } + + for _, content := range detects { + v, ok := r.ScannedCves[content.cveContent.CveID] + if ok { + if v.CveContents == nil { + v.CveContents = models.NewCveContents(content.cveContent) + } else { + v.CveContents[models.ArchLinuxSecurityTracker] = []models.CveContent{content.cveContent} + } + 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.fixStatuses { + v.AffectedPackages = v.AffectedPackages.Store(s) + } + r.ScannedCves[content.cveContent.CveID] = v + } + + return len(unique(maps.Keys(detects))), 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 + } + + if vera.LessThan(verb) { + c.fixStatuses = append(c.fixStatuses, + models.PackageFixStatus{ + Name: pkgName, + FixedIn: issue.Fixed, + }) + } + } else { + verb, err := version.NewVersion(issue.Affected) + if err != nil { + logging.Log.Debugf("Failed to parse version. version: %s, err: %v", issue.Affected, err) + continue + } + + if vera.LessThanOrEqual(verb) { + c.fixStatuses = append(c.fixStatuses, + models.PackageFixStatus{ + Name: pkgName, + }) + } + } + + if len(c.fixStatuses) > 0 { + contents = append(contents, c) + } + } + + return contents +} diff --git a/gost/gost.go b/gost/gost.go index 9e1f640c8b..d5fbf90163 100644 --- a/gost/gost.go +++ b/gost/gost.go @@ -67,6 +67,8 @@ func NewGostClient(cnf config.GostConf, family string, o logging.LogOpts) (Clien base := Base{driver: db, baseURL: cnf.GetURL()} switch family { + case constant.Arch: + return Arch{base}, nil case constant.Debian, constant.Raspbian: return Debian{base}, nil case constant.Ubuntu: diff --git a/models/cvecontents.go b/models/cvecontents.go index a9108436bd..1db74adc7e 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -442,6 +442,9 @@ const ( // RedHatAPI is RedHat RedHatAPI CveContentType = "redhat_api" + // ArchLinuxSecurityTracker is Arch Linux security tracker + ArchLinuxSecurityTracker CveContentType = "arch_linux_security_tracker" + // DebianSecurityTracker is Debian Security tracker DebianSecurityTracker CveContentType = "debian_security_tracker" diff --git a/models/vulninfos.go b/models/vulninfos.go index 3e85e81149..3bee99138a 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -1003,6 +1003,9 @@ const ( // RedHatAPIStr is : RedHatAPIStr = "RedHatAPIMatch" + // ArchLinuxSecurityTrackerMatchStr : + ArchLinuxSecurityTrackerMatchStr = "ArchLinuxSecurityTrackerMatch" + // DebianSecurityTrackerMatchStr : DebianSecurityTrackerMatchStr = "DebianSecurityTrackerMatch" @@ -1044,6 +1047,9 @@ 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 = Confidence{100, ArchLinuxSecurityTrackerMatchStr, 0} + // DebianSecurityTrackerMatch ranking how confident the CVE-ID was detected correctly DebianSecurityTrackerMatch = Confidence{100, DebianSecurityTrackerMatchStr, 0} diff --git a/oval/arch.go b/oval/arch.go new file mode 100644 index 0000000000..17e9f2a362 --- /dev/null +++ b/oval/arch.go @@ -0,0 +1,31 @@ +//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 54a9d93b88..5b6ae37baf 100644 --- a/oval/util.go +++ b/oval/util.go @@ -603,6 +603,8 @@ 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: @@ -647,6 +649,8 @@ 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: diff --git a/scanner/arch.go b/scanner/arch.go index 62d07a68f6..3758e08724 100644 --- a/scanner/arch.go +++ b/scanner/arch.go @@ -1,6 +1,7 @@ package scanner import ( + "bufio" "strings" "github.com/future-architect/vuls/config" @@ -97,20 +98,13 @@ func (o *arch) parseInstalledPackages(stdout string) (models.Packages, models.Sr var name, version, release, arch string for _, line := range lines { columns := strings.Split(line, ":") - if len(columns) != 2 { - continue - } - leftColumn := strings.TrimSpace(columns[0]) - rightColumn := strings.TrimSpace(columns[1]) + rightColumn := strings.TrimSpace(strings.Join(columns[1:], ":")) switch leftColumn { case "Name": name = rightColumn case "Version": values := strings.Split(rightColumn, "-") - if len(values) != 2 { - continue - } version = values[0] release = values[1] case "Architecture": @@ -160,13 +154,23 @@ func (o *arch) scanPackages() error { Version: version, } - packs, err := o.scanInstalledPackages() + installed, err := o.scanInstalledPackages() if err != nil { o.log.Errorf("Failed to scan installed packages: %s", err) return err } - o.Packages = packs + updatable, err := o.scanUpdatablePackages() + if err != nil { + err = xerrors.Errorf("Failed to scan updatable packages: %w", err) + o.log.Warnf("err: %+v", err) + o.warns = append(o.warns, err) + // Only warning this error + } else { + installed.MergeNewVersion(updatable) + } + + o.Packages = installed return nil } @@ -180,3 +184,58 @@ func (o *arch) scanInstalledPackages() (models.Packages, error) { return pkgs, nil } + +// TODO: Clean any traces +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) + r := o.exec(cmd, noSudo) + if !r.isSuccess() { + return nil, xerrors.Errorf("Failed to SSH: %s", r) + } + pkgs, _ := o.parseOutdatedPackages(r.Stdout) + + unlinkCmd := `TMPPATH="${TMPDIR:-/tmp}/vuls" +rm -r "$TMPPATH"` + + cmd = util.PrependProxyEnv(unlinkCmd) + 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 pkgs, nil +} + +func (o *arch) parseOutdatedPackages(stdout string) (models.Packages, error) { + packs := models.Packages{} + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + if !strings.Contains(line, "->") { + continue + } + ss := strings.Fields(line) + name := ss[0] + fullVersionInfo := ss[3] + + versionAndRelease := strings.Split(fullVersionInfo, "-") + + packs[name] = models.Package{ + Name: name, + NewVersion: versionAndRelease[0], + NewRelease: versionAndRelease[1], + } + } + return packs, nil +} From 512eadbead1348dd1be6e21586176cd2510c801d Mon Sep 17 00:00:00 2001 From: Sankalp Date: Tue, 20 Aug 2024 17:41:11 +0200 Subject: [PATCH 3/3] [Remove] Comment Removes obsolete comment Signed-off-by: Sankalp --- scanner/arch.go | 1 - 1 file changed, 1 deletion(-) diff --git a/scanner/arch.go b/scanner/arch.go index 3758e08724..62ae5277f7 100644 --- a/scanner/arch.go +++ b/scanner/arch.go @@ -185,7 +185,6 @@ func (o *arch) scanInstalledPackages() (models.Packages, error) { return pkgs, nil } -// TODO: Clean any traces func (o *arch) scanUpdatablePackages() (models.Packages, error) { listOutdateCmd := `TMPPATH="${TMPDIR:-/tmp}/vuls" DBPATH="$(pacman-conf DBPath)"