Completed
Pull Request — master (#769)
by
unknown
11:45
created

scan/base.go   F

Size/Duplication

Total Lines 833
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 189
eloc 565
dl 0
loc 833
rs 2
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A scan.detectWp 0 20 4
F scan.detectWpPlugin 0 49 14
F scan.detectWpTheme 0 49 14
C scan.*base.scanWp 0 27 9
C scan.detectWpCore 0 48 11
F scan.coreConvertVinfos 0 44 14
A scan.*base.lxdPs 0 7 2
A scan.*base.ip 0 11 2
A scan.*base.getErrs 0 2 1
A scan.*base.setDistro 0 10 1
B scan.*base.runningContainers 0 23 8
A scan.*base.setServerInfo 0 2 1
A scan.*base.isAwsInstanceID 0 2 1
A scan.*base.parseDockerPs 0 17 5
A scan.*base.detectPlatform 0 21 4
A scan.*base.wpConvertToModel 0 2 1
A scan.*base.setErrs 0 2 1
B scan.*base.parseLxdPs 0 19 6
A scan.*base.lxcPs 0 7 2
A scan.*base.getDistro 0 2 1
A scan.*base.dockerPs 0 7 2
A scan.*base.exec 0 2 1
A scan.*base.runningKernel 0 19 5
C scan.*base.detectRunningOnAws 0 43 9
B scan.*base.allContainers 0 23 8
A scan.*base.parseSystemctlStatus 0 9 3
B scan.*base.convertToModel 0 34 5
B scan.*base.exitedContainers 0 23 8
A scan.*base.setPlatform 0 2 1
A scan.*base.getPlatform 0 2 1
F scan.contentConvertVinfos 0 72 19
A scan.*base.detectServiceName 0 7 2
B scan.*base.parseIP 0 21 7
A scan.*base.parseLxcPs 0 13 4
A scan.*base.getServerInfo 0 2 1
C scan.*base.detectInitSystem 0 30 11
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 scan
19
20
import (
21
	"bufio"
22
	"encoding/json"
23
	"fmt"
24
	"net"
25
	"regexp"
26
	"strings"
27
	"time"
28
29
	"github.com/future-architect/vuls/config"
30
	"github.com/future-architect/vuls/models"
31
	"github.com/hashicorp/go-version"
32
	"github.com/sirupsen/logrus"
33
	"io/ioutil"
34
	"net/http"
35
)
36
37
type base struct {
38
	ServerInfo config.ServerInfo
39
	Distro     config.Distro
40
	Platform   models.Platform
41
	osPackages
42
43
	log  *logrus.Entry
44
	errs []error
45
}
46
47
//Command is for check dependence
48
type Command struct {
49
	Command string
50
	Name    string
51
}
52
53
func (l *base) scanWp() (err error) {
54
	if len(l.ServerInfo.WpPath) == 0 && len(l.ServerInfo.WpToken) == 0 {
55
		return
56
	}
57
	if len(l.ServerInfo.WpPath) == 0 {
58
		return fmt.Errorf("not found : WpPath")
59
	}
60
	if len(l.ServerInfo.WpToken) == 0 {
61
		return fmt.Errorf("not found : WpToken")
62
	}
63
64
	cmd := Command{Command: "wp cli", Name: "wp"}
65
	if r := exec(l.ServerInfo, cmd.Command, noSudo); !r.isSuccess() {
66
		return fmt.Errorf("%s command not installed", cmd.Name)
67
	}
68
69
	var vinfos []models.VulnInfo
70
	if vinfos, err = detectWp(l); err != nil {
71
		l.log.Errorf("Failed to scan wordpress: %s", err)
72
		return err
73
	}
74
	l.WpVulnInfos = map[string]models.VulnInfo{}
75
	for _, vinfo := range vinfos {
76
		l.WpVulnInfos[vinfo.CveID] = vinfo
77
	}
78
79
	return
80
}
81
82
//WpCveInfos is for wpvulndb's json
83
type WpCveInfos struct {
84
	ReleaseDate     string      `json:"release_date"`
85
	ChangelogURL    string      `json:"changelog_url"`
86
	Status          string      `json:"status"`
87
	LatestVersion   string      `json:"latest_version"`
88
	LastUpdated     string      `json:"last_updated"`
89
	Popular         bool        `json:"popular"`
90
	Vulnerabilities []WpCveInfo `json:"vulnerabilities"`
91
	Error           string      `json:"error"`
92
}
93
94
//WpCveInfo is for wpvulndb's json
95
type WpCveInfo struct {
96
	ID            int        `json:"id"`
97
	Title         string     `json:"title"`
98
	CreatedAt     string     `json:"created_at"`
99
	UpdatedAt     string     `json:"updated_at"`
100
	PublishedDate string     `json:"published_date"`
101
	VulnType      string     `json:"vuln_type"`
102
	References    References `json:"references"`
103
	FixedIn       string     `json:"fixed_in"`
104
}
105
106
//References is for wpvulndb's json
107
type References struct {
108
	URL     []string `json:"url"`
109
	Cve     []string `json:"cve"`
110
	Secunia []string `json:"secunia"`
111
}
112
113
func detectWp(c *base) (vinfos []models.VulnInfo, err error) {
114
	var coreVulns []models.VulnInfo
115
	if coreVulns, err = detectWpCore(c); err != nil {
116
		return
117
	}
118
	vinfos = append(vinfos, coreVulns...)
119
120
	var themeVulns []models.VulnInfo
121
	if themeVulns, err = detectWpTheme(c); err != nil {
122
		return
123
	}
124
	vinfos = append(vinfos, themeVulns...)
125
126
	var pluginVulns []models.VulnInfo
127
	if pluginVulns, err = detectWpPlugin(c); err != nil {
128
		return
129
	}
130
	vinfos = append(vinfos, pluginVulns...)
131
132
	return
133
}
134
135
func detectWpCore(c *base) (vinfos []models.VulnInfo, err error) {
136
	cmd := fmt.Sprintf("wp core version --path=%s", c.ServerInfo.WpPath)
137
138
	var coreVersion string
139
	var r execResult
140
	if r = exec(c.ServerInfo, cmd, noSudo); r.isSuccess() {
141
		tmp := strings.Split(r.Stdout, ".")
142
		coreVersion = strings.Join(tmp, "")
143
		coreVersion = strings.TrimRight(coreVersion, "\r\n")
144
		if len(coreVersion) == 0 {
145
			return
146
		}
147
	}
148
	if !r.isSuccess() {
149
		return vinfos, fmt.Errorf("%s", cmd)
150
	}
151
152
	url := fmt.Sprintf("https://wpvulndb.com/api/v3/wordpresses/%s", coreVersion)
153
	token := fmt.Sprintf("Token token=%s", c.ServerInfo.WpToken)
154
	var req *http.Request
155
	req, err = http.NewRequest("GET", url, nil)
156
	if err != nil {
157
		return
158
	}
159
	req.Header.Set("Authorization", token)
160
	client := new(http.Client)
161
	var resp *http.Response
162
	resp, err = client.Do(req)
163
	if err != nil {
164
		return
165
	}
166
	body, _ := ioutil.ReadAll(resp.Body)
167
	defer resp.Body.Close()
168
	if resp.StatusCode != 200 && resp.StatusCode != 404 {
169
		return vinfos, fmt.Errorf("status: %s", resp.Status)
170
	} else if resp.StatusCode == 404 {
171
		var jsonError WpCveInfos
172
		if err = json.Unmarshal([]byte(string(body)), &jsonError); err != nil {
173
			return
174
		}
175
		if jsonError.Error == "HTTP Token: Access denied.\n" {
176
			return vinfos, fmt.Errorf("wordpress: HTTP Token: Access denied.")
0 ignored issues
show
introduced by
error strings should not be capitalized or end with punctuation or a newline
Loading history...
177
		} else {
0 ignored issues
show
introduced by
if block ends with a return statement, so drop this else and outdent its block
Loading history...
178
			return vinfos, fmt.Errorf("status: %s", resp.Status)
179
		}
180
	}
181
	coreConvertVinfos(string(body))
182
	return
183
}
184
185
func coreConvertVinfos(stdout string) (vinfos []models.VulnInfo, err error) {
186
	data := map[string]WpCveInfos{}
187
	if err = json.Unmarshal([]byte(stdout), &data); err != nil {
188
		var jsonError WpCveInfos
189
		if err = json.Unmarshal([]byte(stdout), &jsonError); err != nil {
190
			return
191
		}
192
	}
193
	for _, e := range data {
194
		if len(e.Vulnerabilities) == 0 {
195
			continue
196
		}
197
		for _, vulnerability := range e.Vulnerabilities {
198
			if len(vulnerability.References.Cve) == 0 {
199
				continue
200
			}
201
			notFixedYet := false
202
			if len(vulnerability.FixedIn) == 0 {
203
				notFixedYet = true
204
			}
205
			var cveIDs []string
206
			for _, cveNumber := range vulnerability.References.Cve {
207
				cveIDs = append(cveIDs, "CVE-"+cveNumber)
208
			}
209
210
			for _, cveID := range cveIDs {
211
				vinfos = append(vinfos, models.VulnInfo{
212
					CveID: cveID,
213
					CveContents: models.NewCveContents(
214
						models.CveContent{
215
							CveID: cveID,
216
							Title: vulnerability.Title,
217
						},
218
					),
219
					AffectedPackages: models.PackageStatuses{
220
						{
221
							NotFixedYet: notFixedYet,
222
						},
223
					},
224
				})
225
			}
226
		}
227
	}
228
	return
229
}
230
231
//WpStatus is for wp command
232
type WpStatus struct {
233
	Name    string `json:"name"`
234
	Status  string `json:"status"`
235
	Update  string `json:"update"`
236
	Version string `json:"version"`
237
}
238
239
func detectWpTheme(c *base) (vinfos []models.VulnInfo, err error) {
240
	cmd := fmt.Sprintf("wp theme list --path=%s --format=json", c.ServerInfo.WpPath)
241
242
	var themes []WpStatus
243
	var r execResult
244
	if r = exec(c.ServerInfo, cmd, noSudo); r.isSuccess() {
245
		if err = json.Unmarshal([]byte(r.Stdout), &themes); err != nil {
246
			return
247
		}
248
	}
249
	if !r.isSuccess() {
250
		return vinfos, fmt.Errorf("%s", cmd)
251
	}
252
253
	for _, theme := range themes {
254
		url := fmt.Sprintf("https://wpvulndb.com/api/v3/themes/%s", theme.Name)
255
		token := fmt.Sprintf("Token token=%s", c.ServerInfo.WpToken)
256
		var req *http.Request
257
		req, err = http.NewRequest("GET", url, nil)
258
		if err != nil {
259
			return
260
		}
261
		req.Header.Set("Authorization", token)
262
		client := new(http.Client)
263
		var resp *http.Response
264
		resp, err = client.Do(req)
265
		if err != nil {
266
			return
267
		}
268
		body, _ := ioutil.ReadAll(resp.Body)
269
		defer resp.Body.Close()
270
		if resp.StatusCode != 200 && resp.StatusCode != 404 {
271
			return vinfos, fmt.Errorf("status: %s", resp.Status)
272
		} else if resp.StatusCode == 404 {
273
			var jsonError WpCveInfos
274
			if err = json.Unmarshal([]byte(string(body)), &jsonError); err != nil {
275
				return
276
			}
277
			if jsonError.Error == "HTTP Token: Access denied.\n" {
278
				return vinfos, fmt.Errorf("wordpress: HTTP Token: Access denied.")
0 ignored issues
show
introduced by
error strings should not be capitalized or end with punctuation or a newline
Loading history...
279
			} else if jsonError.Error == "Not found" {
280
				c.log.Infof("wordpress: %s not found", theme.Name)
281
			} else {
282
				return vinfos, fmt.Errorf("status: %s", resp.Status)
283
			}
284
		}
285
		contentConvertVinfos(string(body), theme)
286
	}
287
	return
288
}
289
290
func detectWpPlugin(c *base) (vinfos []models.VulnInfo, err error) {
291
	cmd := fmt.Sprintf("wp plugin list --path=%s --format=json", c.ServerInfo.WpPath)
292
293
	var plugins []WpStatus
294
	var r execResult
295
	if r := exec(c.ServerInfo, cmd, noSudo); r.isSuccess() {
296
		if err = json.Unmarshal([]byte(r.Stdout), &plugins); err != nil {
297
			return
298
		}
299
	}
300
	if !r.isSuccess() {
301
		return vinfos, fmt.Errorf("%s", cmd)
302
	}
303
304
	for _, plugin := range plugins {
305
		url := fmt.Sprintf("https://wpvulndb.com/api/v3/plugins/%s", plugin.Name)
306
		token := fmt.Sprintf("Token token=%s", c.ServerInfo.WpToken)
307
		var req *http.Request
308
		req, err = http.NewRequest("GET", url, nil)
309
		if err != nil {
310
			return
311
		}
312
		req.Header.Set("Authorization", token)
313
		client := new(http.Client)
314
		var resp *http.Response
315
		resp, err = client.Do(req)
316
		if err != nil {
317
			return
318
		}
319
		body, _ := ioutil.ReadAll(resp.Body)
320
		defer resp.Body.Close()
321
		if resp.StatusCode != 200 && resp.StatusCode != 404 {
322
			return vinfos, fmt.Errorf("status: %s", resp.Status)
323
		} else if resp.StatusCode == 404 {
324
			var jsonError WpCveInfos
325
			if err = json.Unmarshal([]byte(string(body)), &jsonError); err != nil {
326
				return
327
			}
328
			if jsonError.Error == "HTTP Token: Access denied.\n" {
329
				return vinfos, fmt.Errorf("wordpress: HTTP Token: Access denied.")
0 ignored issues
show
introduced by
error strings should not be capitalized or end with punctuation or a newline
Loading history...
330
			} else if jsonError.Error == "Not found" {
331
				c.log.Infof("wordpress: %s not found", plugin.Name)
332
			} else {
333
				return vinfos, fmt.Errorf("status: %s", resp.Status)
334
			}
335
		}
336
		contentConvertVinfos(string(body), plugin)
337
	}
338
	return
339
}
340
341
func contentConvertVinfos(stdout string, content WpStatus) (vinfos []models.VulnInfo, err error) {
342
	data := map[string]WpCveInfos{}
343
	if err = json.Unmarshal([]byte(stdout), &data); err != nil {
344
		var jsonError WpCveInfos
345
		if err = json.Unmarshal([]byte(stdout), &jsonError); err != nil {
346
			return
347
		}
348
	}
349
350
	for _, e := range data {
351
		if len(e.Vulnerabilities) == 0 {
352
			continue
353
		}
354
		for _, vulnerability := range e.Vulnerabilities {
355
			if len(vulnerability.References.Cve) == 0 {
356
				continue
357
			}
358
359
			var cveIDs []string
360
			for _, cveNumber := range vulnerability.References.Cve {
361
				cveIDs = append(cveIDs, "CVE-"+cveNumber)
362
			}
363
364
			if len(vulnerability.FixedIn) == 0 {
365
				for _, cveID := range cveIDs {
366
					vinfos = append(vinfos, models.VulnInfo{
367
						CveID: cveID,
368
						CveContents: models.NewCveContents(
369
							models.CveContent{
370
								CveID: cveID,
371
								Title: vulnerability.Title,
372
							},
373
						),
374
						AffectedPackages: models.PackageStatuses{
375
							{
376
								NotFixedYet: true,
377
							},
378
						},
379
					})
380
				}
381
			}
382
			var v1 *version.Version
383
			v1, err = version.NewVersion(content.Version)
384
			if err != nil {
385
				return
386
			}
387
			var v2 *version.Version
388
			v2, err = version.NewVersion(vulnerability.FixedIn)
389
			if err != nil {
390
				return
391
			}
392
			if v1.LessThan(v2) {
393
				for _, cveID := range cveIDs {
394
					vinfos = append(vinfos, models.VulnInfo{
395
						CveID: cveID,
396
						CveContents: models.NewCveContents(
397
							models.CveContent{
398
								CveID: cveID,
399
								Title: vulnerability.Title,
400
							},
401
						),
402
						AffectedPackages: models.PackageStatuses{
403
							{
404
								NotFixedYet: false,
405
							},
406
						},
407
					})
408
				}
409
			}
410
		}
411
	}
412
	return
413
}
414
415
func (l *base) wpConvertToModel() models.VulnInfos {
416
	return l.WpVulnInfos
417
}
418
419
func (l *base) exec(cmd string, sudo bool) execResult {
420
	return exec(l.ServerInfo, cmd, sudo, l.log)
421
}
422
423
func (l *base) setServerInfo(c config.ServerInfo) {
424
	l.ServerInfo = c
425
}
426
427
func (l *base) getServerInfo() config.ServerInfo {
428
	return l.ServerInfo
429
}
430
431
func (l *base) setDistro(fam, rel string) {
432
	d := config.Distro{
433
		Family:  fam,
434
		Release: rel,
435
	}
436
	l.Distro = d
437
438
	s := l.getServerInfo()
439
	s.Distro = d
440
	l.setServerInfo(s)
441
}
442
443
func (l *base) getDistro() config.Distro {
444
	return l.Distro
445
}
446
447
func (l *base) setPlatform(p models.Platform) {
448
	l.Platform = p
449
}
450
451
func (l *base) getPlatform() models.Platform {
452
	return l.Platform
453
}
454
455
func (l *base) runningKernel() (release, version string, err error) {
456
	r := l.exec("uname -r", noSudo)
457
	if !r.isSuccess() {
458
		return "", "", fmt.Errorf("Failed to SSH: %s", r)
459
	}
460
	release = strings.TrimSpace(r.Stdout)
461
462
	switch l.Distro.Family {
463
	case config.Debian:
464
		r := l.exec("uname -a", noSudo)
465
		if !r.isSuccess() {
466
			return "", "", fmt.Errorf("Failed to SSH: %s", r)
467
		}
468
		ss := strings.Fields(r.Stdout)
469
		if 6 < len(ss) {
470
			version = ss[6]
471
		}
472
	}
473
	return
474
}
475
476
func (l *base) allContainers() (containers []config.Container, err error) {
477
	switch l.ServerInfo.ContainerType {
478
	case "", "docker":
479
		stdout, err := l.dockerPs("-a --format '{{.ID}} {{.Names}} {{.Image}}'")
480
		if err != nil {
481
			return containers, err
482
		}
483
		return l.parseDockerPs(stdout)
484
	case "lxd":
485
		stdout, err := l.lxdPs("-c n")
486
		if err != nil {
487
			return containers, err
488
		}
489
		return l.parseLxdPs(stdout)
490
	case "lxc":
491
		stdout, err := l.lxcPs("-1")
492
		if err != nil {
493
			return containers, err
494
		}
495
		return l.parseLxcPs(stdout)
496
	default:
497
		return containers, fmt.Errorf(
498
			"Not supported yet: %s", l.ServerInfo.ContainerType)
499
	}
500
}
501
502
func (l *base) runningContainers() (containers []config.Container, err error) {
503
	switch l.ServerInfo.ContainerType {
504
	case "", "docker":
505
		stdout, err := l.dockerPs("--format '{{.ID}} {{.Names}} {{.Image}}'")
506
		if err != nil {
507
			return containers, err
508
		}
509
		return l.parseDockerPs(stdout)
510
	case "lxd":
511
		stdout, err := l.lxdPs("volatile.last_state.power=RUNNING -c n")
512
		if err != nil {
513
			return containers, err
514
		}
515
		return l.parseLxdPs(stdout)
516
	case "lxc":
517
		stdout, err := l.lxcPs("-1 --running")
518
		if err != nil {
519
			return containers, err
520
		}
521
		return l.parseLxcPs(stdout)
522
	default:
523
		return containers, fmt.Errorf(
524
			"Not supported yet: %s", l.ServerInfo.ContainerType)
525
	}
526
}
527
528
func (l *base) exitedContainers() (containers []config.Container, err error) {
529
	switch l.ServerInfo.ContainerType {
530
	case "", "docker":
531
		stdout, err := l.dockerPs("--filter 'status=exited' --format '{{.ID}} {{.Names}} {{.Image}}'")
532
		if err != nil {
533
			return containers, err
534
		}
535
		return l.parseDockerPs(stdout)
536
	case "lxd":
537
		stdout, err := l.lxdPs("volatile.last_state.power=STOPPED -c n")
538
		if err != nil {
539
			return containers, err
540
		}
541
		return l.parseLxdPs(stdout)
542
	case "lxc":
543
		stdout, err := l.lxcPs("-1 --stopped")
544
		if err != nil {
545
			return containers, err
546
		}
547
		return l.parseLxcPs(stdout)
548
	default:
549
		return containers, fmt.Errorf(
550
			"Not supported yet: %s", l.ServerInfo.ContainerType)
551
	}
552
}
553
554
func (l *base) dockerPs(option string) (string, error) {
555
	cmd := fmt.Sprintf("docker ps %s", option)
556
	r := l.exec(cmd, noSudo)
557
	if !r.isSuccess() {
558
		return "", fmt.Errorf("Failed to SSH: %s", r)
559
	}
560
	return r.Stdout, nil
561
}
562
563
func (l *base) lxdPs(option string) (string, error) {
564
	cmd := fmt.Sprintf("lxc list %s", option)
565
	r := l.exec(cmd, noSudo)
566
	if !r.isSuccess() {
567
		return "", fmt.Errorf("failed to SSH: %s", r)
568
	}
569
	return r.Stdout, nil
570
}
571
572
func (l *base) lxcPs(option string) (string, error) {
573
	cmd := fmt.Sprintf("lxc-ls %s 2>/dev/null", option)
574
	r := l.exec(cmd, sudo)
575
	if !r.isSuccess() {
576
		return "", fmt.Errorf("failed to SSH: %s", r)
577
	}
578
	return r.Stdout, nil
579
}
580
581
func (l *base) parseDockerPs(stdout string) (containers []config.Container, err error) {
582
	lines := strings.Split(stdout, "\n")
583
	for _, line := range lines {
584
		fields := strings.Fields(line)
585
		if len(fields) == 0 {
586
			break
587
		}
588
		if len(fields) != 3 {
589
			return containers, fmt.Errorf("Unknown format: %s", line)
590
		}
591
		containers = append(containers, config.Container{
592
			ContainerID: fields[0],
593
			Name:        fields[1],
594
			Image:       fields[2],
595
		})
596
	}
597
	return
598
}
599
600
func (l *base) parseLxdPs(stdout string) (containers []config.Container, err error) {
601
	lines := strings.Split(stdout, "\n")
602
	for i, line := range lines[3:] {
603
		if i%2 == 1 {
604
			continue
605
		}
606
		fields := strings.Fields(strings.Replace(line, "|", " ", -1))
607
		if len(fields) == 0 {
608
			break
609
		}
610
		if len(fields) != 1 {
611
			return containers, fmt.Errorf("Unknown format: %s", line)
612
		}
613
		containers = append(containers, config.Container{
614
			ContainerID: fields[0],
615
			Name:        fields[0],
616
		})
617
	}
618
	return
619
}
620
621
func (l *base) parseLxcPs(stdout string) (containers []config.Container, err error) {
622
	lines := strings.Split(stdout, "\n")
623
	for _, line := range lines {
624
		fields := strings.Fields(line)
625
		if len(fields) == 0 {
626
			break
627
		}
628
		containers = append(containers, config.Container{
629
			ContainerID: fields[0],
630
			Name:        fields[0],
631
		})
632
	}
633
	return
634
}
635
636
// ip executes ip command and returns IP addresses
637
func (l *base) ip() ([]string, []string, error) {
638
	// e.g.
639
	// 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000\    link/ether 52:54:00:2a:86:4c brd ff:ff:ff:ff:ff:ff
640
	// 2: eth0    inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0
641
	// 2: eth0    inet6 fe80::5054:ff:fe2a:864c/64 scope link \       valid_lft forever preferred_lft forever
642
	r := l.exec("/sbin/ip -o addr", noSudo)
643
	if !r.isSuccess() {
644
		return nil, nil, fmt.Errorf("Failed to detect IP address: %v", r)
645
	}
646
	ipv4Addrs, ipv6Addrs := l.parseIP(r.Stdout)
647
	return ipv4Addrs, ipv6Addrs, nil
648
}
649
650
// parseIP parses the results of ip command
651
func (l *base) parseIP(stdout string) (ipv4Addrs []string, ipv6Addrs []string) {
652
	lines := strings.Split(stdout, "\n")
653
	for _, line := range lines {
654
		fields := strings.Fields(line)
655
		if len(fields) < 4 {
656
			continue
657
		}
658
		ip, _, err := net.ParseCIDR(fields[3])
659
		if err != nil {
660
			continue
661
		}
662
		if !ip.IsGlobalUnicast() {
663
			continue
664
		}
665
		if ipv4 := ip.To4(); ipv4 != nil {
666
			ipv4Addrs = append(ipv4Addrs, ipv4.String())
667
		} else {
668
			ipv6Addrs = append(ipv6Addrs, ip.String())
669
		}
670
	}
671
	return
672
}
673
674
func (l *base) detectPlatform() {
675
	if l.getServerInfo().Mode.IsOffline() {
676
		l.setPlatform(models.Platform{Name: "unknown"})
677
		return
678
	}
679
	ok, instanceID, err := l.detectRunningOnAws()
680
	if err != nil {
681
		l.setPlatform(models.Platform{Name: "other"})
682
		return
683
	}
684
	if ok {
685
		l.setPlatform(models.Platform{
686
			Name:       "aws",
687
			InstanceID: instanceID,
688
		})
689
		return
690
	}
691
692
	//TODO Azure, GCP...
693
	l.setPlatform(models.Platform{Name: "other"})
694
	return
695
}
696
697
func (l *base) detectRunningOnAws() (ok bool, instanceID string, err error) {
698
	if r := l.exec("type curl", noSudo); r.isSuccess() {
699
		cmd := "curl --max-time 1 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id"
700
		r := l.exec(cmd, noSudo)
701
		if r.isSuccess() {
702
			id := strings.TrimSpace(r.Stdout)
703
			if !l.isAwsInstanceID(id) {
704
				return false, "", nil
705
			}
706
			return true, id, nil
707
		}
708
709
		switch r.ExitStatus {
710
		case 28, 7:
711
			// Not running on AWS
712
			//  7   Failed to connect to host.
713
			// 28  operation timeout.
714
			return false, "", nil
715
		}
716
	}
717
718
	if r := l.exec("type wget", noSudo); r.isSuccess() {
719
		cmd := "wget --tries=3 --timeout=1 --no-proxy -q -O - http://169.254.169.254/latest/meta-data/instance-id"
720
		r := l.exec(cmd, noSudo)
721
		if r.isSuccess() {
722
			id := strings.TrimSpace(r.Stdout)
723
			if !l.isAwsInstanceID(id) {
724
				return false, "", nil
725
			}
726
			return true, id, nil
727
		}
728
729
		switch r.ExitStatus {
730
		case 4, 8:
731
			// Not running on AWS
732
			// 4   Network failure
733
			// 8   Server issued an error response.
734
			return false, "", nil
735
		}
736
	}
737
	return false, "", fmt.Errorf(
738
		"Failed to curl or wget to AWS instance metadata on %s. container: %s",
739
		l.ServerInfo.ServerName, l.ServerInfo.Container.Name)
740
}
741
742
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html
743
var awsInstanceIDPattern = regexp.MustCompile(`^i-[0-9a-f]+$`)
744
745
func (l *base) isAwsInstanceID(str string) bool {
746
	return awsInstanceIDPattern.MatchString(str)
747
}
748
749
func (l *base) convertToModel() models.ScanResult {
750
	ctype := l.ServerInfo.ContainerType
751
	if l.ServerInfo.Container.ContainerID != "" && ctype == "" {
752
		ctype = "docker"
753
	}
754
	container := models.Container{
755
		ContainerID: l.ServerInfo.Container.ContainerID,
756
		Name:        l.ServerInfo.Container.Name,
757
		Image:       l.ServerInfo.Container.Image,
758
		Type:        ctype,
759
	}
760
761
	errs := []string{}
762
	for _, e := range l.errs {
763
		errs = append(errs, fmt.Sprintf("%s", e))
764
	}
765
766
	return models.ScanResult{
767
		JSONVersion:   models.JSONVersion,
768
		ServerName:    l.ServerInfo.ServerName,
769
		ScannedAt:     time.Now(),
770
		ScanMode:      l.ServerInfo.Mode.String(),
771
		Family:        l.Distro.Family,
772
		Release:       l.Distro.Release,
773
		Container:     container,
774
		Platform:      l.Platform,
775
		IPv4Addrs:     l.ServerInfo.IPv4Addrs,
776
		IPv6Addrs:     l.ServerInfo.IPv6Addrs,
777
		ScannedCves:   l.VulnInfos,
778
		RunningKernel: l.Kernel,
779
		Packages:      l.Packages,
780
		SrcPackages:   l.SrcPackages,
781
		Optional:      l.ServerInfo.Optional,
782
		Errors:        errs,
783
	}
784
}
785
786
func (l *base) setErrs(errs []error) {
787
	l.errs = errs
788
}
789
790
func (l *base) getErrs() []error {
791
	return l.errs
792
}
793
794
const (
795
	systemd  = "systemd"
796
	upstart  = "upstart"
797
	sysVinit = "init"
798
)
799
800
// https://unix.stackexchange.com/questions/196166/how-to-find-out-if-a-system-uses-sysv-upstart-or-systemd-initsystem
801
func (l *base) detectInitSystem() (string, error) {
802
	var f func(string) (string, error)
803
	f = func(cmd string) (string, error) {
804
		r := l.exec(cmd, sudo)
805
		if !r.isSuccess() {
806
			return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
807
		}
808
		scanner := bufio.NewScanner(strings.NewReader(r.Stdout))
809
		scanner.Scan()
810
		line := strings.TrimSpace(scanner.Text())
811
		if strings.Contains(line, "systemd") {
812
			return systemd, nil
813
		} else if strings.Contains(line, "upstart") {
814
			return upstart, nil
815
		} else if strings.Contains(line, "File: ‘/proc/1/exe’ -> ‘/sbin/init’") ||
816
			strings.Contains(line, "File: `/proc/1/exe' -> `/sbin/init'") {
817
			return f("stat /sbin/init")
818
		} else if line == "File: ‘/sbin/init’" ||
819
			line == "File: `/sbin/init'" {
820
			r := l.exec("/sbin/init --version", noSudo)
821
			if r.isSuccess() {
822
				if strings.Contains(r.Stdout, "upstart") {
823
					return upstart, nil
824
				}
825
			}
826
			return sysVinit, nil
827
		}
828
		return "", fmt.Errorf("Failed to detect a init system: %s", line)
829
	}
830
	return f("stat /proc/1/exe")
831
}
832
833
func (l *base) detectServiceName(pid string) (string, error) {
834
	cmd := fmt.Sprintf("systemctl status --quiet --no-pager %s", pid)
835
	r := l.exec(cmd, noSudo)
836
	if !r.isSuccess() {
837
		return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
838
	}
839
	return l.parseSystemctlStatus(r.Stdout), nil
840
}
841
842
func (l *base) parseSystemctlStatus(stdout string) string {
843
	scanner := bufio.NewScanner(strings.NewReader(stdout))
844
	scanner.Scan()
845
	line := scanner.Text()
846
	ss := strings.Fields(line)
847
	if len(ss) < 2 || strings.HasPrefix(line, "Failed to get unit for PID") {
848
		return ""
849
	}
850
	return ss[1]
851
}
852