1package models
2
3import (
4	"bytes"
5	"fmt"
6	"reflect"
7	"regexp"
8	"strings"
9	"time"
10
11	"github.com/future-architect/vuls/config"
12	"github.com/future-architect/vuls/cwe"
13	"github.com/future-architect/vuls/util"
14)
15
16// ScanResults is a slide of ScanResult
17type ScanResults []ScanResult
18
19// ScanResult has the result of scanned CVE information.
20type ScanResult struct {
21	JSONVersion      int                   `json:"jsonVersion"`
22	Lang             string                `json:"lang"`
23	ServerUUID       string                `json:"serverUUID"`
24	ServerName       string                `json:"serverName"` // TOML Section key
25	Family           string                `json:"family"`
26	Release          string                `json:"release"`
27	Container        Container             `json:"container"`
28	Platform         Platform              `json:"platform"`
29	IPv4Addrs        []string              `json:"ipv4Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
30	IPv6Addrs        []string              `json:"ipv6Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
31	IPSIdentifiers   map[config.IPS]string `json:"ipsIdentifiers,omitempty"`
32	ScannedAt        time.Time             `json:"scannedAt"`
33	ScanMode         string                `json:"scanMode"`
34	ScannedVersion   string                `json:"scannedVersion"`
35	ScannedRevision  string                `json:"scannedRevision"`
36	ScannedBy        string                `json:"scannedBy"`
37	ScannedVia       string                `json:"scannedVia"`
38	ScannedIPv4Addrs []string              `json:"scannedIpv4Addrs,omitempty"`
39	ScannedIPv6Addrs []string              `json:"scannedIpv6Addrs,omitempty"`
40	ReportedAt       time.Time             `json:"reportedAt"`
41	ReportedVersion  string                `json:"reportedVersion"`
42	ReportedRevision string                `json:"reportedRevision"`
43	ReportedBy       string                `json:"reportedBy"`
44	Errors           []string              `json:"errors"`
45	Warnings         []string              `json:"warnings"`
46
47	ScannedCves       VulnInfos              `json:"scannedCves"`
48	RunningKernel     Kernel                 `json:"runningKernel"`
49	Packages          Packages               `json:"packages"`
50	SrcPackages       SrcPackages            `json:",omitempty"`
51	WordPressPackages *WordPressPackages     `json:",omitempty"`
52	LibraryScanners   LibraryScanners        `json:"libraries,omitempty"`
53	CweDict           CweDict                `json:"cweDict,omitempty"`
54	Optional          map[string]interface{} `json:",omitempty"`
55	Config            struct {
56		Scan   config.Config `json:"scan"`
57		Report config.Config `json:"report"`
58	} `json:"config"`
59}
60
61// CweDict is a dictionary for CWE
62type CweDict map[string]CweDictEntry
63
64// Get the name, url, top10URL for the specified cweID, lang
65func (c CweDict) Get(cweID, lang string) (name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL string) {
66	cweNum := strings.TrimPrefix(cweID, "CWE-")
67	switch config.Conf.Lang {
68	case "ja":
69		if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
70			top10Rank = dict.OwaspTopTen2017
71			top10URL = cwe.OwaspTopTen2017GitHubURLJa[dict.OwaspTopTen2017]
72		}
73		if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
74			cweTop25Rank = dict.CweTopTwentyfive2019
75			cweTop25URL = cwe.CweTopTwentyfive2019URL
76		}
77		if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
78			sansTop25Rank = dict.SansTopTwentyfive
79			sansTop25URL = cwe.SansTopTwentyfiveURL
80		}
81		if dict, ok := cwe.CweDictJa[cweNum]; ok {
82			name = dict.Name
83			url = fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
84		} else {
85			if dict, ok := cwe.CweDictEn[cweNum]; ok {
86				name = dict.Name
87			}
88			url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
89		}
90	default:
91		if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
92			top10Rank = dict.OwaspTopTen2017
93			top10URL = cwe.OwaspTopTen2017GitHubURLEn[dict.OwaspTopTen2017]
94		}
95		if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
96			cweTop25Rank = dict.CweTopTwentyfive2019
97			cweTop25URL = cwe.CweTopTwentyfive2019URL
98		}
99		if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
100			sansTop25Rank = dict.SansTopTwentyfive
101			sansTop25URL = cwe.SansTopTwentyfiveURL
102		}
103		url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
104		if dict, ok := cwe.CweDictEn[cweNum]; ok {
105			name = dict.Name
106		}
107	}
108	return
109}
110
111// CweDictEntry is a entry of CWE
112type CweDictEntry struct {
113	En                   *cwe.Cwe `json:"en,omitempty"`
114	Ja                   *cwe.Cwe `json:"ja,omitempty"`
115	OwaspTopTen2017      string   `json:"owaspTopTen2017"`
116	CweTopTwentyfive2019 string   `json:"cweTopTwentyfive2019"`
117	SansTopTwentyfive    string   `json:"sansTopTwentyfive"`
118}
119
120// Kernel has the Release, version and whether need restart
121type Kernel struct {
122	Release        string `json:"release"`
123	Version        string `json:"version"`
124	RebootRequired bool   `json:"rebootRequired"`
125}
126
127// FilterByCvssOver is filter function.
128func (r ScanResult) FilterByCvssOver(over float64) ScanResult {
129	filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
130		v2Max := v.MaxCvss2Score()
131		v3Max := v.MaxCvss3Score()
132		max := v2Max.Value.Score
133		if max < v3Max.Value.Score {
134			max = v3Max.Value.Score
135		}
136		if over <= max {
137			return true
138		}
139		return false
140	})
141	r.ScannedCves = filtered
142	return r
143}
144
145// FilterIgnoreCves is filter function.
146func (r ScanResult) FilterIgnoreCves() ScanResult {
147
148	ignoreCves := []string{}
149	if len(r.Container.Name) == 0 {
150		ignoreCves = config.Conf.Servers[r.ServerName].IgnoreCves
151	} else {
152		if s, ok := config.Conf.Servers[r.ServerName]; ok {
153			if con, ok := s.Containers[r.Container.Name]; ok {
154				ignoreCves = con.IgnoreCves
155			} else {
156				return r
157			}
158		} else {
159			util.Log.Errorf("%s is not found in config.toml",
160				r.ServerName)
161			return r
162		}
163	}
164
165	filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
166		for _, c := range ignoreCves {
167			if v.CveID == c {
168				return false
169			}
170		}
171		return true
172	})
173	r.ScannedCves = filtered
174	return r
175}
176
177// FilterUnfixed is filter function.
178func (r ScanResult) FilterUnfixed() ScanResult {
179	if !config.Conf.IgnoreUnfixed {
180		return r
181	}
182	filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
183		// Report cves detected by CPE because Vuls can't know 'fixed' or 'unfixed'
184		if len(v.CpeURIs) != 0 {
185			return true
186		}
187		NotFixedAll := true
188		for _, p := range v.AffectedPackages {
189			NotFixedAll = NotFixedAll && p.NotFixedYet
190		}
191		return !NotFixedAll
192	})
193	r.ScannedCves = filtered
194	return r
195}
196
197// FilterIgnorePkgs is filter function.
198func (r ScanResult) FilterIgnorePkgs() ScanResult {
199	var ignorePkgsRegexps []string
200	if len(r.Container.Name) == 0 {
201		ignorePkgsRegexps = config.Conf.Servers[r.ServerName].IgnorePkgsRegexp
202	} else {
203		if s, ok := config.Conf.Servers[r.ServerName]; ok {
204			if con, ok := s.Containers[r.Container.Name]; ok {
205				ignorePkgsRegexps = con.IgnorePkgsRegexp
206			} else {
207				return r
208			}
209		} else {
210			util.Log.Errorf("%s is not found in config.toml",
211				r.ServerName)
212			return r
213		}
214	}
215
216	regexps := []*regexp.Regexp{}
217	for _, pkgRegexp := range ignorePkgsRegexps {
218		re, err := regexp.Compile(pkgRegexp)
219		if err != nil {
220			util.Log.Errorf("Failed to parse %s. err: %+v", pkgRegexp, err)
221			continue
222		} else {
223			regexps = append(regexps, re)
224		}
225	}
226	if len(regexps) == 0 {
227		return r
228	}
229
230	filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
231		if len(v.AffectedPackages) == 0 {
232			return true
233		}
234		for _, p := range v.AffectedPackages {
235			match := false
236			for _, re := range regexps {
237				if re.MatchString(p.Name) {
238					match = true
239				}
240			}
241			if !match {
242				return true
243			}
244		}
245		return false
246	})
247
248	r.ScannedCves = filtered
249	return r
250}
251
252// FilterInactiveWordPressLibs is filter function.
253func (r ScanResult) FilterInactiveWordPressLibs() ScanResult {
254	if !config.Conf.Servers[r.ServerName].WordPress.IgnoreInactive {
255		return r
256	}
257
258	filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
259		if len(v.WpPackageFixStats) == 0 {
260			return true
261		}
262		// Ignore if all libs in this vulnInfo inactive
263		for _, wp := range v.WpPackageFixStats {
264			if p, ok := r.WordPressPackages.Find(wp.Name); ok {
265				if p.Status != Inactive {
266					return true
267				}
268			}
269		}
270		return false
271	})
272	r.ScannedCves = filtered
273	return r
274}
275
276// ReportFileName returns the filename on localhost without extension
277func (r ScanResult) ReportFileName() (name string) {
278	if len(r.Container.ContainerID) == 0 {
279		return fmt.Sprintf("%s", r.ServerName)
280	}
281	return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
282}
283
284// ReportKeyName returns the name of key on S3, Azure-Blob without extension
285func (r ScanResult) ReportKeyName() (name string) {
286	timestr := r.ScannedAt.Format(time.RFC3339)
287	if len(r.Container.ContainerID) == 0 {
288		return fmt.Sprintf("%s/%s", timestr, r.ServerName)
289	}
290	return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName)
291}
292
293// ServerInfo returns server name one line
294func (r ScanResult) ServerInfo() string {
295	if len(r.Container.ContainerID) == 0 {
296		return fmt.Sprintf("%s (%s%s)",
297			r.FormatServerName(), r.Family, r.Release)
298	}
299	return fmt.Sprintf(
300		"%s (%s%s) on %s",
301		r.FormatServerName(),
302		r.Family,
303		r.Release,
304		r.ServerName,
305	)
306}
307
308// ServerInfoTui returns server information for TUI sidebar
309func (r ScanResult) ServerInfoTui() string {
310	if len(r.Container.ContainerID) == 0 {
311		line := fmt.Sprintf("%s (%s%s)",
312			r.ServerName, r.Family, r.Release)
313		if len(r.Warnings) != 0 {
314			line = "[Warn] " + line
315		}
316		if r.RunningKernel.RebootRequired {
317			return "[Reboot] " + line
318		}
319		return line
320	}
321
322	fmtstr := "|-- %s (%s%s)"
323	if r.RunningKernel.RebootRequired {
324		fmtstr = "|-- [Reboot] %s (%s%s)"
325	}
326	return fmt.Sprintf(fmtstr, r.Container.Name, r.Family, r.Release)
327}
328
329// FormatServerName returns server and container name
330func (r ScanResult) FormatServerName() (name string) {
331	if len(r.Container.ContainerID) == 0 {
332		name = r.ServerName
333	} else {
334		name = fmt.Sprintf("%s@%s",
335			r.Container.Name, r.ServerName)
336	}
337	if r.RunningKernel.RebootRequired {
338		name = "[Reboot Required] " + name
339	}
340	return
341}
342
343// FormatTextReportHeader returns header of text report
344func (r ScanResult) FormatTextReportHeader() string {
345	var buf bytes.Buffer
346	for i := 0; i < len(r.ServerInfo()); i++ {
347		buf.WriteString("=")
348	}
349
350	return fmt.Sprintf("%s\n%s\n%s, %s, %s, %s, %s, %s\n",
351		r.ServerInfo(),
352		buf.String(),
353		r.ScannedCves.FormatCveSummary(),
354		r.ScannedCves.FormatFixedStatus(r.Packages),
355		r.FormatUpdatablePacksSummary(),
356		r.FormatExploitCveSummary(),
357		r.FormatMetasploitCveSummary(),
358		r.FormatAlertSummary(),
359	)
360}
361
362// FormatUpdatablePacksSummary returns a summary of updatable packages
363func (r ScanResult) FormatUpdatablePacksSummary() string {
364	if !r.isDisplayUpdatableNum() {
365		return fmt.Sprintf("%d installed", len(r.Packages))
366	}
367
368	nUpdatable := 0
369	for _, p := range r.Packages {
370		if p.NewVersion == "" {
371			continue
372		}
373		if p.Version != p.NewVersion || p.Release != p.NewRelease {
374			nUpdatable++
375		}
376	}
377	return fmt.Sprintf("%d installed, %d updatable",
378		len(r.Packages),
379		nUpdatable)
380}
381
382// FormatExploitCveSummary returns a summary of exploit cve
383func (r ScanResult) FormatExploitCveSummary() string {
384	nExploitCve := 0
385	for _, vuln := range r.ScannedCves {
386		if 0 < len(vuln.Exploits) {
387			nExploitCve++
388		}
389	}
390	return fmt.Sprintf("%d exploits", nExploitCve)
391}
392
393// FormatMetasploitCveSummary returns a summary of exploit cve
394func (r ScanResult) FormatMetasploitCveSummary() string {
395	nMetasploitCve := 0
396	for _, vuln := range r.ScannedCves {
397		if 0 < len(vuln.Metasploits) {
398			nMetasploitCve++
399		}
400	}
401	return fmt.Sprintf("%d modules", nMetasploitCve)
402}
403
404// FormatAlertSummary returns a summary of CERT alerts
405func (r ScanResult) FormatAlertSummary() string {
406	jaCnt := 0
407	enCnt := 0
408	for _, vuln := range r.ScannedCves {
409		if len(vuln.AlertDict.En) > 0 {
410			enCnt += len(vuln.AlertDict.En)
411		}
412		if len(vuln.AlertDict.Ja) > 0 {
413			jaCnt += len(vuln.AlertDict.Ja)
414		}
415	}
416	return fmt.Sprintf("en: %d, ja: %d alerts", enCnt, jaCnt)
417}
418
419func (r ScanResult) isDisplayUpdatableNum() bool {
420	if r.Family == config.FreeBSD {
421		return false
422	}
423
424	var mode config.ScanMode
425	s, _ := config.Conf.Servers[r.ServerName]
426	mode = s.Mode
427
428	if mode.IsOffline() {
429		return false
430	}
431	if mode.IsFastRoot() || mode.IsDeep() {
432		return true
433	}
434	if mode.IsFast() {
435		switch r.Family {
436		case config.RedHat,
437			config.Oracle,
438			config.Debian,
439			config.Ubuntu,
440			config.Raspbian:
441			return false
442		default:
443			return true
444		}
445	}
446	return false
447}
448
449// IsContainer returns whether this ServerInfo is about container
450func (r ScanResult) IsContainer() bool {
451	return 0 < len(r.Container.ContainerID)
452}
453
454// IsDeepScanMode checks if the scan mode is deep scan mode.
455func (r ScanResult) IsDeepScanMode() bool {
456	for _, s := range r.Config.Scan.Servers {
457		for _, m := range s.ScanMode {
458			if m == "deep" {
459				return true
460			}
461		}
462	}
463	return false
464}
465
466// Container has Container information
467type Container struct {
468	ContainerID string `json:"containerID"`
469	Name        string `json:"name"`
470	Image       string `json:"image"`
471	Type        string `json:"type"`
472	UUID        string `json:"uuid"`
473}
474
475// Platform has platform information
476type Platform struct {
477	Name       string `json:"name"` // aws or azure or gcp or other...
478	InstanceID string `json:"instanceID"`
479}
480
481// RemoveRaspbianPackFromResult is for Raspberry Pi and removes the Raspberry Pi dedicated package from ScanResult.
482func (r ScanResult) RemoveRaspbianPackFromResult() ScanResult {
483	if r.Family != config.Raspbian {
484		return r
485	}
486
487	result := r
488	packs := make(Packages)
489	for _, pack := range r.Packages {
490		if !IsRaspbianPackage(pack.Name, pack.Version) {
491			packs[pack.Name] = pack
492		}
493	}
494	srcPacks := make(SrcPackages)
495	for _, pack := range r.SrcPackages {
496		if !IsRaspbianPackage(pack.Name, pack.Version) {
497			srcPacks[pack.Name] = pack
498
499		}
500	}
501
502	result.Packages = packs
503	result.SrcPackages = srcPacks
504
505	return result
506}
507
508func (r ScanResult) ClearFields(targetTagNames []string) ScanResult {
509	if len(targetTagNames) == 0 {
510		return r
511	}
512	target := map[string]bool{}
513	for _, n := range targetTagNames {
514		target[strings.ToLower(n)] = true
515	}
516	t := reflect.ValueOf(r).Type()
517	for i := 0; i < t.NumField(); i++ {
518		f := t.Field(i)
519		jsonValue := strings.Split(f.Tag.Get("json"), ",")[0]
520		if ok := target[strings.ToLower(jsonValue)]; ok {
521			vv := reflect.New(f.Type).Elem().Interface()
522			reflect.ValueOf(&r).Elem().FieldByName(f.Name).Set(reflect.ValueOf(vv))
523		}
524	}
525	return r
526}
527