/* Vuls - Vulnerability Scanner Copyright (C) 2016 Future Architect, Inc. Japan. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package models import ( "bytes" "fmt" "strings" "time" "github.com/future-architect/vuls/config" cvedict "github.com/kotakanbe/go-cve-dictionary/models" ) // JSONVersion is JSON Version const JSONVersion = "0.3.0" // ScanResults is slice of ScanResult. type ScanResults []ScanResult //TODO // // Len implement Sort Interface // func (s ScanResults) Len() int { // return len(s) // } // // Swap implement Sort Interface // func (s ScanResults) Swap(i, j int) { // s[i], s[j] = s[j], s[i] // } // // Less implement Sort Interface // func (s ScanResults) Less(i, j int) bool { // if s[i].ServerName == s[j].ServerName { // return s[i].Container.ContainerID < s[i].Container.ContainerID // } // return s[i].ServerName < s[j].ServerName // } // ScanResult has the result of scanned CVE information. type ScanResult struct { ScannedAt time.Time JSONVersion string Lang string ServerName string // TOML Section key Family string Release string Container Container Platform Platform // Scanned Vulns by SSH scan + CPE + OVAL ScannedCves VulnInfos Packages Packages Errors []string Optional [][]interface{} } // ConvertNvdToModel convert NVD to CveContent func (r ScanResult) ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent { var cpes []Cpe for _, c := range nvd.Cpes { cpes = append(cpes, Cpe{CpeName: c.CpeName}) } var refs []Reference for _, r := range nvd.References { refs = append(refs, Reference{ Link: r.Link, Source: r.Source, }) } validVec := true for _, v := range []string{ nvd.AccessVector, nvd.AccessComplexity, nvd.Authentication, nvd.ConfidentialityImpact, nvd.IntegrityImpact, nvd.AvailabilityImpact, } { if len(v) == 0 { validVec = false } } vector := "" if validVec { vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s", string(nvd.AccessVector[0]), string(nvd.AccessComplexity[0]), string(nvd.Authentication[0]), string(nvd.ConfidentialityImpact[0]), string(nvd.IntegrityImpact[0]), string(nvd.AvailabilityImpact[0])) } //TODO CVSSv3 return &CveContent{ Type: NVD, CveID: cveID, Summary: nvd.Summary, Cvss2Score: nvd.Score, Cvss2Vector: vector, Severity: "", // severity is not contained in NVD SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID, Cpes: cpes, CweID: nvd.CweID, References: refs, Published: nvd.PublishedDate, LastModified: nvd.LastModifiedDate, } } // ConvertJvnToModel convert JVN to CveContent func (r ScanResult) ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent { var cpes []Cpe for _, c := range jvn.Cpes { cpes = append(cpes, Cpe{CpeName: c.CpeName}) } refs := []Reference{} for _, r := range jvn.References { refs = append(refs, Reference{ Link: r.Link, Source: r.Source, }) } vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")") return &CveContent{ Type: JVN, CveID: cveID, Title: jvn.Title, Summary: jvn.Summary, Severity: jvn.Severity, Cvss2Score: jvn.Score, Cvss2Vector: vector, SourceLink: jvn.JvnLink, Cpes: cpes, References: refs, Published: jvn.PublishedDate, LastModified: jvn.LastModifiedDate, } } // FilterByCvssOver is filter function. func (r ScanResult) FilterByCvssOver(over float64) ScanResult { // TODO: Set correct default value if over == 0 { over = -1.1 } // TODO: Filter by ignore cves??? filtered := r.ScannedCves.Find(func(v VulnInfo) bool { values := v.CveContents.Cvss2Scores() for _, v := range values { score := v.Value.Score if over <= score { return true } } return false }) copiedScanResult := r copiedScanResult.ScannedCves = filtered return copiedScanResult } // ReportFileName returns the filename on localhost without extention func (r ScanResult) ReportFileName() (name string) { if len(r.Container.ContainerID) == 0 { return fmt.Sprintf("%s", r.ServerName) } return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName) } // ReportKeyName returns the name of key on S3, Azure-Blob without extention func (r ScanResult) ReportKeyName() (name string) { timestr := r.ScannedAt.Format(time.RFC3339) if len(r.Container.ContainerID) == 0 { 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 len(r.Container.ContainerID) == 0 { return fmt.Sprintf("%s (%s%s)", r.ServerName, r.Family, r.Release) } return fmt.Sprintf( "%s / %s (%s%s) on %s", r.Container.Name, r.Container.ContainerID, r.Family, r.Release, r.ServerName, ) } // ServerInfoTui returns server infromation for TUI sidebar func (r ScanResult) ServerInfoTui() string { if len(r.Container.ContainerID) == 0 { return fmt.Sprintf("%s (%s%s)", r.ServerName, r.Family, r.Release) } return fmt.Sprintf( "|-- %s (%s%s)", r.Container.Name, r.Family, r.Release, // r.Container.ContainerID, ) } // FormatServerName returns server and container name func (r ScanResult) FormatServerName() string { if len(r.Container.ContainerID) == 0 { return r.ServerName } return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName) } // CveSummary summarize the number of CVEs group by CVSSv2 Severity func (r ScanResult) CveSummary(ignoreUnscoreCves bool) string { var high, medium, low, unknown int for _, vInfo := range r.ScannedCves { score := vInfo.CveContents.MaxCvss2Score().Value.Score if score < 0.1 { score = vInfo.CveContents.MaxCvss3Score().Value.Score } switch { case 7.0 <= score: high++ case 4.0 <= score: medium++ case 0 < score: low++ default: unknown++ } } if ignoreUnscoreCves { return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", high+medium+low, high, medium, low) } return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", high+medium+low+unknown, high, medium, low, unknown) } // FormatTextReportHeadedr returns header of text report func (r ScanResult) FormatTextReportHeadedr() string { serverInfo := r.ServerInfo() var buf bytes.Buffer for i := 0; i < len(serverInfo); i++ { buf.WriteString("=") } return fmt.Sprintf("%s\n%s\n%s\t%s\n", r.ServerInfo(), buf.String(), r.CveSummary(config.Conf.IgnoreUnscoredCves), r.Packages.FormatUpdatablePacksSummary(), ) } // Confidence is a ranking how confident the CVE-ID was deteted correctly // Score: 0 - 100 type Confidence struct { Score int DetectionMethod string } func (c Confidence) String() string { return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod) } const ( // CpeNameMatchStr is a String representation of CpeNameMatch CpeNameMatchStr = "CpeNameMatch" // YumUpdateSecurityMatchStr is a String representation of YumUpdateSecurityMatch YumUpdateSecurityMatchStr = "YumUpdateSecurityMatch" // PkgAuditMatchStr is a String representation of PkgAuditMatch PkgAuditMatchStr = "PkgAuditMatch" // OvalMatchStr is a String representation of OvalMatch OvalMatchStr = "OvalMatch" // ChangelogExactMatchStr is a String representation of ChangelogExactMatch ChangelogExactMatchStr = "ChangelogExactMatch" // ChangelogLenientMatchStr is a String representation of ChangelogLenientMatch ChangelogLenientMatchStr = "ChangelogLenientMatch" // FailedToGetChangelog is a String representation of FailedToGetChangelog FailedToGetChangelog = "FailedToGetChangelog" // FailedToFindVersionInChangelog is a String representation of FailedToFindVersionInChangelog FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog" ) var ( // CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly CpeNameMatch = Confidence{100, CpeNameMatchStr} // YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr} // PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly PkgAuditMatch = Confidence{100, PkgAuditMatchStr} // OvalMatch is a ranking how confident the CVE-ID was deteted correctly OvalMatch = Confidence{100, OvalMatchStr} // ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr} // ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr} ) // VulnInfos is VulnInfo list, getter/setter, sortable methods. type VulnInfos map[string]VulnInfo // Find elements that matches the function passed in argument func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos { filtered := VulnInfos{} for _, vv := range v { if f(vv) { filtered[vv.CveID] = vv } } return filtered } // FindScoredVulns return socred vulnerabilities func (v VulnInfos) FindScoredVulns() VulnInfos { return v.Find(func(vv VulnInfo) bool { if 0 < vv.CveContents.MaxCvss2Score().Value.Score || 0 < vv.CveContents.MaxCvss3Score().Value.Score { return true } return false }) } // VulnInfo holds a vulnerability information and unsecure packages type VulnInfo struct { CveID string Confidence Confidence PackageNames []string DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD CpeNames []string CveContents CveContents } // Cvss2CalcURL returns CVSS v2 caluclator's URL func (v VulnInfo) Cvss2CalcURL() string { return "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=" + v.CveID } // Cvss3CalcURL returns CVSS v3 caluclator's URL func (v VulnInfo) Cvss3CalcURL() string { return "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=" + v.CveID } // TODO // NilToEmpty set nil slice or map fields to empty to avoid null in JSON // func (v *VulnInfo) NilToEmpty() { // if v.CpeNames == nil { // v.CpeNames = []string{} // } // if v.DistroAdvisories == nil { // v.DistroAdvisories = []DistroAdvisory{} // } // if v.PackageNames == nil { // v.PackageNames = []string{} // } // if v.CveContents == nil { // v.CveContents = NewCveContents() // } // } // CveContentType is a source of CVE information type CveContentType string // NewCveContentType create CveContentType func NewCveContentType(name string) CveContentType { switch name { case "nvd": return NVD case "jvn": return JVN case "redhat", "centos": return RedHat case "ubuntu": return Ubuntu case "debian": return Debian default: return Unknown } } const ( // NVD is NVD NVD CveContentType = "nvd" // JVN is JVN JVN CveContentType = "jvn" // RedHat is RedHat RedHat CveContentType = "redhat" // Debian is Debian Debian CveContentType = "debian" // Ubuntu is Ubuntu Ubuntu CveContentType = "ubuntu" // Unknown is Unknown Unknown CveContentType = "unknown" ) // CveContentTypes has slide of CveContentType type CveContentTypes []CveContentType // AllCveContetTypes has all of CveContentTypes var AllCveContetTypes = CveContentTypes{NVD, JVN, RedHat, Debian, Ubuntu} // Except returns CveContentTypes except for given args func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentTypes) { for _, ctype := range c { found := false for _, except := range excepts { if ctype == except { found = true break } } if !found { excepted = append(excepted, ctype) } } return } // CveContents has CveContent type CveContents map[CveContentType]CveContent // NewCveContents create CveContents func NewCveContents(conts ...CveContent) CveContents { m := map[CveContentType]CveContent{} for _, cont := range conts { m[cont.Type] = cont } return m } // CveContentStr has CveContentType and Value type CveContentStr struct { Type CveContentType Value string } // Except returns CveContents except given keys for enumeration func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) { for ctype, content := range v { found := false for _, exceptCtype := range exceptCtypes { if ctype == exceptCtype { found = true break } } if !found { values[ctype] = content } } return } // CveContentCvss2 has CveContentType and Cvss2 type CveContentCvss2 struct { Type CveContentType Value Cvss2 } // Cvss2 has CVSS v2 type Cvss2 struct { Score float64 Vector string Severity string } // Format CVSS Score and Vector func (c Cvss2) Format() string { return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector) } func cvss2ScoreToSeverity(score float64) string { if 7.0 <= score { return "HIGH" } else if 4.0 <= score { return "MEDIUM" } return "LOW" } // Cvss2Scores returns CVSS V2 Scores func (v CveContents) Cvss2Scores() (values []CveContentCvss2) { order := []CveContentType{NVD, RedHat, JVN} for _, ctype := range order { if cont, found := v[ctype]; found && 0 < cont.Cvss2Score { // https://nvd.nist.gov/vuln-metrics/cvss sev := cont.Severity if ctype == NVD { sev = cvss2ScoreToSeverity(cont.Cvss2Score) } values = append(values, CveContentCvss2{ Type: ctype, Value: Cvss2{ Score: cont.Cvss2Score, Vector: cont.Cvss2Vector, Severity: sev, }, }) } } return } // MaxCvss2Score returns Max CVSS V2 Score func (v CveContents) MaxCvss2Score() CveContentCvss2 { //TODO Severity Ubuntu, Debian... order := []CveContentType{NVD, RedHat, JVN} max := 0.0 value := CveContentCvss2{ Type: Unknown, Value: Cvss2{}, } for _, ctype := range order { if cont, found := v[ctype]; found && max < cont.Cvss2Score { // https://nvd.nist.gov/vuln-metrics/cvss sev := cont.Severity if ctype == NVD { sev = cvss2ScoreToSeverity(cont.Cvss2Score) } value = CveContentCvss2{ Type: ctype, Value: Cvss2{ Score: cont.Cvss2Score, Vector: cont.Cvss2Vector, Severity: sev, }, } max = cont.Cvss2Score } } return value } // CveContentCvss3 has CveContentType and Cvss3 type CveContentCvss3 struct { Type CveContentType Value Cvss3 } // Cvss3 has CVSS v3 Score, Vector and Severity type Cvss3 struct { Score float64 Vector string Severity string } // Format CVSS Score and Vector func (c Cvss3) Format() string { return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector) } func cvss3ScoreToSeverity(score float64) string { if 9.0 <= score { return "CRITICAL" } else if 7.0 <= score { return "HIGH" } else if 4.0 <= score { return "MEDIUM" } return "LOW" } // Cvss3Scores returns CVSS V3 Score func (v CveContents) Cvss3Scores() (values []CveContentCvss3) { //TODO Severity Ubuntu, Debian... order := []CveContentType{RedHat} for _, ctype := range order { if cont, found := v[ctype]; found && 0 < cont.Cvss3Score { // https://nvd.nist.gov/vuln-metrics/cvss sev := cont.Severity if ctype == NVD { sev = cvss3ScoreToSeverity(cont.Cvss2Score) } values = append(values, CveContentCvss3{ Type: ctype, Value: Cvss3{ Score: cont.Cvss3Score, Vector: cont.Cvss3Vector, Severity: sev, }, }) } } return } // MaxCvss3Score returns Max CVSS V3 Score func (v CveContents) MaxCvss3Score() CveContentCvss3 { //TODO Severity Ubuntu, Debian... order := []CveContentType{RedHat} max := 0.0 value := CveContentCvss3{ Type: Unknown, Value: Cvss3{}, } for _, ctype := range order { if cont, found := v[ctype]; found && max < cont.Cvss3Score { // https://nvd.nist.gov/vuln-metrics/cvss sev := cont.Severity if ctype == NVD { sev = cvss3ScoreToSeverity(cont.Cvss2Score) } value = CveContentCvss3{ Type: ctype, Value: Cvss3{ Score: cont.Cvss3Score, Vector: cont.Cvss3Vector, Severity: sev, }, } max = cont.Cvss3Score } } return value } // FormatMaxCvssScore returns Max CVSS Score func (v CveContents) FormatMaxCvssScore() string { v2Max := v.MaxCvss2Score() v3Max := v.MaxCvss3Score() if v2Max.Value.Score <= v3Max.Value.Score { return fmt.Sprintf("%3.1f %s (%s)", v3Max.Value.Score, strings.ToUpper(v3Max.Value.Severity), v3Max.Type) } return fmt.Sprintf("%3.1f %s (%s)", v2Max.Value.Score, strings.ToUpper(v2Max.Value.Severity), v2Max.Type) } // Titles returns tilte (TUI) func (v CveContents) Titles(lang, myFamily string) (values []CveContentStr) { if lang == "ja" { if cont, found := v[JVN]; found && 0 < len(cont.Title) { values = append(values, CveContentStr{JVN, cont.Title}) } } order := CveContentTypes{NVD, NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...) for _, ctype := range order { // Only JVN has meaningful title. so return first 100 char of summary if cont, found := v[ctype]; found && 0 < len(cont.Summary) { summary := strings.Replace(cont.Summary, "\n", " ", -1) index := 75 if len(summary) < index { index = len(summary) } values = append(values, CveContentStr{ Type: ctype, Value: summary[0:index] + "...", }) } } if len(values) == 0 { values = []CveContentStr{{ Type: Unknown, Value: "-", }} } return } // Summaries returns summaries func (v CveContents) Summaries(lang, myFamily string) (values []CveContentStr) { if lang == "ja" { if cont, found := v[JVN]; found && 0 < len(cont.Summary) { summary := cont.Title summary += "\n" + strings.Replace( strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1) values = append(values, CveContentStr{JVN, summary}) } } order := CveContentTypes{NVD, NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...) for _, ctype := range order { if cont, found := v[ctype]; found && 0 < len(cont.Summary) { summary := strings.Replace(cont.Summary, "\n", " ", -1) values = append(values, CveContentStr{ Type: ctype, Value: summary, }) } } if len(values) == 0 { values = []CveContentStr{{ Type: Unknown, Value: "-", }} } return } // SourceLinks returns link of source func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveContentStr) { if lang == "ja" { if cont, found := v[JVN]; found && !cont.Empty() { values = append(values, CveContentStr{JVN, cont.SourceLink}) } } order := CveContentTypes{NVD, NewCveContentType(myFamily)} for _, ctype := range order { if cont, found := v[ctype]; found { values = append(values, CveContentStr{ctype, cont.SourceLink}) } } if len(values) == 0 { return []CveContentStr{{ Type: NVD, Value: "https://nvd.nist.gov/vuln/detail/" + cveID, }} } return values } // VendorLink returns link of source func (v CveContents) VendorLink(myFamily string) CveContentStr { ctype := NewCveContentType(myFamily) if cont, ok := v[ctype]; ok { return CveContentStr{ctype, cont.SourceLink} } return CveContentStr{ctype, ""} } // Severities returns Severities // func (v CveContents) Severities(myFamily string) (values []CveContentValue) { // order := CveContentTypes{NVD, NewCveContentType(myFamily)} // order = append(order, AllCveContetTypes.Except(append(order)...)...) // for _, ctype := range order { // if cont, found := v[ctype]; found && 0 < len(cont.Severity) { // values = append(values, CveContentValue{ // Type: ctype, // Value: cont.Severity, // }) // } // } // return // } // CveContentCpes has CveContentType and Value type CveContentCpes struct { Type CveContentType Value []Cpe } // Cpes returns affected CPEs of this Vulnerability func (v CveContents) Cpes(myFamily string) (values []CveContentCpes) { order := CveContentTypes{NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order)...)...) for _, ctype := range order { if cont, found := v[ctype]; found && 0 < len(cont.Cpes) { values = append(values, CveContentCpes{ Type: ctype, Value: cont.Cpes, }) } } return } // CveContentRefs has CveContentType and Cpes type CveContentRefs struct { Type CveContentType Value []Reference } // References returns References func (v CveContents) References(myFamily string) (values []CveContentRefs) { order := CveContentTypes{NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order)...)...) for _, ctype := range order { if cont, found := v[ctype]; found && 0 < len(cont.References) { values = append(values, CveContentRefs{ Type: ctype, Value: cont.References, }) } } return } // CweIDs returns related CweIDs of the vulnerability func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) { order := CveContentTypes{NewCveContentType(myFamily)} order = append(order, AllCveContetTypes.Except(append(order)...)...) for _, ctype := range order { if cont, found := v[ctype]; found && 0 < len(cont.CweID) { // RedHat's OVAL sometimes contains multiple CWE-IDs separated by spaces for _, cweID := range strings.Fields(cont.CweID) { values = append(values, CveContentStr{ Type: ctype, Value: cweID, }) } } } return } // CveContent has abstraction of various vulnerability information type CveContent struct { Type CveContentType CveID string Title string Summary string Severity string Cvss2Score float64 Cvss2Vector string Cvss3Score float64 Cvss3Vector string SourceLink string Cpes []Cpe References References CweID string Published time.Time LastModified time.Time } // Empty checks the content is empty func (c CveContent) Empty() bool { return c.Summary == "" } // Cpe is Common Platform Enumeration type Cpe struct { CpeName string } // References is a slice of Reference type References []Reference // Find elements that matches the function passed in argument func (r References) Find(f func(r Reference) bool) (refs []Reference) { for _, rr := range r { refs = append(refs, rr) } return } // Reference has a related link of the CVE type Reference struct { Source string Link string RefID string } // 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 _, a := range as { if pack, ok := ps[a.Name]; ok { pack.NewVersion = a.NewVersion pack.NewRelease = a.NewRelease ps[a.Name] = pack } } } // Merge returns merged map (immutable) func (ps Packages) Merge(other Packages) Packages { merged := map[string]Package{} for k, v := range ps { merged[k] = v } for k, v := range other { merged[k] = v } return merged } // FormatVersionsFromTo returns updatable packages func (ps Packages) FormatVersionsFromTo() string { ss := []string{} for _, pack := range ps { ss = append(ss, pack.FormatVersionFromTo()) } return strings.Join(ss, "\n") } // FormatUpdatablePacksSummary returns a summary of updatable packages func (ps Packages) FormatUpdatablePacksSummary() string { nUpdatable := 0 for _, p := range ps { if p.NewVersion != "" { nUpdatable++ } } return fmt.Sprintf("%d updatable packages", nUpdatable) } // Package has installed packages. type Package struct { Name string Version string Release string NewVersion string NewRelease string Repository string Changelog Changelog NotFixedYet bool // Ubuntu OVAL Only } // FormatVer returns package name-version-release func (p Package) FormatVer() string { str := p.Name if 0 < len(p.Version) { str = fmt.Sprintf("%s-%s", str, p.Version) } if 0 < len(p.Release) { str = fmt.Sprintf("%s-%s", str, p.Release) } return str } // FormatNewVer returns package name-version-release func (p Package) FormatNewVer() string { str := p.Name if 0 < len(p.NewVersion) { str = fmt.Sprintf("%s-%s", str, p.NewVersion) } if 0 < len(p.NewRelease) { str = fmt.Sprintf("%s-%s", str, p.NewRelease) } return str } // FormatVersionFromTo formats installed and new package version func (p Package) FormatVersionFromTo() string { return fmt.Sprintf("%s -> %s", p.FormatVer(), p.FormatNewVer()) } // Changelog has contents of changelog and how to get it. // Method: modesl.detectionMethodStr type Changelog struct { Contents string Method string } // DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. type DistroAdvisory struct { AdvisoryID string Severity string Issued time.Time Updated time.Time } // Container has Container information type Container struct { ContainerID string Name string Image string Type string } // Platform has platform information type Platform struct { Name string // aws or azure or gcp or other... InstanceID string }