1package models
2
3import (
4	"bytes"
5	"fmt"
6	"sort"
7	"strings"
8	"time"
9
10	"github.com/future-architect/vuls/config"
11	exploitmodels "github.com/mozqnet/go-exploitdb/models"
12)
13
14// VulnInfos has a map of VulnInfo
15// Key: CveID
16type VulnInfos map[string]VulnInfo
17
18// Find elements that matches the function passed in argument
19func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos {
20	filtered := VulnInfos{}
21	for _, vv := range v {
22		if f(vv) {
23			filtered[vv.CveID] = vv
24		}
25	}
26	return filtered
27}
28
29// FindScoredVulns return scored vulnerabilities
30func (v VulnInfos) FindScoredVulns() VulnInfos {
31	return v.Find(func(vv VulnInfo) bool {
32		if 0 < vv.MaxCvss2Score().Value.Score ||
33			0 < vv.MaxCvss3Score().Value.Score {
34			return true
35		}
36		return false
37	})
38}
39
40// ToSortedSlice returns slice of VulnInfos that is sorted by Score, CVE-ID
41func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) {
42	for k := range v {
43		sorted = append(sorted, v[k])
44	}
45	sort.Slice(sorted, func(i, j int) bool {
46		maxI := sorted[i].MaxCvssScore()
47		maxJ := sorted[j].MaxCvssScore()
48		if maxI.Value.Score != maxJ.Value.Score {
49			return maxJ.Value.Score < maxI.Value.Score
50		}
51		return sorted[i].CveID < sorted[j].CveID
52	})
53	return
54}
55
56// CountGroupBySeverity summarize the number of CVEs group by CVSSv2 Severity
57func (v VulnInfos) CountGroupBySeverity() map[string]int {
58	m := map[string]int{}
59	for _, vInfo := range v {
60		score := vInfo.MaxCvss2Score().Value.Score
61		if score < 0.1 {
62			score = vInfo.MaxCvss3Score().Value.Score
63		}
64		switch {
65		case 7.0 <= score:
66			m["High"]++
67		case 4.0 <= score:
68			m["Medium"]++
69		case 0 < score:
70			m["Low"]++
71		default:
72			m["Unknown"]++
73		}
74	}
75	return m
76}
77
78// FormatCveSummary summarize the number of CVEs group by CVSSv2 Severity
79func (v VulnInfos) FormatCveSummary() string {
80	m := v.CountGroupBySeverity()
81
82	if config.Conf.IgnoreUnscoredCves {
83		return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
84			m["High"]+m["Medium"]+m["Low"], m["High"], m["Medium"], m["Low"])
85	}
86	return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
87		m["High"]+m["Medium"]+m["Low"]+m["Unknown"],
88		m["High"], m["Medium"], m["Low"], m["Unknown"])
89}
90
91// FormatFixedStatus summarize the number of cves are fixed.
92func (v VulnInfos) FormatFixedStatus(packs Packages) string {
93	total, fixed := 0, 0
94	for _, vInfo := range v {
95		if len(vInfo.CpeURIs) != 0 {
96			continue
97		}
98		total++
99		if vInfo.PatchStatus(packs) == "fixed" {
100			fixed++
101		}
102	}
103	return fmt.Sprintf("%d/%d Fixed", fixed, total)
104}
105
106// PackageFixStatuses is a list of PackageStatus
107type PackageFixStatuses []PackageFixStatus
108
109// Names return a slice of package names
110func (ps PackageFixStatuses) Names() (names []string) {
111	for _, p := range ps {
112		names = append(names, p.Name)
113	}
114	return names
115}
116
117// Store insert given pkg if missing, update pkg if exists
118func (ps PackageFixStatuses) Store(pkg PackageFixStatus) PackageFixStatuses {
119	for i, p := range ps {
120		if p.Name == pkg.Name {
121			ps[i] = pkg
122			return ps
123		}
124	}
125	ps = append(ps, pkg)
126	return ps
127}
128
129// Sort by Name
130func (ps PackageFixStatuses) Sort() {
131	sort.Slice(ps, func(i, j int) bool {
132		return ps[i].Name < ps[j].Name
133	})
134	return
135}
136
137// PackageFixStatus has name and other status abount the package
138type PackageFixStatus struct {
139	Name        string `json:"name,omitempty"`
140	NotFixedYet bool   `json:"notFixedYet,omitempty"`
141	FixState    string `json:"fixState,omitempty"`
142	FixedIn     string `json:"fixedIn,omitempty"`
143}
144
145// VulnInfo has a vulnerability information and unsecure packages
146type VulnInfo struct {
147	CveID                string               `json:"cveID,omitempty"`
148	Confidences          Confidences          `json:"confidences,omitempty"`
149	AffectedPackages     PackageFixStatuses   `json:"affectedPackages,omitempty"`
150	DistroAdvisories     DistroAdvisories     `json:"distroAdvisories,omitempty"` // for Aamazon, RHEL, FreeBSD
151	CveContents          CveContents          `json:"cveContents,omitempty"`
152	Exploits             []Exploit            `json:"exploits,omitempty"`
153	Metasploits          []Metasploit         `json:"metasploits,omitempty"`
154	AlertDict            AlertDict            `json:"alertDict,omitempty"`
155	CpeURIs              []string             `json:"cpeURIs,omitempty"` // CpeURIs related to this CVE defined in config.toml
156	GitHubSecurityAlerts GitHubSecurityAlerts `json:"gitHubSecurityAlerts,omitempty"`
157	WpPackageFixStats    WpPackageFixStats    `json:"wpPackageFixStats,omitempty"`
158	LibraryFixedIns      LibraryFixedIns      `json:"libraryFixedIns,omitempty"`
159
160	VulnType string `json:"vulnType,omitempty"`
161}
162
163// Alert has XCERT alert information
164type Alert struct {
165	URL   string `json:"url,omitempty"`
166	Title string `json:"title,omitempty"`
167	Team  string `json:"team,omitempty"`
168}
169
170// GitHubSecurityAlerts is a list of GitHubSecurityAlert
171type GitHubSecurityAlerts []GitHubSecurityAlert
172
173// Add adds given arg to the slice and return the slice (immutable)
174func (g GitHubSecurityAlerts) Add(alert GitHubSecurityAlert) GitHubSecurityAlerts {
175	for _, a := range g {
176		if a.PackageName == alert.PackageName {
177			return g
178		}
179	}
180	return append(g, alert)
181}
182
183// Names return a slice of lib names
184func (g GitHubSecurityAlerts) Names() (names []string) {
185	for _, a := range g {
186		names = append(names, a.PackageName)
187	}
188	return names
189}
190
191// GitHubSecurityAlert has detected CVE-ID, PackageName, Status fetched via GitHub API
192type GitHubSecurityAlert struct {
193	PackageName   string    `json:"packageName"`
194	FixedIn       string    `json:"fixedIn"`
195	AffectedRange string    `json:"affectedRange"`
196	Dismissed     bool      `json:"dismissed"`
197	DismissedAt   time.Time `json:"dismissedAt"`
198	DismissReason string    `json:"dismissReason"`
199}
200
201// LibraryFixedIns is a list of Library's FixedIn
202type LibraryFixedIns []LibraryFixedIn
203
204// Names return a slice of names
205func (lfs LibraryFixedIns) Names() (names []string) {
206	for _, lf := range lfs {
207		names = append(names, lf.Name)
208	}
209	return names
210}
211
212// WpPackageFixStats is a list of WpPackageFixStatus
213type WpPackageFixStats []WpPackageFixStatus
214
215// Names return a slice of names
216func (ws WpPackageFixStats) Names() (names []string) {
217	for _, w := range ws {
218		names = append(names, w.Name)
219	}
220	return names
221}
222
223// WpPackages has a list of WpPackage
224type WpPackages []WpPackage
225
226// Add adds given arg to the slice and return the slice (immutable)
227func (g WpPackages) Add(pkg WpPackage) WpPackages {
228	for _, a := range g {
229		if a.Name == pkg.Name {
230			return g
231		}
232	}
233	return append(g, pkg)
234}
235
236// Titles returns tilte (TUI)
237func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) {
238	if lang == "ja" {
239		if cont, found := v.CveContents[Jvn]; found && 0 < len(cont.Title) {
240			values = append(values, CveContentStr{Jvn, cont.Title})
241		}
242	}
243
244	// RedHat API has one line title.
245	if cont, found := v.CveContents[RedHatAPI]; found && 0 < len(cont.Title) {
246		values = append(values, CveContentStr{RedHatAPI, cont.Title})
247	}
248
249	order := CveContentTypes{Trivy, Nvd, NvdXML, NewCveContentType(myFamily)}
250	order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...)
251	for _, ctype := range order {
252		// Only JVN has meaningful title. so return first 100 char of summary
253		if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Summary) {
254			summary := strings.Replace(cont.Summary, "\n", " ", -1)
255			values = append(values, CveContentStr{
256				Type:  ctype,
257				Value: summary,
258			})
259		}
260	}
261
262	for _, adv := range v.DistroAdvisories {
263		values = append(values, CveContentStr{
264			Type:  "Vendor",
265			Value: strings.Replace(adv.Description, "\n", " ", -1),
266		})
267	}
268
269	if len(values) == 0 {
270		values = []CveContentStr{{
271			Type:  Unknown,
272			Value: "-",
273		}}
274	}
275	return
276}
277
278// Summaries returns summaries
279func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
280	if lang == "ja" {
281		if cont, found := v.CveContents[Jvn]; found && 0 < len(cont.Summary) {
282			summary := cont.Title
283			summary += "\n" + strings.Replace(
284				strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1)
285			values = append(values, CveContentStr{Jvn, summary})
286		}
287	}
288
289	order := CveContentTypes{Trivy, NewCveContentType(myFamily), Nvd, NvdXML}
290	order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...)
291	for _, ctype := range order {
292		if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Summary) {
293			summary := strings.Replace(cont.Summary, "\n", " ", -1)
294			values = append(values, CveContentStr{
295				Type:  ctype,
296				Value: summary,
297			})
298		}
299	}
300
301	for _, adv := range v.DistroAdvisories {
302		values = append(values, CveContentStr{
303			Type:  "Vendor",
304			Value: adv.Description,
305		})
306	}
307
308	if v, ok := v.CveContents[WPVulnDB]; ok {
309		values = append(values, CveContentStr{
310			Type:  "WPVDB",
311			Value: v.Title,
312		})
313	}
314
315	if len(values) == 0 {
316		return []CveContentStr{{
317			Type:  Unknown,
318			Value: "-",
319		}}
320	}
321
322	return
323}
324
325// Mitigations returns mitigations
326func (v VulnInfo) Mitigations(myFamily string) (values []CveContentStr) {
327	order := CveContentTypes{RedHatAPI}
328	for _, ctype := range order {
329		if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Mitigation) {
330			values = append(values, CveContentStr{
331				Type:  ctype,
332				Value: cont.Mitigation,
333			})
334		}
335	}
336
337	if len(values) == 0 {
338		return []CveContentStr{{
339			Type:  Unknown,
340			Value: "-",
341		}}
342	}
343	return
344}
345
346// Cvss2Scores returns CVSS V2 Scores
347func (v VulnInfo) Cvss2Scores(myFamily string) (values []CveContentCvss) {
348	order := []CveContentType{Nvd, NvdXML, RedHatAPI, RedHat, Jvn}
349	if myFamily != config.RedHat && myFamily != config.CentOS {
350		order = append(order, NewCveContentType(myFamily))
351	}
352	for _, ctype := range order {
353		if cont, found := v.CveContents[ctype]; found {
354			if cont.Cvss2Score == 0 || cont.Cvss2Severity == "" {
355				continue
356			}
357			// https://nvd.nist.gov/vuln-metrics/cvss
358			values = append(values, CveContentCvss{
359				Type: ctype,
360				Value: Cvss{
361					Type:     CVSS2,
362					Score:    cont.Cvss2Score,
363					Vector:   cont.Cvss2Vector,
364					Severity: strings.ToUpper(cont.Cvss2Severity),
365				},
366			})
367		}
368	}
369
370	for _, adv := range v.DistroAdvisories {
371		if adv.Severity != "" {
372			values = append(values, CveContentCvss{
373				Type: "Advisory",
374				Value: Cvss{
375					Type:                 CVSS2,
376					Score:                severityToV2ScoreRoughly(adv.Severity),
377					CalculatedBySeverity: true,
378					Vector:               "-",
379					Severity:             strings.ToUpper(adv.Severity),
380				},
381			})
382		}
383	}
384
385	// An OVAL entry in Ubuntu and Debian has only severity (CVSS score isn't included).
386	// Show severity and dummy score calculated roughly.
387	order = append(order, AllCveContetTypes.Except(order...)...)
388	for _, ctype := range order {
389		if cont, found := v.CveContents[ctype]; found &&
390			cont.Cvss2Score == 0 &&
391			cont.Cvss3Score == 0 &&
392			cont.Cvss2Severity != "" {
393
394			values = append(values, CveContentCvss{
395				Type: cont.Type,
396				Value: Cvss{
397					Type:                 CVSS2,
398					Score:                severityToV2ScoreRoughly(cont.Cvss2Severity),
399					CalculatedBySeverity: true,
400					Vector:               "-",
401					Severity:             strings.ToUpper(cont.Cvss2Severity),
402				},
403			})
404		}
405	}
406
407	return
408}
409
410// Cvss3Scores returns CVSS V3 Score
411func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) {
412	order := []CveContentType{Nvd, RedHatAPI, RedHat, Jvn}
413	for _, ctype := range order {
414		if cont, found := v.CveContents[ctype]; found {
415			// https://nvd.nist.gov/vuln-metrics/cvss
416			values = append(values, CveContentCvss{
417				Type: ctype,
418				Value: Cvss{
419					Type:     CVSS3,
420					Score:    cont.Cvss3Score,
421					Vector:   cont.Cvss3Vector,
422					Severity: strings.ToUpper(cont.Cvss3Severity),
423				},
424			})
425		}
426	}
427
428	if cont, found := v.CveContents[Trivy]; found && cont.Cvss3Severity != "" {
429		values = append(values, CveContentCvss{
430			Type: Trivy,
431			Value: Cvss{
432				Type:     CVSS3,
433				Score:    severityToV2ScoreRoughly(cont.Cvss3Severity),
434				Severity: strings.ToUpper(cont.Cvss3Severity),
435			},
436		})
437	}
438
439	return
440}
441
442// MaxCvss3Score returns Max CVSS V3 Score
443func (v VulnInfo) MaxCvss3Score() CveContentCvss {
444	order := []CveContentType{Nvd, RedHat, RedHatAPI, Jvn}
445	max := 0.0
446	value := CveContentCvss{
447		Type:  Unknown,
448		Value: Cvss{Type: CVSS3},
449	}
450	for _, ctype := range order {
451		if cont, found := v.CveContents[ctype]; found && max < cont.Cvss3Score {
452			// https://nvd.nist.gov/vuln-metrics/cvss
453			value = CveContentCvss{
454				Type: ctype,
455				Value: Cvss{
456					Type:     CVSS3,
457					Score:    cont.Cvss3Score,
458					Vector:   cont.Cvss3Vector,
459					Severity: strings.ToUpper(cont.Cvss3Severity),
460				},
461			}
462			max = cont.Cvss3Score
463		}
464	}
465	return value
466}
467
468// MaxCvssScore returns max CVSS Score
469// If there is no CVSS Score, return Severity as a numerical value.
470func (v VulnInfo) MaxCvssScore() CveContentCvss {
471	v3Max := v.MaxCvss3Score()
472	v2Max := v.MaxCvss2Score()
473	max := v3Max
474	if max.Type == Unknown {
475		return v2Max
476	}
477
478	if max.Value.Score < v2Max.Value.Score && !v2Max.Value.CalculatedBySeverity {
479		max = v2Max
480	}
481	return max
482}
483
484// MaxCvss2Score returns Max CVSS V2 Score
485func (v VulnInfo) MaxCvss2Score() CveContentCvss {
486	order := []CveContentType{Nvd, NvdXML, RedHat, RedHatAPI, Jvn}
487	max := 0.0
488	value := CveContentCvss{
489		Type:  Unknown,
490		Value: Cvss{Type: CVSS2},
491	}
492	for _, ctype := range order {
493		if cont, found := v.CveContents[ctype]; found && max < cont.Cvss2Score {
494			// https://nvd.nist.gov/vuln-metrics/cvss
495			value = CveContentCvss{
496				Type: ctype,
497				Value: Cvss{
498					Type:     CVSS2,
499					Score:    cont.Cvss2Score,
500					Vector:   cont.Cvss2Vector,
501					Severity: strings.ToUpper(cont.Cvss2Severity),
502				},
503			}
504			max = cont.Cvss2Score
505		}
506	}
507	if 0 < max {
508		return value
509	}
510
511	// If CVSS score isn't on NVD, RedHat and JVN, use OVAL and advisory Severity.
512	// Convert severity to cvss srore roughly, then returns max severity.
513	// Only Ubuntu, RedHat and Oracle have severity data in OVAL.
514	order = []CveContentType{Ubuntu, RedHat, Oracle}
515	for _, ctype := range order {
516		if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Cvss2Severity) {
517			score := severityToV2ScoreRoughly(cont.Cvss2Severity)
518			if max < score {
519				value = CveContentCvss{
520					Type: ctype,
521					Value: Cvss{
522						Type:                 CVSS2,
523						Score:                score,
524						CalculatedBySeverity: true,
525						Vector:               cont.Cvss2Vector,
526						Severity:             strings.ToUpper(cont.Cvss2Severity),
527					},
528				}
529			}
530			max = score
531		}
532	}
533
534	// Only RedHat, Oracle and Amazon has severity data in advisory.
535	for _, adv := range v.DistroAdvisories {
536		if adv.Severity != "" {
537			score := severityToV2ScoreRoughly(adv.Severity)
538			if max < score {
539				value = CveContentCvss{
540					Type: "Vendor",
541					Value: Cvss{
542						Type:                 CVSS2,
543						Score:                score,
544						CalculatedBySeverity: true,
545						Vector:               "-",
546						Severity:             adv.Severity,
547					},
548				}
549			}
550		}
551	}
552	return value
553}
554
555// AttackVector returns attack vector string
556func (v VulnInfo) AttackVector() string {
557	for _, cnt := range v.CveContents {
558		if strings.HasPrefix(cnt.Cvss2Vector, "AV:N") ||
559			strings.Contains(cnt.Cvss3Vector, "AV:N") {
560			return "AV:N"
561		} else if strings.HasPrefix(cnt.Cvss2Vector, "AV:A") ||
562			strings.Contains(cnt.Cvss3Vector, "AV:A") {
563			return "AV:A"
564		} else if strings.HasPrefix(cnt.Cvss2Vector, "AV:L") ||
565			strings.Contains(cnt.Cvss3Vector, "AV:L") {
566			return "AV:L"
567		} else if strings.Contains(cnt.Cvss3Vector, "AV:P") {
568			// no AV:P in CVSS v2
569			return "AV:P"
570		}
571	}
572	if cont, found := v.CveContents[DebianSecurityTracker]; found {
573		if attackRange, found := cont.Optional["attack range"]; found {
574			return attackRange
575		}
576	}
577	return ""
578}
579
580// PatchStatus returns fixed or unfixed string
581func (v VulnInfo) PatchStatus(packs Packages) string {
582	// Vuls don't know patch status of the CPE
583	if len(v.CpeURIs) != 0 {
584		return ""
585	}
586	for _, p := range v.AffectedPackages {
587		if p.NotFixedYet {
588			return "unfixed"
589		}
590
591		// Fast and offline mode can not get the candidate version.
592		// Vuls can be considered as 'fixed' if not-fixed-yet==true and
593		// the fixed-in-version (information in the oval) is not an empty.
594		if p.FixedIn != "" {
595			continue
596		}
597
598		// fast, offline mode doesn't have new version
599		if pack, ok := packs[p.Name]; ok {
600			if pack.NewVersion == "" {
601				return "unknown"
602			}
603		}
604	}
605	return "fixed"
606}
607
608// CveContentCvss has CVSS information
609type CveContentCvss struct {
610	Type  CveContentType `json:"type"`
611	Value Cvss           `json:"value"`
612}
613
614// CvssType Represent the type of CVSS
615type CvssType string
616
617const (
618	// CVSS2 means CVSS vesion2
619	CVSS2 CvssType = "2"
620
621	// CVSS3 means CVSS vesion3
622	CVSS3 CvssType = "3"
623)
624
625// Cvss has CVSS Score
626type Cvss struct {
627	Type                 CvssType `json:"type"`
628	Score                float64  `json:"score"`
629	CalculatedBySeverity bool     `json:"calculatedBySeverity"`
630	Vector               string   `json:"vector"`
631	Severity             string   `json:"severity"`
632}
633
634// Format CVSS Score and Vector
635func (c Cvss) Format() string {
636	if c.Score == 0 || c.Vector == "" {
637		return c.Severity
638	}
639	switch c.Type {
640	case CVSS2:
641		return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
642	case CVSS3:
643		return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
644	}
645	return ""
646}
647
648// Amazon Linux Security Advisory
649// Critical, Important, Medium, Low
650// https://alas.aws.amazon.com/
651//
652// RedHat, Oracle OVAL
653// Critical, Important, Moderate, Low
654// https://access.redhat.com/security/updates/classification
655//
656// Ubuntu OVAL
657// Critical, High, Medium, Low
658// https://wiki.ubuntu.com/Bugs/Importance
659// https://people.canonical.com/~ubuntu-security/cve/priority.html
660func severityToV2ScoreRoughly(severity string) float64 {
661	switch strings.ToUpper(severity) {
662	case "CRITICAL":
663		return 10.0
664	case "IMPORTANT", "HIGH":
665		return 8.9
666	case "MODERATE", "MEDIUM":
667		return 6.9
668	case "LOW":
669		return 3.9
670	}
671	return 0
672}
673
674// FormatMaxCvssScore returns Max CVSS Score
675func (v VulnInfo) FormatMaxCvssScore() string {
676	max := v.MaxCvssScore()
677	return fmt.Sprintf("%3.1f %s (%s)",
678		max.Value.Score,
679		strings.ToUpper(max.Value.Severity),
680		max.Type)
681}
682
683// Cvss2CalcURL returns CVSS v2 caluclator's URL
684func (v VulnInfo) Cvss2CalcURL() string {
685	return "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=" + v.CveID
686}
687
688// Cvss3CalcURL returns CVSS v3 caluclator's URL
689func (v VulnInfo) Cvss3CalcURL() string {
690	return "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=" + v.CveID
691}
692
693// VendorLinks returns links of vendor support's URL
694func (v VulnInfo) VendorLinks(family string) map[string]string {
695	links := map[string]string{}
696	if strings.HasPrefix(v.CveID, "WPVDBID") {
697		links["WPVulnDB"] = fmt.Sprintf("https://wpvulndb.com/vulnerabilities/%s",
698			strings.TrimPrefix(v.CveID, "WPVDBID-"))
699		return links
700	}
701
702	switch family {
703	case config.RedHat, config.CentOS:
704		links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID
705		for _, advisory := range v.DistroAdvisories {
706			aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1)
707			links[advisory.AdvisoryID] = fmt.Sprintf("https://rhn.redhat.com/errata/%s.html", aidURL)
708		}
709		return links
710	case config.Oracle:
711		links["Oracle-CVE"] = fmt.Sprintf("https://linux.oracle.com/cve/%s.html", v.CveID)
712		for _, advisory := range v.DistroAdvisories {
713			links[advisory.AdvisoryID] =
714				fmt.Sprintf("https://linux.oracle.com/errata/%s.html", advisory.AdvisoryID)
715		}
716		return links
717	case config.Amazon:
718		links["RHEL-CVE"] = "https://access.redhat.com/security/cve/" + v.CveID
719		for _, advisory := range v.DistroAdvisories {
720			if strings.HasPrefix(advisory.AdvisoryID, "ALAS2") {
721				links[advisory.AdvisoryID] =
722					fmt.Sprintf("https://alas.aws.amazon.com/AL2/%s.html",
723						strings.Replace(advisory.AdvisoryID, "ALAS2", "ALAS", -1))
724			} else {
725				links[advisory.AdvisoryID] =
726					fmt.Sprintf("https://alas.aws.amazon.com/%s.html", advisory.AdvisoryID)
727			}
728		}
729		return links
730	case config.Ubuntu:
731		links["Ubuntu-CVE"] = "http://people.ubuntu.com/~ubuntu-security/cve/" + v.CveID
732		return links
733	case config.Debian:
734		links["Debian-CVE"] = "https://security-tracker.debian.org/tracker/" + v.CveID
735	case config.SUSEEnterpriseServer:
736		links["SUSE-CVE"] = "https://www.suse.com/security/cve/" + v.CveID
737	case config.FreeBSD:
738		for _, advisory := range v.DistroAdvisories {
739			links["FreeBSD-VuXML"] = fmt.Sprintf("https://vuxml.freebsd.org/freebsd/%s.html", advisory.AdvisoryID)
740
741		}
742		return links
743	}
744	return links
745}
746
747// DistroAdvisories is a list of DistroAdvisory
748type DistroAdvisories []DistroAdvisory
749
750// AppendIfMissing appends if missing
751func (advs *DistroAdvisories) AppendIfMissing(adv *DistroAdvisory) bool {
752	for _, a := range *advs {
753		if a.AdvisoryID == adv.AdvisoryID {
754			return false
755		}
756	}
757	*advs = append(*advs, *adv)
758	return true
759}
760
761// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information.
762type DistroAdvisory struct {
763	AdvisoryID  string    `json:"advisoryID"`
764	Severity    string    `json:"severity"`
765	Issued      time.Time `json:"issued"`
766	Updated     time.Time `json:"updated"`
767	Description string    `json:"description"`
768}
769
770// Format the distro advisory information
771func (p DistroAdvisory) Format() string {
772	if p.AdvisoryID == "" {
773		return ""
774	}
775
776	var delim bytes.Buffer
777	for i := 0; i < len(p.AdvisoryID); i++ {
778		delim.WriteString("-")
779	}
780	buf := []string{p.AdvisoryID, delim.String(), p.Description}
781	return strings.Join(buf, "\n")
782}
783
784// Exploit :
785type Exploit struct {
786	ExploitType  exploitmodels.ExploitType `json:"exploitType"`
787	ID           string                    `json:"id"`
788	URL          string                    `json:"url"`
789	Description  string                    `json:"description"`
790	DocumentURL  *string                   `json:"documentURL,omitempty"`
791	ShellCodeURL *string                   `json:"shellCodeURL,omitempty"`
792	BinaryURL    *string                   `json:"binaryURL,omitempty"`
793}
794
795// Metasploit :
796type Metasploit struct {
797	Name        string   `json:"name"`
798	Title       string   `json:"title"`
799	Description string   `json:"description,omitempty"`
800	URLs        []string `json:",omitempty"`
801}
802
803// AlertDict has target cve's JPCERT and USCERT alert data
804type AlertDict struct {
805	Ja []Alert `json:"ja"`
806	En []Alert `json:"en"`
807}
808
809// FormatSource returns which source has this alert
810func (a AlertDict) FormatSource() string {
811	s := []string{}
812	if len(a.En) != 0 {
813		s = append(s, "USCERT")
814	}
815	if len(a.Ja) != 0 {
816		s = append(s, "JPCERT")
817	}
818	return strings.Join(s, "/")
819}
820
821// Confidences is a list of Confidence
822type Confidences []Confidence
823
824// AppendIfMissing appends confidence to the list if missiong
825func (cs *Confidences) AppendIfMissing(confidence Confidence) {
826	for _, c := range *cs {
827		if c.DetectionMethod == confidence.DetectionMethod {
828			return
829		}
830	}
831	*cs = append(*cs, confidence)
832}
833
834// SortByConfident sorts Confidences
835func (cs Confidences) SortByConfident() Confidences {
836	sort.Slice(cs, func(i, j int) bool {
837		return cs[i].SortOrder < cs[j].SortOrder
838	})
839	return cs
840}
841
842// Confidence is a ranking how confident the CVE-ID was deteted correctly
843// Score: 0 - 100
844type Confidence struct {
845	Score           int             `json:"score"`
846	DetectionMethod DetectionMethod `json:"detectionMethod"`
847	SortOrder       int             `json:"-"`
848}
849
850func (c Confidence) String() string {
851	return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod)
852}
853
854// DetectionMethod indicates
855// - How to detect the CveID
856// - How to get the changelog difference between installed and candidate version
857type DetectionMethod string
858
859const (
860	// CpeNameMatchStr is a String representation of CpeNameMatch
861	CpeNameMatchStr = "CpeNameMatch"
862
863	// YumUpdateSecurityMatchStr is a String representation of YumUpdateSecurityMatch
864	YumUpdateSecurityMatchStr = "YumUpdateSecurityMatch"
865
866	// PkgAuditMatchStr is a String representation of PkgAuditMatch
867	PkgAuditMatchStr = "PkgAuditMatch"
868
869	// OvalMatchStr is a String representation of OvalMatch
870	OvalMatchStr = "OvalMatch"
871
872	// RedHatAPIStr is a String representation of RedHatAPIMatch
873	RedHatAPIStr = "RedHatAPIMatch"
874
875	// DebianSecurityTrackerMatchStr is a String representation of DebianSecurityTrackerMatch
876	DebianSecurityTrackerMatchStr = "DebianSecurityTrackerMatch"
877
878	// TrivyMatchStr is a String representation of Trivy
879	TrivyMatchStr = "TrivyMatch"
880
881	// ChangelogExactMatchStr is a String representation of ChangelogExactMatch
882	ChangelogExactMatchStr = "ChangelogExactMatch"
883
884	// ChangelogLenientMatchStr is a String representation of ChangelogLenientMatch
885	ChangelogLenientMatchStr = "ChangelogLenientMatch"
886
887	// GitHubMatchStr is a String representation of GitHubMatch
888	GitHubMatchStr = "GitHubMatch"
889
890	// WPVulnDBMatchStr is a String representation of WordPress VulnDB scanning
891	WPVulnDBMatchStr = "WPVulnDBMatch"
892
893	// FailedToGetChangelog is a String representation of FailedToGetChangelog
894	FailedToGetChangelog = "FailedToGetChangelog"
895
896	// FailedToFindVersionInChangelog is a String representation of FailedToFindVersionInChangelog
897	FailedToFindVersionInChangelog = "FailedToFindVersionInChangelog"
898)
899
900var (
901	// CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly
902	CpeNameMatch = Confidence{100, CpeNameMatchStr, 1}
903
904	// YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly
905	YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr, 2}
906
907	// PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly
908	PkgAuditMatch = Confidence{100, PkgAuditMatchStr, 2}
909
910	// OvalMatch is a ranking how confident the CVE-ID was deteted correctly
911	OvalMatch = Confidence{100, OvalMatchStr, 0}
912
913	// RedHatAPIMatch ranking how confident the CVE-ID was deteted correctly
914	RedHatAPIMatch = Confidence{100, RedHatAPIStr, 0}
915
916	// DebianSecurityTrackerMatch ranking how confident the CVE-ID was deteted correctly
917	DebianSecurityTrackerMatch = Confidence{100, DebianSecurityTrackerMatchStr, 0}
918
919	// TrivyMatch ranking how confident the CVE-ID was deteted correctly
920	TrivyMatch = Confidence{100, TrivyMatchStr, 0}
921
922	// ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly
923	ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr, 3}
924
925	// ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly
926	ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr, 4}
927
928	// GitHubMatch is a ranking how confident the CVE-ID was deteted correctly
929	GitHubMatch = Confidence{97, GitHubMatchStr, 2}
930
931	// WPVulnDBMatch is a ranking how confident the CVE-ID was deteted correctly
932	WPVulnDBMatch = Confidence{100, WPVulnDBMatchStr, 0}
933)
934