* feat(report): Enhance scan result and cyclondex for supply chain security on Integration with GitHub Security Alerts * derive ecosystem/version from dependency graph * fix vars name && fetch manifest info on GSA && arrange ghpkgToPURL structure * fix miscs * typo in error message * fix ecosystem equally to trivy * miscs * refactoring * recursive dependency graph pagination * change var name && update comments * omit map type of ghpkgToPURL in signatures * fix vars name * goimports * make fmt * fix comment Co-authored-by: MaineK00n <mainek00n.1229@gmail.com>
519 lines
15 KiB
Go
519 lines
15 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/future-architect/vuls/config"
|
|
"github.com/future-architect/vuls/constant"
|
|
"github.com/future-architect/vuls/cwe"
|
|
"github.com/future-architect/vuls/logging"
|
|
)
|
|
|
|
// ScanResults is a slide of ScanResult
|
|
type ScanResults []ScanResult
|
|
|
|
// ScanResult has the result of scanned CVE information.
|
|
type ScanResult struct {
|
|
JSONVersion int `json:"jsonVersion"`
|
|
Lang string `json:"lang"`
|
|
ServerUUID string `json:"serverUUID"`
|
|
ServerName string `json:"serverName"` // TOML Section key
|
|
Family string `json:"family"`
|
|
Release string `json:"release"`
|
|
Container Container `json:"container"`
|
|
Platform Platform `json:"platform"`
|
|
IPv4Addrs []string `json:"ipv4Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
|
|
IPv6Addrs []string `json:"ipv6Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
|
|
IPSIdentifiers map[string]string `json:"ipsIdentifiers,omitempty"`
|
|
ScannedAt time.Time `json:"scannedAt"`
|
|
ScanMode string `json:"scanMode"`
|
|
ScannedVersion string `json:"scannedVersion"`
|
|
ScannedRevision string `json:"scannedRevision"`
|
|
ScannedBy string `json:"scannedBy"`
|
|
ScannedVia string `json:"scannedVia"`
|
|
ScannedIPv4Addrs []string `json:"scannedIpv4Addrs,omitempty"`
|
|
ScannedIPv6Addrs []string `json:"scannedIpv6Addrs,omitempty"`
|
|
ReportedAt time.Time `json:"reportedAt"`
|
|
ReportedVersion string `json:"reportedVersion"`
|
|
ReportedRevision string `json:"reportedRevision"`
|
|
ReportedBy string `json:"reportedBy"`
|
|
Errors []string `json:"errors"`
|
|
Warnings []string `json:"warnings"`
|
|
|
|
ScannedCves VulnInfos `json:"scannedCves"`
|
|
RunningKernel Kernel `json:"runningKernel"`
|
|
Packages Packages `json:"packages"`
|
|
SrcPackages SrcPackages `json:",omitempty"`
|
|
EnabledDnfModules []string `json:"enabledDnfModules,omitempty"` // for dnf modules
|
|
WordPressPackages WordPressPackages `json:",omitempty"`
|
|
GitHubManifests DependencyGraphManifests `json:"gitHubManifests,omitempty"`
|
|
LibraryScanners LibraryScanners `json:"libraries,omitempty"`
|
|
CweDict CweDict `json:"cweDict,omitempty"`
|
|
Optional map[string]interface{} `json:",omitempty"`
|
|
Config struct {
|
|
Scan config.Config `json:"scan"`
|
|
Report config.Config `json:"report"`
|
|
} `json:"config"`
|
|
}
|
|
|
|
// Container has Container information
|
|
type Container struct {
|
|
ContainerID string `json:"containerID"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
Type string `json:"type"`
|
|
UUID string `json:"uuid"`
|
|
}
|
|
|
|
// Platform has platform information
|
|
type Platform struct {
|
|
Name string `json:"name"` // aws or azure or gcp or other...
|
|
InstanceID string `json:"instanceID"`
|
|
}
|
|
|
|
// Kernel has the Release, version and whether need restart
|
|
type Kernel struct {
|
|
Release string `json:"release"`
|
|
Version string `json:"version"`
|
|
RebootRequired bool `json:"rebootRequired"`
|
|
}
|
|
|
|
// FilterInactiveWordPressLibs is filter function.
|
|
func (r *ScanResult) FilterInactiveWordPressLibs(detectInactive bool) {
|
|
if detectInactive {
|
|
return
|
|
}
|
|
|
|
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
|
|
if len(v.WpPackageFixStats) == 0 {
|
|
return true
|
|
}
|
|
// Ignore if all libs in this vulnInfo inactive
|
|
for _, wp := range v.WpPackageFixStats {
|
|
if p, ok := r.WordPressPackages.Find(wp.Name); ok {
|
|
if p.Status != Inactive {
|
|
return true
|
|
}
|
|
} else {
|
|
logging.Log.Warnf("Failed to find the WordPress pkg: %+s", wp.Name)
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
r.ScannedCves = filtered
|
|
}
|
|
|
|
// ReportFileName returns the filename on localhost without extension
|
|
func (r ScanResult) ReportFileName() (name string) {
|
|
if r.Container.ContainerID == "" {
|
|
return r.ServerName
|
|
}
|
|
return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
|
|
}
|
|
|
|
// ReportKeyName returns the name of key on S3, Azure-Blob without extension
|
|
func (r ScanResult) ReportKeyName() (name string) {
|
|
timestr := r.ScannedAt.Format(time.RFC3339)
|
|
if r.Container.ContainerID == "" {
|
|
return fmt.Sprintf("%s/%s", timestr, r.ServerName)
|
|
}
|
|
return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName)
|
|
}
|
|
|
|
// ServerInfo returns server name one line
|
|
func (r ScanResult) ServerInfo() string {
|
|
if r.Container.ContainerID == "" {
|
|
return fmt.Sprintf("%s (%s%s)",
|
|
r.FormatServerName(), r.Family, r.Release)
|
|
}
|
|
return fmt.Sprintf(
|
|
"%s (%s%s) on %s",
|
|
r.FormatServerName(),
|
|
r.Family,
|
|
r.Release,
|
|
r.ServerName,
|
|
)
|
|
}
|
|
|
|
// ServerInfoTui returns server information for TUI sidebar
|
|
func (r ScanResult) ServerInfoTui() string {
|
|
if r.Container.ContainerID == "" {
|
|
line := fmt.Sprintf("%s (%s%s)",
|
|
r.ServerName, r.Family, r.Release)
|
|
if len(r.Warnings) != 0 {
|
|
line = "[Warn] " + line
|
|
}
|
|
if r.RunningKernel.RebootRequired {
|
|
return "[Reboot] " + line
|
|
}
|
|
return line
|
|
}
|
|
|
|
fmtstr := "|-- %s (%s%s)"
|
|
if r.RunningKernel.RebootRequired {
|
|
fmtstr = "|-- [Reboot] %s (%s%s)"
|
|
}
|
|
return fmt.Sprintf(fmtstr, r.Container.Name, r.Family, r.Release)
|
|
}
|
|
|
|
// FormatServerName returns server and container name
|
|
func (r ScanResult) FormatServerName() (name string) {
|
|
if r.Container.ContainerID == "" {
|
|
name = r.ServerName
|
|
} else {
|
|
name = fmt.Sprintf("%s@%s",
|
|
r.Container.Name, r.ServerName)
|
|
}
|
|
if r.RunningKernel.RebootRequired {
|
|
name = "[Reboot Required] " + name
|
|
}
|
|
return
|
|
}
|
|
|
|
// FormatTextReportHeader returns header of text report
|
|
func (r ScanResult) FormatTextReportHeader() string {
|
|
var buf bytes.Buffer
|
|
for i := 0; i < len(r.ServerInfo()); i++ {
|
|
buf.WriteString("=")
|
|
}
|
|
|
|
pkgs := r.FormatUpdatablePkgsSummary()
|
|
if 0 < len(r.WordPressPackages) {
|
|
pkgs = fmt.Sprintf("%s, %d WordPress pkgs", pkgs, len(r.WordPressPackages))
|
|
}
|
|
if 0 < len(r.LibraryScanners) {
|
|
pkgs = fmt.Sprintf("%s, %d libs", pkgs, r.LibraryScanners.Total())
|
|
}
|
|
|
|
return fmt.Sprintf("%s\n%s\n%s\n%s, %s, %s, %s\n%s\n",
|
|
r.ServerInfo(),
|
|
buf.String(),
|
|
r.ScannedCves.FormatCveSummary(),
|
|
r.ScannedCves.FormatFixedStatus(r.Packages),
|
|
r.FormatExploitCveSummary(),
|
|
r.FormatMetasploitCveSummary(),
|
|
r.FormatAlertSummary(),
|
|
pkgs)
|
|
}
|
|
|
|
// FormatUpdatablePkgsSummary returns a summary of updatable packages
|
|
func (r ScanResult) FormatUpdatablePkgsSummary() string {
|
|
mode := r.Config.Scan.Servers[r.ServerName].Mode
|
|
if !r.isDisplayUpdatableNum(mode) {
|
|
return fmt.Sprintf("%d installed", len(r.Packages))
|
|
}
|
|
|
|
nUpdatable := 0
|
|
for _, p := range r.Packages {
|
|
if p.NewVersion == "" {
|
|
continue
|
|
}
|
|
if p.Version != p.NewVersion || p.Release != p.NewRelease {
|
|
nUpdatable++
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d installed, %d updatable",
|
|
len(r.Packages),
|
|
nUpdatable)
|
|
}
|
|
|
|
// FormatExploitCveSummary returns a summary of exploit cve
|
|
func (r ScanResult) FormatExploitCveSummary() string {
|
|
nExploitCve := 0
|
|
for _, vuln := range r.ScannedCves {
|
|
if 0 < len(vuln.Exploits) {
|
|
nExploitCve++
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d poc", nExploitCve)
|
|
}
|
|
|
|
// FormatMetasploitCveSummary returns a summary of exploit cve
|
|
func (r ScanResult) FormatMetasploitCveSummary() string {
|
|
nMetasploitCve := 0
|
|
for _, vuln := range r.ScannedCves {
|
|
if 0 < len(vuln.Metasploits) {
|
|
nMetasploitCve++
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d exploits", nMetasploitCve)
|
|
}
|
|
|
|
// FormatAlertSummary returns a summary of CERT alerts
|
|
func (r ScanResult) FormatAlertSummary() string {
|
|
cisaCnt := 0
|
|
uscertCnt := 0
|
|
jpcertCnt := 0
|
|
for _, vuln := range r.ScannedCves {
|
|
if len(vuln.AlertDict.CISA) > 0 {
|
|
cisaCnt += len(vuln.AlertDict.CISA)
|
|
}
|
|
if len(vuln.AlertDict.USCERT) > 0 {
|
|
uscertCnt += len(vuln.AlertDict.USCERT)
|
|
}
|
|
if len(vuln.AlertDict.JPCERT) > 0 {
|
|
jpcertCnt += len(vuln.AlertDict.JPCERT)
|
|
}
|
|
}
|
|
return fmt.Sprintf("cisa: %d, uscert: %d, jpcert: %d alerts", cisaCnt, uscertCnt, jpcertCnt)
|
|
}
|
|
|
|
func (r ScanResult) isDisplayUpdatableNum(mode config.ScanMode) bool {
|
|
if r.Family == constant.FreeBSD {
|
|
return false
|
|
}
|
|
|
|
if mode.IsOffline() {
|
|
return false
|
|
}
|
|
if mode.IsFastRoot() || mode.IsDeep() {
|
|
return true
|
|
}
|
|
if mode.IsFast() {
|
|
switch r.Family {
|
|
case constant.RedHat,
|
|
constant.Oracle,
|
|
constant.Debian,
|
|
constant.Ubuntu,
|
|
constant.Raspbian:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsContainer returns whether this ServerInfo is about container
|
|
func (r ScanResult) IsContainer() bool {
|
|
return 0 < len(r.Container.ContainerID)
|
|
}
|
|
|
|
// RemoveRaspbianPackFromResult is for Raspberry Pi and removes the Raspberry Pi dedicated package from ScanResult.
|
|
func (r ScanResult) RemoveRaspbianPackFromResult() *ScanResult {
|
|
if r.Family != constant.Raspbian {
|
|
return &r
|
|
}
|
|
|
|
packs := make(Packages)
|
|
for _, pack := range r.Packages {
|
|
if !IsRaspbianPackage(pack.Name, pack.Version) {
|
|
packs[pack.Name] = pack
|
|
}
|
|
}
|
|
srcPacks := make(SrcPackages)
|
|
for _, pack := range r.SrcPackages {
|
|
if !IsRaspbianPackage(pack.Name, pack.Version) {
|
|
srcPacks[pack.Name] = pack
|
|
}
|
|
}
|
|
|
|
r.Packages = packs
|
|
r.SrcPackages = srcPacks
|
|
|
|
return &r
|
|
}
|
|
|
|
// ClearFields clears a given fields of ScanResult
|
|
func (r ScanResult) ClearFields(targetTagNames []string) ScanResult {
|
|
if len(targetTagNames) == 0 {
|
|
return r
|
|
}
|
|
target := map[string]bool{}
|
|
for _, n := range targetTagNames {
|
|
target[strings.ToLower(n)] = true
|
|
}
|
|
t := reflect.ValueOf(r).Type()
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
jsonValue := strings.Split(f.Tag.Get("json"), ",")[0]
|
|
if ok := target[strings.ToLower(jsonValue)]; ok {
|
|
vv := reflect.New(f.Type).Elem().Interface()
|
|
reflect.ValueOf(&r).Elem().FieldByName(f.Name).Set(reflect.ValueOf(vv))
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// CheckEOL checks the EndOfLife of the OS
|
|
func (r *ScanResult) CheckEOL() {
|
|
switch r.Family {
|
|
case constant.ServerTypePseudo, constant.Raspbian:
|
|
return
|
|
}
|
|
|
|
eol, found := config.GetEOL(r.Family, r.Release)
|
|
if !found {
|
|
r.Warnings = append(r.Warnings,
|
|
fmt.Sprintf("Failed to check EOL. Register the issue to https://github.com/future-architect/vuls/issues with the information in `Family: %s Release: %s`",
|
|
r.Family, r.Release))
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
if eol.IsStandardSupportEnded(now) {
|
|
r.Warnings = append(r.Warnings, "Standard OS support is EOL(End-of-Life). Purchase extended support if available or Upgrading your OS is strongly recommended.")
|
|
if eol.ExtendedSupportUntil.IsZero() {
|
|
return
|
|
}
|
|
if !eol.IsExtendedSuppportEnded(now) {
|
|
r.Warnings = append(r.Warnings,
|
|
fmt.Sprintf("Extended support available until %s. Check the vendor site.",
|
|
eol.ExtendedSupportUntil.Format("2006-01-02")))
|
|
} else {
|
|
r.Warnings = append(r.Warnings,
|
|
"Extended support is also EOL. There are many Vulnerabilities that are not detected, Upgrading your OS strongly recommended.")
|
|
}
|
|
} else if !eol.StandardSupportUntil.IsZero() &&
|
|
now.AddDate(0, 3, 0).After(eol.StandardSupportUntil) {
|
|
r.Warnings = append(r.Warnings,
|
|
fmt.Sprintf("Standard OS support will be end in 3 months. EOL date: %s",
|
|
eol.StandardSupportUntil.Format("2006-01-02")))
|
|
}
|
|
}
|
|
|
|
// SortForJSONOutput sort list elements in the ScanResult to diff in integration-test
|
|
func (r *ScanResult) SortForJSONOutput() {
|
|
for k, v := range r.Packages {
|
|
sort.Slice(v.AffectedProcs, func(i, j int) bool {
|
|
return v.AffectedProcs[i].PID < v.AffectedProcs[j].PID
|
|
})
|
|
sort.Slice(v.NeedRestartProcs, func(i, j int) bool {
|
|
return v.NeedRestartProcs[i].PID < v.NeedRestartProcs[j].PID
|
|
})
|
|
r.Packages[k] = v
|
|
}
|
|
for i, v := range r.LibraryScanners {
|
|
sort.Slice(v.Libs, func(i, j int) bool {
|
|
switch strings.Compare(v.Libs[i].Name, v.Libs[j].Name) {
|
|
case -1:
|
|
return true
|
|
case 1:
|
|
return false
|
|
}
|
|
return v.Libs[i].Version < v.Libs[j].Version
|
|
|
|
})
|
|
r.LibraryScanners[i] = v
|
|
}
|
|
|
|
for k, v := range r.ScannedCves {
|
|
sort.Slice(v.AffectedPackages, func(i, j int) bool {
|
|
return v.AffectedPackages[i].Name < v.AffectedPackages[j].Name
|
|
})
|
|
sort.Slice(v.DistroAdvisories, func(i, j int) bool {
|
|
return v.DistroAdvisories[i].AdvisoryID < v.DistroAdvisories[j].AdvisoryID
|
|
})
|
|
sort.Slice(v.Exploits, func(i, j int) bool {
|
|
return v.Exploits[i].URL < v.Exploits[j].URL
|
|
})
|
|
sort.Slice(v.Metasploits, func(i, j int) bool {
|
|
return v.Metasploits[i].Name < v.Metasploits[j].Name
|
|
})
|
|
sort.Slice(v.Mitigations, func(i, j int) bool {
|
|
return v.Mitigations[i].URL < v.Mitigations[j].URL
|
|
})
|
|
|
|
v.CveContents.Sort()
|
|
|
|
sort.Slice(v.AlertDict.USCERT, func(i, j int) bool {
|
|
return v.AlertDict.USCERT[i].Title < v.AlertDict.USCERT[j].Title
|
|
})
|
|
sort.Slice(v.AlertDict.JPCERT, func(i, j int) bool {
|
|
return v.AlertDict.JPCERT[i].Title < v.AlertDict.JPCERT[j].Title
|
|
})
|
|
sort.Slice(v.AlertDict.CISA, func(i, j int) bool {
|
|
return v.AlertDict.CISA[i].Title < v.AlertDict.CISA[j].Title
|
|
})
|
|
r.ScannedCves[k] = v
|
|
}
|
|
}
|
|
|
|
// CweDict is a dictionary for CWE
|
|
type CweDict map[string]CweDictEntry
|
|
|
|
// AttentionCWE has OWASP TOP10, CWE TOP25, CWE/SANS TOP25 rank and url
|
|
type AttentionCWE struct {
|
|
Rank string
|
|
URL string
|
|
}
|
|
|
|
// Get the name, url, top10URL for the specified cweID, lang
|
|
func (c CweDict) Get(cweID, lang string) (name, url string, owasp, cwe25, sans map[string]AttentionCWE) {
|
|
cweNum := strings.TrimPrefix(cweID, "CWE-")
|
|
dict, ok := c[cweNum]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
owasp, cwe25, sans = fillAttentionCwe(dict, lang)
|
|
switch lang {
|
|
case "ja":
|
|
if dict, ok := cwe.CweDictJa[cweNum]; ok {
|
|
name = dict.Name
|
|
url = fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
|
|
} else {
|
|
if dict, ok := cwe.CweDictEn[cweNum]; ok {
|
|
name = dict.Name
|
|
}
|
|
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
|
|
}
|
|
default:
|
|
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
|
|
if dict, ok := cwe.CweDictEn[cweNum]; ok {
|
|
name = dict.Name
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func fillAttentionCwe(dict CweDictEntry, lang string) (owasp, cwe25, sans map[string]AttentionCWE) {
|
|
owasp, cwe25, sans = map[string]AttentionCWE{}, map[string]AttentionCWE{}, map[string]AttentionCWE{}
|
|
switch lang {
|
|
case "ja":
|
|
for year, rank := range dict.OwaspTopTens {
|
|
owasp[year] = AttentionCWE{
|
|
Rank: rank,
|
|
URL: cwe.OwaspTopTenURLsJa[year][rank],
|
|
}
|
|
}
|
|
default:
|
|
for year, rank := range dict.OwaspTopTens {
|
|
owasp[year] = AttentionCWE{
|
|
Rank: rank,
|
|
URL: cwe.OwaspTopTenURLsEn[year][rank],
|
|
}
|
|
}
|
|
}
|
|
|
|
for year, rank := range dict.CweTopTwentyfives {
|
|
cwe25[year] = AttentionCWE{
|
|
Rank: rank,
|
|
URL: cwe.CweTopTwentyfiveURLs[year],
|
|
}
|
|
}
|
|
|
|
for year, rank := range dict.SansTopTwentyfives {
|
|
sans[year] = AttentionCWE{
|
|
Rank: rank,
|
|
URL: cwe.SansTopTwentyfiveURLs[year],
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// CweDictEntry is a entry of CWE
|
|
type CweDictEntry struct {
|
|
En *cwe.Cwe `json:"en,omitempty"`
|
|
Ja *cwe.Cwe `json:"ja,omitempty"`
|
|
OwaspTopTens map[string]string `json:"owaspTopTens"`
|
|
CweTopTwentyfives map[string]string `json:"cweTopTwentyfives"`
|
|
SansTopTwentyfives map[string]string `json:"sansTopTwentyfives"`
|
|
}
|