459 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package models
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"golang.org/x/exp/slices"
 | 
						|
	"golang.org/x/xerrors"
 | 
						|
 | 
						|
	"github.com/future-architect/vuls/constant"
 | 
						|
)
 | 
						|
 | 
						|
// Packages is Map of Package
 | 
						|
// { "package-name": Package }
 | 
						|
type Packages map[string]Package
 | 
						|
 | 
						|
// NewPackages create Packages
 | 
						|
func NewPackages(packs ...Package) Packages {
 | 
						|
	m := Packages{}
 | 
						|
	for _, pack := range packs {
 | 
						|
		m[pack.Name] = pack
 | 
						|
	}
 | 
						|
	return m
 | 
						|
}
 | 
						|
 | 
						|
// MergeNewVersion merges candidate version information to the receiver struct
 | 
						|
func (ps Packages) MergeNewVersion(as Packages) {
 | 
						|
	for name, pack := range ps {
 | 
						|
		pack.NewVersion = pack.Version
 | 
						|
		pack.NewRelease = pack.Release
 | 
						|
		ps[name] = pack
 | 
						|
	}
 | 
						|
 | 
						|
	for _, a := range as {
 | 
						|
		if pack, ok := ps[a.Name]; ok {
 | 
						|
			pack.NewVersion = a.NewVersion
 | 
						|
			pack.NewRelease = a.NewRelease
 | 
						|
			pack.Repository = a.Repository
 | 
						|
			ps[a.Name] = pack
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Merge returns merged map (immutable)
 | 
						|
func (ps Packages) Merge(other Packages) Packages {
 | 
						|
	merged := Packages{}
 | 
						|
	for k, v := range ps {
 | 
						|
		merged[k] = v
 | 
						|
	}
 | 
						|
	for k, v := range other {
 | 
						|
		merged[k] = v
 | 
						|
	}
 | 
						|
	return merged
 | 
						|
}
 | 
						|
 | 
						|
// FindOne search a element
 | 
						|
func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) {
 | 
						|
	for key, p := range ps {
 | 
						|
		if f(p) {
 | 
						|
			return key, p, true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return "", Package{}, false
 | 
						|
}
 | 
						|
 | 
						|
// FindByFQPN search a package by Fully-Qualified-Package-Name
 | 
						|
func (ps Packages) FindByFQPN(nameVerRel string) (*Package, error) {
 | 
						|
	for _, p := range ps {
 | 
						|
		if nameVerRel == p.FQPN() {
 | 
						|
			return &p, nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, xerrors.Errorf("Failed to find the package: %s", nameVerRel)
 | 
						|
}
 | 
						|
 | 
						|
// Package has installed binary packages.
 | 
						|
type Package struct {
 | 
						|
	Name             string               `json:"name"`
 | 
						|
	Version          string               `json:"version"`
 | 
						|
	Release          string               `json:"release"`
 | 
						|
	NewVersion       string               `json:"newVersion"`
 | 
						|
	NewRelease       string               `json:"newRelease"`
 | 
						|
	Arch             string               `json:"arch"`
 | 
						|
	Repository       string               `json:"repository"`
 | 
						|
	ModularityLabel  string               `json:"modularitylabel"`
 | 
						|
	Changelog        *Changelog           `json:"changelog,omitempty"`
 | 
						|
	AffectedProcs    []AffectedProcess    `json:",omitempty"`
 | 
						|
	NeedRestartProcs []NeedRestartProcess `json:",omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// FQPN returns Fully-Qualified-Package-Name
 | 
						|
// name-version-release.arch
 | 
						|
func (p Package) FQPN() string {
 | 
						|
	fqpn := p.Name
 | 
						|
	if p.Version != "" {
 | 
						|
		fqpn += fmt.Sprintf("-%s", p.Version)
 | 
						|
	}
 | 
						|
	if p.Release != "" {
 | 
						|
		fqpn += fmt.Sprintf("-%s", p.Release)
 | 
						|
	}
 | 
						|
	return fqpn
 | 
						|
}
 | 
						|
 | 
						|
// FormatVer returns package version-release
 | 
						|
func (p Package) FormatVer() string {
 | 
						|
	ver := p.Version
 | 
						|
	if 0 < len(p.Release) {
 | 
						|
		ver = fmt.Sprintf("%s-%s", ver, p.Release)
 | 
						|
	}
 | 
						|
	return ver
 | 
						|
}
 | 
						|
 | 
						|
// FormatNewVer returns package version-release
 | 
						|
func (p Package) FormatNewVer() string {
 | 
						|
	ver := p.NewVersion
 | 
						|
	if 0 < len(p.NewRelease) {
 | 
						|
		ver = fmt.Sprintf("%s-%s", ver, p.NewRelease)
 | 
						|
	}
 | 
						|
	return ver
 | 
						|
}
 | 
						|
 | 
						|
// FormatVersionFromTo formats installed and new package version
 | 
						|
func (p Package) FormatVersionFromTo(stat PackageFixStatus) string {
 | 
						|
	to := p.FormatNewVer()
 | 
						|
	if stat.NotFixedYet {
 | 
						|
		if stat.FixState != "" {
 | 
						|
			to = stat.FixState
 | 
						|
		} else {
 | 
						|
			to = "Not Fixed Yet"
 | 
						|
		}
 | 
						|
	} else if p.NewVersion == "" {
 | 
						|
		to = "Unknown"
 | 
						|
	}
 | 
						|
	var fixedIn string
 | 
						|
	if stat.FixedIn != "" {
 | 
						|
		fixedIn = fmt.Sprintf(" (FixedIn: %s)", stat.FixedIn)
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("%s-%s -> %s%s",
 | 
						|
		p.Name, p.FormatVer(), to, fixedIn)
 | 
						|
}
 | 
						|
 | 
						|
// FormatChangelog formats the changelog
 | 
						|
func (p Package) FormatChangelog() string {
 | 
						|
	buf := []string{}
 | 
						|
	packVer := fmt.Sprintf("%s-%s -> %s",
 | 
						|
		p.Name, p.FormatVer(), p.FormatNewVer())
 | 
						|
	var delim bytes.Buffer
 | 
						|
	for i := 0; i < len(packVer); i++ {
 | 
						|
		delim.WriteString("-")
 | 
						|
	}
 | 
						|
 | 
						|
	clog := p.Changelog.Contents
 | 
						|
	if lines := strings.Split(clog, "\n"); len(lines) != 0 {
 | 
						|
		clog = strings.Join(lines[0:len(lines)-1], "\n")
 | 
						|
	}
 | 
						|
 | 
						|
	switch p.Changelog.Method {
 | 
						|
	case FailedToGetChangelog:
 | 
						|
		clog = "No changelogs"
 | 
						|
	case FailedToFindVersionInChangelog:
 | 
						|
		clog = "Failed to parse changelogs. For details, check yourself"
 | 
						|
	}
 | 
						|
	buf = append(buf, packVer, delim.String(), clog)
 | 
						|
	return strings.Join(buf, "\n")
 | 
						|
}
 | 
						|
 | 
						|
// Changelog has contents of changelog and how to get it.
 | 
						|
// Method: models.detectionMethodStr
 | 
						|
type Changelog struct {
 | 
						|
	Contents string          `json:"contents"`
 | 
						|
	Method   DetectionMethod `json:"method"`
 | 
						|
}
 | 
						|
 | 
						|
// AffectedProcess keep a processes information affected by software update
 | 
						|
type AffectedProcess struct {
 | 
						|
	PID             string     `json:"pid,omitempty"`
 | 
						|
	Name            string     `json:"name,omitempty"`
 | 
						|
	ListenPorts     []string   `json:"listenPorts,omitempty"`
 | 
						|
	ListenPortStats []PortStat `json:"listenPortStats,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// PortStat has the result of parsing the port information to the address and port.
 | 
						|
type PortStat struct {
 | 
						|
	BindAddress     string   `json:"bindAddress"`
 | 
						|
	Port            string   `json:"port"`
 | 
						|
	PortReachableTo []string `json:"portReachableTo"`
 | 
						|
}
 | 
						|
 | 
						|
// NewPortStat create a PortStat from ipPort str
 | 
						|
func NewPortStat(ipPort string) (*PortStat, error) {
 | 
						|
	if ipPort == "" {
 | 
						|
		return &PortStat{}, nil
 | 
						|
	}
 | 
						|
	sep := strings.LastIndex(ipPort, ":")
 | 
						|
	if sep == -1 {
 | 
						|
		return nil, xerrors.Errorf("Failed to parse IP:Port: %s", ipPort)
 | 
						|
	}
 | 
						|
	return &PortStat{
 | 
						|
		BindAddress: ipPort[:sep],
 | 
						|
		Port:        ipPort[sep+1:],
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// HasReachablePort checks if Package.AffectedProcs has PortReachableTo
 | 
						|
func (p Package) HasReachablePort() bool {
 | 
						|
	for _, ap := range p.AffectedProcs {
 | 
						|
		for _, lp := range ap.ListenPortStats {
 | 
						|
			if len(lp.PortReachableTo) > 0 {
 | 
						|
				return true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// NeedRestartProcess keep a processes information affected by software update
 | 
						|
type NeedRestartProcess struct {
 | 
						|
	PID         string `json:"pid"`
 | 
						|
	Path        string `json:"path"`
 | 
						|
	ServiceName string `json:"serviceName"`
 | 
						|
	InitSystem  string `json:"initSystem"`
 | 
						|
	HasInit     bool   `json:"-"`
 | 
						|
}
 | 
						|
 | 
						|
// SrcPackage has installed source package information.
 | 
						|
// Debian based Linux has both of package and source information in dpkg.
 | 
						|
// OVAL database often includes a source version (Not a binary version),
 | 
						|
// so it is also needed to capture source version for OVAL version comparison.
 | 
						|
// https://github.com/future-architect/vuls/issues/504
 | 
						|
type SrcPackage struct {
 | 
						|
	Name        string   `json:"name"`
 | 
						|
	Version     string   `json:"version"`
 | 
						|
	Arch        string   `json:"arch"`
 | 
						|
	BinaryNames []string `json:"binaryNames"`
 | 
						|
}
 | 
						|
 | 
						|
// AddBinaryName add the name if not exists
 | 
						|
func (s *SrcPackage) AddBinaryName(name string) {
 | 
						|
	if slices.Contains(s.BinaryNames, name) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	s.BinaryNames = append(s.BinaryNames, name)
 | 
						|
}
 | 
						|
 | 
						|
// SrcPackages is Map of SrcPackage
 | 
						|
// { "package-name": SrcPackage }
 | 
						|
type SrcPackages map[string]SrcPackage
 | 
						|
 | 
						|
// FindByBinName finds by bin-package-name
 | 
						|
func (s SrcPackages) FindByBinName(name string) (*SrcPackage, bool) {
 | 
						|
	for _, p := range s {
 | 
						|
		for _, binName := range p.BinaryNames {
 | 
						|
			if binName == name {
 | 
						|
				return &p, true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, false
 | 
						|
}
 | 
						|
 | 
						|
// raspiPackNamePattern is a regular expression pattern to detect the Raspberry Pi specific package from the package name.
 | 
						|
// e.g. libraspberrypi-dev, rpi-eeprom, python3-rpi.gpio, pi-bluetooth
 | 
						|
var raspiPackNamePattern = regexp.MustCompile(`(.*raspberry.*|^rpi.*|.*-rpi.*|^pi-.*)`)
 | 
						|
 | 
						|
// raspiPackNamePattern is a regular expression pattern to detect the Raspberry Pi specific package from the version.
 | 
						|
// e.g. ffmpeg 7:4.1.4-1+rpt7~deb10u1, vlc 3.0.10-0+deb10u1+rpt2
 | 
						|
var raspiPackVersionPattern = regexp.MustCompile(`.+\+rp(t|i)\d+`)
 | 
						|
 | 
						|
// raspiPackNameList is a package name array of Raspberry Pi specific packages that are difficult to detect with regular expressions.
 | 
						|
var raspiPackNameList = []string{"piclone", "pipanel", "pishutdown", "piwiz", "pixflat-icons"}
 | 
						|
 | 
						|
// IsRaspbianPackage judges whether it is a package related to Raspberry Pi from the package name and version
 | 
						|
func IsRaspbianPackage(name, version string) bool {
 | 
						|
	if raspiPackNamePattern.MatchString(name) || raspiPackVersionPattern.MatchString(version) {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	for _, n := range raspiPackNameList {
 | 
						|
		if n == name {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// RenameKernelSourcePackageName is change common kernel source package
 | 
						|
func RenameKernelSourcePackageName(family, name string) string {
 | 
						|
	switch family {
 | 
						|
	case constant.Debian, constant.Raspbian:
 | 
						|
		return strings.NewReplacer("linux-signed", "linux", "linux-latest", "linux", "-amd64", "", "-arm64", "", "-i386", "").Replace(name)
 | 
						|
	case constant.Ubuntu:
 | 
						|
		return strings.NewReplacer("linux-signed", "linux", "linux-meta", "linux").Replace(name)
 | 
						|
	default:
 | 
						|
		return name
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// IsKernelSourcePackage check whether the source package is a kernel package
 | 
						|
func IsKernelSourcePackage(family, name string) bool {
 | 
						|
	switch family {
 | 
						|
	case constant.Debian, constant.Raspbian:
 | 
						|
		switch ss := strings.Split(RenameKernelSourcePackageName(family, name), "-"); len(ss) {
 | 
						|
		case 1:
 | 
						|
			return ss[0] == "linux"
 | 
						|
		case 2:
 | 
						|
			if ss[0] != "linux" {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			switch ss[1] {
 | 
						|
			case "grsec":
 | 
						|
				return true
 | 
						|
			default:
 | 
						|
				_, err := strconv.ParseFloat(ss[1], 64)
 | 
						|
				return err == nil
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	case constant.Ubuntu: // https://git.launchpad.net/ubuntu-cve-tracker/tree/scripts/cve_lib.py#n1219
 | 
						|
		switch ss := strings.Split(RenameKernelSourcePackageName(family, name), "-"); len(ss) {
 | 
						|
		case 1:
 | 
						|
			return ss[0] == "linux"
 | 
						|
		case 2:
 | 
						|
			if ss[0] != "linux" {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			switch ss[1] {
 | 
						|
			case "armadaxp", "mako", "manta", "flo", "goldfish", "joule", "raspi", "raspi2", "snapdragon", "allwinner", "aws", "azure", "bluefield", "dell300x", "gcp", "gke", "gkeop", "ibm", "iot", "laptop", "lowlatency", "kvm", "nvidia", "oem", "oracle", "euclid", "hwe", "riscv", "starfive", "realtime", "mtk":
 | 
						|
				return true
 | 
						|
			default:
 | 
						|
				_, err := strconv.ParseFloat(ss[1], 64)
 | 
						|
				return err == nil
 | 
						|
			}
 | 
						|
		case 3:
 | 
						|
			if ss[0] != "linux" {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			switch ss[1] {
 | 
						|
			case "ti":
 | 
						|
				return ss[2] == "omap4"
 | 
						|
			case "raspi", "raspi2", "allwinner", "gke", "gkeop", "ibm", "oracle", "riscv", "starfive":
 | 
						|
				_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
				return err == nil
 | 
						|
			case "aws":
 | 
						|
				switch ss[2] {
 | 
						|
				case "hwe", "edge":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "azure":
 | 
						|
				switch ss[2] {
 | 
						|
				case "cvm", "fde", "edge":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "gcp":
 | 
						|
				switch ss[2] {
 | 
						|
				case "edge":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "intel":
 | 
						|
				switch ss[2] {
 | 
						|
				case "iotg", "opt":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "oem":
 | 
						|
				switch ss[2] {
 | 
						|
				case "osp1":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "lts":
 | 
						|
				switch ss[2] {
 | 
						|
				case "utopic", "vivid", "wily", "xenial":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					return false
 | 
						|
				}
 | 
						|
			case "hwe":
 | 
						|
				switch ss[2] {
 | 
						|
				case "edge":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			case "xilinx":
 | 
						|
				return ss[2] == "zynqmp"
 | 
						|
			case "nvidia":
 | 
						|
				switch ss[2] {
 | 
						|
				case "tegra":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[2], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				return false
 | 
						|
			}
 | 
						|
		case 4:
 | 
						|
			if ss[0] != "linux" {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			switch ss[1] {
 | 
						|
			case "azure":
 | 
						|
				if ss[2] != "fde" {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
				_, err := strconv.ParseFloat(ss[3], 64)
 | 
						|
				return err == nil
 | 
						|
			case "intel":
 | 
						|
				if ss[2] != "iotg" {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
				_, err := strconv.ParseFloat(ss[3], 64)
 | 
						|
				return err == nil
 | 
						|
			case "lowlatency":
 | 
						|
				if ss[2] != "hwe" {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
				_, err := strconv.ParseFloat(ss[3], 64)
 | 
						|
				return err == nil
 | 
						|
			case "nvidia":
 | 
						|
				if ss[2] != "tegra" {
 | 
						|
					return false
 | 
						|
				}
 | 
						|
				switch ss[3] {
 | 
						|
				case "igx":
 | 
						|
					return true
 | 
						|
				default:
 | 
						|
					_, err := strconv.ParseFloat(ss[3], 64)
 | 
						|
					return err == nil
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				return false
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		return false
 | 
						|
	}
 | 
						|
}
 |