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"` 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"` }