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