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