Issues (121)

scan/serverapi.go (8 issues)

Severity
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
	"fmt"
22
	"net/http"
23
	"os"
24
	"path/filepath"
25
	"time"
26
27
	"github.com/future-architect/vuls/cache"
28
	"github.com/future-architect/vuls/config"
29
	"github.com/future-architect/vuls/models"
30
	"github.com/future-architect/vuls/report"
31
	"github.com/future-architect/vuls/util"
32
	"golang.org/x/xerrors"
33
)
34
35
const (
36
	scannedViaRemote = "remote"
37
	scannedViaLocal  = "local"
38
	scannedViaPseudo = "pseudo"
39
)
40
41
var (
42
	errOSFamilyHeader      = xerrors.New("X-Vuls-OS-Family header is required")
43
	errOSReleaseHeader     = xerrors.New("X-Vuls-OS-Release header is required")
44
	errKernelVersionHeader = xerrors.New("X-Vuls-Kernel-Version header is required")
45
	errServerNameHeader    = xerrors.New("X-Vuls-Server-Name header is required")
46
)
47
48
var servers, errServers []osTypeInterface
49
50
// Base Interface of redhat, debian, freebsd
51
type osTypeInterface interface {
52
	setServerInfo(config.ServerInfo)
53
	getServerInfo() config.ServerInfo
54
	setDistro(string, string)
55
	getDistro() config.Distro
56
	detectPlatform()
57
	getPlatform() models.Platform
58
59
	checkScanMode() error
60
	checkDeps() error
61
	checkIfSudoNoPasswd() error
62
63
	preCure() error
64
	postScan() error
65
	scanWordPress() error
66
	scanPackages() error
67
	convertToModel() models.ScanResult
68
69
	parseInstalledPackages(string) (models.Packages, models.SrcPackages, error)
70
71
	runningContainers() ([]config.Container, error)
72
	exitedContainers() ([]config.Container, error)
73
	allContainers() ([]config.Container, error)
74
75
	getErrs() []error
76
	setErrs([]error)
77
}
78
79
// osPackages is included by base struct
80
type osPackages struct {
81
	// installed packages
82
	Packages models.Packages
83
84
	// installed source packages (Debian based only)
85
	SrcPackages models.SrcPackages
86
87
	// unsecure packages
88
	VulnInfos models.VulnInfos
89
90
	// kernel information
91
	Kernel models.Kernel
92
}
93
94
// Retry as it may stall on the first SSH connection
95
// https://github.com/future-architect/vuls/pull/753
96
func detectDebianWithRetry(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) {
97
	type Response struct {
98
		itsMe bool
99
		deb   osTypeInterface
100
		err   error
101
	}
102
	resChan := make(chan Response, 1)
103
	go func(c config.ServerInfo) {
104
		itsMe, osType, fatalErr := detectDebian(c)
105
		resChan <- Response{itsMe, osType, fatalErr}
106
	}(c)
107
108
	timeout := time.After(time.Duration(3) * time.Second)
109
	select {
110
	case res := <-resChan:
111
		return res.itsMe, res.deb, res.err
112
	case <-timeout:
113
		time.Sleep(100 * time.Millisecond)
114
		return detectDebian(c)
115
	}
116
}
117
118
func detectOS(c config.ServerInfo) (osType osTypeInterface) {
119
	var itsMe bool
120
	var fatalErr error
121
122
	if itsMe, osType, _ = detectPseudo(c); itsMe {
123
		util.Log.Debugf("Pseudo")
124
		return
125
	}
126
127
	itsMe, osType, fatalErr = detectDebianWithRetry(c)
128
	if fatalErr != nil {
129
		osType.setErrs([]error{
130
			xerrors.Errorf("Failed to detect OS: %w", fatalErr)})
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
131
		return
132
	}
133
134
	if itsMe {
135
		util.Log.Debugf("Debian like Linux. Host: %s:%s", c.Host, c.Port)
136
		return
137
	}
138
139
	if itsMe, osType = detectRedhat(c); itsMe {
140
		util.Log.Debugf("Redhat like Linux. Host: %s:%s", c.Host, c.Port)
141
		return
142
	}
143
144
	if itsMe, osType = detectSUSE(c); itsMe {
145
		util.Log.Debugf("SUSE Linux. Host: %s:%s", c.Host, c.Port)
146
		return
147
	}
148
149
	if itsMe, osType = detectFreebsd(c); itsMe {
150
		util.Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
151
		return
152
	}
153
154
	if itsMe, osType = detectAlpine(c); itsMe {
155
		util.Log.Debugf("Alpine. Host: %s:%s", c.Host, c.Port)
156
		return
157
	}
158
159
	//TODO darwin https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/darwin.rb
160
	osType.setErrs([]error{xerrors.New("Unknown OS Type")})
161
	return
162
}
163
164
// PrintSSHableServerNames print SSH-able servernames
165
func PrintSSHableServerNames() bool {
166
	if len(servers) == 0 {
167
		util.Log.Error("No scannable servers")
168
		return false
169
	}
170
	util.Log.Info("Scannable servers are below...")
171
	for _, s := range servers {
172
		if s.getServerInfo().IsContainer() {
173
			fmt.Printf("%s@%s ",
174
				s.getServerInfo().Container.Name,
175
				s.getServerInfo().ServerName,
176
			)
177
		} else {
178
			fmt.Printf("%s ", s.getServerInfo().ServerName)
179
		}
180
	}
181
	fmt.Printf("\n")
182
	return true
183
}
184
185
// InitServers detect the kind of OS distribution of target servers
186
func InitServers(timeoutSec int) error {
187
	servers, errServers = detectServerOSes(timeoutSec)
188
	if len(servers) == 0 {
189
		return xerrors.New("No scannable servers")
190
	}
191
192
	actives, inactives := detectContainerOSes(timeoutSec)
193
	if config.Conf.ContainersOnly {
194
		servers = actives
195
		errServers = inactives
196
	} else {
197
		servers = append(servers, actives...)
198
		errServers = append(errServers, inactives...)
199
	}
200
	return nil
201
}
202
203
func detectServerOSes(timeoutSec int) (servers, errServers []osTypeInterface) {
204
	util.Log.Info("Detecting OS of servers... ")
205
	osTypeChan := make(chan osTypeInterface, len(config.Conf.Servers))
206
	defer close(osTypeChan)
207
	for _, s := range config.Conf.Servers {
208
		go func(s config.ServerInfo) {
209
			defer func() {
210
				if p := recover(); p != nil {
211
					util.Log.Debugf("Panic: %s on %s", p, s.ServerName)
212
				}
213
			}()
214
			osTypeChan <- detectOS(s)
215
		}(s)
216
	}
217
218
	timeout := time.After(time.Duration(timeoutSec) * time.Second)
219
	for i := 0; i < len(config.Conf.Servers); i++ {
220
		select {
221
		case res := <-osTypeChan:
222
			if 0 < len(res.getErrs()) {
223
				errServers = append(errServers, res)
224
				util.Log.Errorf("(%d/%d) Failed: %s, err: %+v",
225
					i+1, len(config.Conf.Servers),
226
					res.getServerInfo().ServerName,
227
					res.getErrs())
228
			} else {
229
				servers = append(servers, res)
230
				util.Log.Infof("(%d/%d) Detected: %s: %s",
231
					i+1, len(config.Conf.Servers),
232
					res.getServerInfo().ServerName,
233
					res.getDistro())
234
			}
235
		case <-timeout:
236
			msg := "Timed out while detecting servers"
237
			util.Log.Error(msg)
238
			for servername, sInfo := range config.Conf.Servers {
239
				found := false
240
				for _, o := range append(servers, errServers...) {
241
					if servername == o.getServerInfo().ServerName {
242
						found = true
243
						break
244
					}
245
				}
246
				if !found {
247
					u := &unknown{}
248
					u.setServerInfo(sInfo)
249
					u.setErrs([]error{
250
						xerrors.New("Timed out"),
251
					})
252
					errServers = append(errServers, u)
253
					util.Log.Errorf("(%d/%d) Timed out: %s",
254
						i+1, len(config.Conf.Servers),
255
						servername)
256
					i++
257
				}
258
			}
259
		}
260
	}
261
	return
262
}
263
264
func detectContainerOSes(timeoutSec int) (actives, inactives []osTypeInterface) {
265
	util.Log.Info("Detecting OS of containers... ")
266
	osTypesChan := make(chan []osTypeInterface, len(servers))
267
	defer close(osTypesChan)
268
	for _, s := range servers {
269
		go func(s osTypeInterface) {
270
			defer func() {
271
				if p := recover(); p != nil {
272
					util.Log.Debugf("Panic: %s on %s",
273
						p, s.getServerInfo().GetServerName())
274
				}
275
			}()
276
			osTypesChan <- detectContainerOSesOnServer(s)
277
		}(s)
278
	}
279
280
	timeout := time.After(time.Duration(timeoutSec) * time.Second)
281
	for i := 0; i < len(servers); i++ {
282
		select {
283
		case res := <-osTypesChan:
284
			for _, osi := range res {
285
				sinfo := osi.getServerInfo()
286
				if 0 < len(osi.getErrs()) {
287
					inactives = append(inactives, osi)
288
					util.Log.Errorf("Failed: %s err: %+v", sinfo.ServerName, osi.getErrs())
289
					continue
290
				}
291
				actives = append(actives, osi)
292
				util.Log.Infof("Detected: %s@%s: %s",
293
					sinfo.Container.Name, sinfo.ServerName, osi.getDistro())
294
			}
295
		case <-timeout:
296
			msg := "Timed out while detecting containers"
297
			util.Log.Error(msg)
298
			for servername, sInfo := range config.Conf.Servers {
299
				found := false
300
				for _, o := range append(actives, inactives...) {
301
					if servername == o.getServerInfo().ServerName {
302
						found = true
303
						break
304
					}
305
				}
306
				if !found {
307
					u := &unknown{}
308
					u.setServerInfo(sInfo)
309
					u.setErrs([]error{
310
						xerrors.New("Timed out"),
311
					})
312
					inactives = append(inactives)
313
					util.Log.Errorf("Timed out: %s", servername)
314
				}
315
			}
316
		}
317
	}
318
	return
319
}
320
321
func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeInterface) {
322
	containerHostInfo := containerHost.getServerInfo()
323
	if len(containerHostInfo.ContainersIncluded) == 0 {
324
		return
325
	}
326
327
	running, err := containerHost.runningContainers()
328
	if err != nil {
329
		containerHost.setErrs([]error{xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
330
			"Failed to get running containers on %s. err: %w",
331
			containerHost.getServerInfo().ServerName, err)})
332
		return append(oses, containerHost)
333
	}
334
335
	if containerHostInfo.ContainersIncluded[0] == "${running}" {
336
		for _, containerInfo := range running {
337
			found := false
338
			for _, ex := range containerHost.getServerInfo().ContainersExcluded {
339
				if containerInfo.Name == ex || containerInfo.ContainerID == ex {
340
					found = true
341
				}
342
			}
343
			if found {
344
				continue
345
			}
346
347
			copied := containerHostInfo
348
			copied.SetContainer(config.Container{
349
				ContainerID: containerInfo.ContainerID,
350
				Name:        containerInfo.Name,
351
				Image:       containerInfo.Image,
352
			})
353
			os := detectOS(copied)
354
			oses = append(oses, os)
355
		}
356
		return oses
357
	}
358
359
	exitedContainers, err := containerHost.exitedContainers()
360
	if err != nil {
361
		containerHost.setErrs([]error{xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
362
			"Failed to get exited containers on %s. err: %w",
363
			containerHost.getServerInfo().ServerName, err)})
364
		return append(oses, containerHost)
365
	}
366
367
	var exited, unknown []string
368
	for _, container := range containerHostInfo.ContainersIncluded {
369
		found := false
370
		for _, c := range running {
371
			if c.ContainerID == container || c.Name == container {
372
				copied := containerHostInfo
373
				copied.SetContainer(c)
374
				os := detectOS(copied)
375
				oses = append(oses, os)
376
				found = true
377
				break
378
			}
379
		}
380
381
		if !found {
382
			foundInExitedContainers := false
383
			for _, c := range exitedContainers {
384
				if c.ContainerID == container || c.Name == container {
385
					exited = append(exited, container)
386
					foundInExitedContainers = true
387
					break
388
				}
389
			}
390
			if !foundInExitedContainers {
391
				unknown = append(unknown, container)
392
			}
393
		}
394
	}
395
	if 0 < len(exited) || 0 < len(unknown) {
396
		containerHost.setErrs([]error{xerrors.Errorf(
397
			"Some containers on %s are exited or unknown. exited: %s, unknown: %s",
398
			containerHost.getServerInfo().ServerName, exited, unknown)})
399
		return append(oses, containerHost)
400
	}
401
	return oses
402
}
403
404
// CheckScanModes checks scan mode
405
func CheckScanModes() error {
406
	for _, s := range servers {
407
		if err := s.checkScanMode(); err != nil {
408
			return xerrors.Errorf("servers.%s.scanMode err: %w",
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
409
				s.getServerInfo().GetServerName(), err)
410
		}
411
	}
412
	return nil
413
}
414
415
// CheckDependencies checks dependencies are installed on target servers.
416
func CheckDependencies(timeoutSec int) {
417
	parallelExec(func(o osTypeInterface) error {
418
		return o.checkDeps()
419
	}, timeoutSec)
420
	return
421
}
422
423
// CheckIfSudoNoPasswd checks whether vuls can sudo with nopassword via SSH
424
func CheckIfSudoNoPasswd(timeoutSec int) {
425
	parallelExec(func(o osTypeInterface) error {
426
		return o.checkIfSudoNoPasswd()
427
	}, timeoutSec)
428
	return
429
}
430
431
// DetectPlatforms detects the platform of each servers.
432
func DetectPlatforms(timeoutSec int) {
433
	detectPlatforms(timeoutSec)
434
	for i, s := range servers {
435
		if s.getServerInfo().IsContainer() {
436
			util.Log.Infof("(%d/%d) %s on %s is running on %s",
437
				i+1, len(servers),
438
				s.getServerInfo().Container.Name,
439
				s.getServerInfo().ServerName,
440
				s.getPlatform().Name,
441
			)
442
443
		} else {
444
			util.Log.Infof("(%d/%d) %s is running on %s",
445
				i+1, len(servers),
446
				s.getServerInfo().ServerName,
447
				s.getPlatform().Name,
448
			)
449
		}
450
	}
451
	return
452
}
453
454
func detectPlatforms(timeoutSec int) {
455
	parallelExec(func(o osTypeInterface) error {
456
		o.detectPlatform()
457
		// Logging only if platform can not be specified
458
		return nil
459
	}, timeoutSec)
460
	return
461
}
462
463
// Scan scan
464
func Scan(timeoutSec int) error {
465
	if len(servers) == 0 {
466
		return xerrors.New("No server defined. Check the configuration")
467
	}
468
469
	if err := setupChangelogCache(); err != nil {
470
		return err
471
	}
472
	defer func() {
473
		if cache.DB != nil {
474
			cache.DB.Close()
475
		}
476
	}()
477
478
	util.Log.Info("Scanning vulnerable OS packages...")
479
	scannedAt := time.Now()
480
	dir, err := EnsureResultDir(scannedAt)
481
	if err != nil {
482
		return err
483
	}
484
	return scanVulns(dir, scannedAt, timeoutSec)
485
}
486
487
// ViaHTTP scans servers by HTTP header and body
488
func ViaHTTP(header http.Header, body string) (models.ScanResult, error) {
489
	family := header.Get("X-Vuls-OS-Family")
490
	if family == "" {
491
		return models.ScanResult{}, errOSFamilyHeader
492
	}
493
494
	release := header.Get("X-Vuls-OS-Release")
495
	if release == "" {
496
		return models.ScanResult{}, errOSReleaseHeader
497
	}
498
499
	kernelRelease := header.Get("X-Vuls-Kernel-Release")
500
	if kernelRelease == "" {
501
		util.Log.Warn("If X-Vuls-Kernel-Release is not specified, there is a possibility of false detection")
502
	}
503
504
	kernelVersion := header.Get("X-Vuls-Kernel-Version")
505
	if family == config.Debian && kernelVersion == "" {
506
		return models.ScanResult{}, errKernelVersionHeader
507
	}
508
509
	serverName := header.Get("X-Vuls-Server-Name")
510
	if config.Conf.ToLocalFile && serverName == "" {
511
		return models.ScanResult{}, errServerNameHeader
512
	}
513
514
	distro := config.Distro{
515
		Family:  family,
516
		Release: release,
517
	}
518
519
	kernel := models.Kernel{
520
		Release: kernelRelease,
521
		Version: kernelVersion,
522
	}
523
	base := base{
524
		Distro: distro,
525
		osPackages: osPackages{
526
			Kernel: kernel,
527
		},
528
		log: util.Log,
529
	}
530
531
	var osType osTypeInterface
532
	switch family {
533
	case config.Debian, config.Ubuntu:
534
		osType = &debian{base: base}
535
	case config.RedHat:
536
		osType = &rhel{
537
			redhatBase: redhatBase{base: base},
538
		}
539
	case config.CentOS:
540
		osType = &centos{
541
			redhatBase: redhatBase{base: base},
542
		}
543
	default:
544
		return models.ScanResult{}, xerrors.Errorf("Server mode for %s is not implemented yet", family)
545
	}
546
547
	installedPackages, srcPackages, err := osType.parseInstalledPackages(body)
548
	if err != nil {
549
		return models.ScanResult{}, err
550
	}
551
552
	result := models.ScanResult{
553
		ServerName: serverName,
554
		Family:     family,
555
		Release:    release,
556
		RunningKernel: models.Kernel{
557
			Release: kernelRelease,
558
			Version: kernelVersion,
559
		},
560
		Packages:    installedPackages,
561
		SrcPackages: srcPackages,
562
		ScannedCves: models.VulnInfos{},
563
	}
564
565
	return result, nil
566
}
567
568
func setupChangelogCache() error {
569
	needToSetupCache := false
570
	for _, s := range servers {
571
		switch s.getDistro().Family {
572
		case config.Raspbian:
573
			needToSetupCache = true
574
			break
575
		case config.Ubuntu, config.Debian:
576
			//TODO changelopg cache for RedHat, Oracle, Amazon, CentOS is not implemented yet.
577
			if s.getServerInfo().Mode.IsDeep() {
578
				needToSetupCache = true
579
			}
580
			break
581
		}
582
	}
583
	if needToSetupCache {
584
		if err := cache.SetupBolt(config.Conf.CacheDBPath, util.Log); err != nil {
585
			return err
586
		}
587
	}
588
	return nil
589
}
590
591
func scanVulns(jsonDir string, scannedAt time.Time, timeoutSec int) error {
592
	var results models.ScanResults
593
	parallelExec(func(o osTypeInterface) (err error) {
594
		if err = o.preCure(); err != nil {
595
			return err
596
		}
597
		if err = o.scanPackages(); err != nil {
598
			return err
599
		}
600
		if err = o.scanWordPress(); err != nil {
601
			return xerrors.Errorf("Failed to scan WordPress: %w", err)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
602
		}
603
		return o.postScan()
604
	}, timeoutSec)
605
606
	hostname, _ := os.Hostname()
607
	ipv4s, ipv6s, err := util.IP()
608
	if err != nil {
609
		util.Log.Errorf("Failed to fetch scannedIPs. err: %+v", err)
610
	}
611
612
	for _, s := range append(servers, errServers...) {
613
		r := s.convertToModel()
614
		r.ScannedAt = scannedAt
615
		r.ScannedVersion = config.Version
616
		r.ScannedRevision = config.Revision
617
		r.ScannedBy = hostname
618
		r.ScannedIPv4Addrs = ipv4s
619
		r.ScannedIPv6Addrs = ipv6s
620
		r.Config.Scan = config.Conf
621
		results = append(results, r)
622
	}
623
624
	config.Conf.FormatJSON = true
625
	ws := []report.ResultWriter{
626
		report.LocalFileWriter{CurrentDir: jsonDir},
627
	}
628
	for _, w := range ws {
629
		if err := w.Write(results...); err != nil {
630
			return xerrors.Errorf("Failed to write summary report: %s", err)
631
		}
632
	}
633
634
	report.StdoutWriter{}.WriteScanSummary(results...)
635
	return nil
636
}
637
638
// EnsureResultDir ensures the directory for scan results
639
func EnsureResultDir(scannedAt time.Time) (currentDir string, err error) {
640
	jsonDirName := scannedAt.Format(time.RFC3339)
641
642
	resultsDir := config.Conf.ResultsDir
643
	if len(resultsDir) == 0 {
644
		wd, _ := os.Getwd()
645
		resultsDir = filepath.Join(wd, "results")
646
	}
647
	jsonDir := filepath.Join(resultsDir, jsonDirName)
648
	if err := os.MkdirAll(jsonDir, 0700); err != nil {
649
		return "", xerrors.Errorf("Failed to create dir: %w", err)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
650
	}
651
652
	symlinkPath := filepath.Join(resultsDir, "current")
653
	if _, err := os.Lstat(symlinkPath); err == nil {
654
		if err := os.Remove(symlinkPath); err != nil {
655
			return "", xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
656
				"Failed to remove symlink. path: %s, err: %w", symlinkPath, err)
657
		}
658
	}
659
660
	if err := os.Symlink(jsonDir, symlinkPath); err != nil {
661
		return "", xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
662
			"Failed to create symlink: path: %s, err: %w", symlinkPath, err)
663
	}
664
	return jsonDir, nil
665
}
666