Issues (121)

scan/debian.go (3 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
	"bufio"
22
	"fmt"
23
	"regexp"
24
	"strconv"
25
	"strings"
26
	"time"
27
28
	"github.com/future-architect/vuls/cache"
29
	"github.com/future-architect/vuls/config"
30
	"github.com/future-architect/vuls/models"
31
	"github.com/future-architect/vuls/util"
32
	version "github.com/knqyf263/go-deb-version"
33
	"golang.org/x/xerrors"
34
)
35
36
// inherit OsTypeInterface
37
type debian struct {
38
	base
39
}
40
41
// NewDebian is constructor
42
func newDebian(c config.ServerInfo) *debian {
43
	d := &debian{
44
		base: base{
45
			osPackages: osPackages{
46
				Packages:  models.Packages{},
47
				VulnInfos: models.VulnInfos{},
48
			},
49
		},
50
	}
51
	d.log = util.NewCustomLogger(c)
52
	d.setServerInfo(c)
53
	return d
54
}
55
56
// Ubuntu, Debian, Raspbian
57
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb
58
func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) {
59
	deb = newDebian(c)
60
61
	if r := exec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() {
62
		if r.Error != nil {
63
			return false, deb, nil
64
		}
65
		if r.ExitStatus == 255 {
66
			return false, deb, xerrors.Errorf("Unable to connect via SSH. Scan with -vvv option to print SSH debugging messages and check SSH settings. If you have never SSH to the host to be scanned, SSH to the host before scanning in order to add the HostKey. %s@%s port: %s\n%s", c.User, c.Host, c.Port, r)
67
		}
68
		util.Log.Debugf("Not Debian like Linux. %s", r)
69
		return false, deb, nil
70
	}
71
72
	// Raspbian
73
	// lsb_release in Raspbian Jessie returns 'Distributor ID: Raspbian'.
74
	// However, lsb_release in Raspbian Wheezy returns 'Distributor ID: Debian'.
75
	if r := exec(c, "cat /etc/issue", noSudo); r.isSuccess() {
76
		//  e.g.
77
		//  Raspbian GNU/Linux 7 \n \l
78
		result := strings.Fields(r.Stdout)
79
		if len(result) > 2 && result[0] == config.Raspbian {
80
			distro := strings.ToLower(trim(result[0]))
81
			deb.setDistro(distro, trim(result[2]))
82
			return true, deb, nil
83
		}
84
	}
85
86
	if r := exec(c, "lsb_release -ir", noSudo); r.isSuccess() {
87
		//  e.g.
88
		//  root@fa3ec524be43:/# lsb_release -ir
89
		//  Distributor ID:	Ubuntu
90
		//  Release:	14.04
91
		re := regexp.MustCompile(`(?s)^Distributor ID:\s*(.+?)\n*Release:\s*(.+?)$`)
92
		result := re.FindStringSubmatch(trim(r.Stdout))
93
94
		if len(result) == 0 {
95
			deb.setDistro("debian/ubuntu", "unknown")
96
			util.Log.Warnf(
97
				"Unknown Debian/Ubuntu version. lsb_release -ir: %s", r)
98
		} else {
99
			distro := strings.ToLower(trim(result[1]))
100
			deb.setDistro(distro, trim(result[2]))
101
		}
102
		return true, deb, nil
103
	}
104
105
	if r := exec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() {
106
		//  e.g.
107
		//  DISTRIB_ID=Ubuntu
108
		//  DISTRIB_RELEASE=14.04
109
		//  DISTRIB_CODENAME=trusty
110
		//  DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS"
111
		re := regexp.MustCompile(`(?s)^DISTRIB_ID=(.+?)\n*DISTRIB_RELEASE=(.+?)\n.*$`)
112
		result := re.FindStringSubmatch(trim(r.Stdout))
113
		if len(result) == 0 {
114
			util.Log.Warnf(
115
				"Unknown Debian/Ubuntu. cat /etc/lsb-release: %s", r)
116
			deb.setDistro("debian/ubuntu", "unknown")
117
		} else {
118
			distro := strings.ToLower(trim(result[1]))
119
			deb.setDistro(distro, trim(result[2]))
120
		}
121
		return true, deb, nil
122
	}
123
124
	// Debian
125
	cmd := "cat /etc/debian_version"
126
	if r := exec(c, cmd, noSudo); r.isSuccess() {
127
		deb.setDistro(config.Debian, trim(r.Stdout))
128
		return true, deb, nil
129
	}
130
131
	util.Log.Debugf("Not Debian like Linux: %s", c.ServerName)
132
	return false, deb, nil
133
}
134
135
func trim(str string) string {
136
	return strings.TrimSpace(str)
137
}
138
139
func (o *debian) checkScanMode() error {
140
	return nil
141
}
142
143
func (o *debian) checkIfSudoNoPasswd() error {
144
	if o.getServerInfo().Mode.IsFast() {
145
		o.log.Infof("sudo ... No need")
146
		return nil
147
	}
148
149
	cmds := []string{
150
		"checkrestart",
151
		"stat /proc/1/exe",
152
	}
153
154
	if !o.getServerInfo().Mode.IsOffline() {
155
		cmds = append(cmds, "apt-get update")
156
	}
157
158
	for _, cmd := range cmds {
159
		cmd = util.PrependProxyEnv(cmd)
160
		o.log.Infof("Checking... sudo %s", cmd)
161
		r := o.exec(cmd, sudo)
162
		if !r.isSuccess() {
163
			o.log.Errorf("sudo error on %s", r)
164
			return xerrors.Errorf("Failed to sudo: %s", r)
165
		}
166
	}
167
168
	initName, err := o.detectInitSystem()
169
	if initName == upstart && err == nil {
170
		cmd := util.PrependProxyEnv("initctl status --help")
171
		o.log.Infof("Checking... sudo %s", cmd)
172
		r := o.exec(cmd, sudo)
173
		if !r.isSuccess() {
174
			o.log.Errorf("sudo error on %s", r)
175
			return xerrors.Errorf("Failed to sudo: %s", r)
176
		}
177
	}
178
179
	o.log.Infof("Sudo... Pass")
180
	return nil
181
}
182
183
type dep struct {
184
	packName      string
185
	required      bool
186
	logFunc       func(string, ...interface{})
187
	additionalMsg string
188
}
189
190
func (o *debian) checkDeps() error {
191
	deps := []dep{}
192
	if o.getServerInfo().Mode.IsDeep() || o.getServerInfo().Mode.IsFastRoot() {
193
		// checkrestart
194
		deps = append(deps, dep{
195
			packName: "debian-goodies",
196
			required: true,
197
			logFunc:  o.log.Errorf,
198
		})
199
	}
200
201
	if o.Distro.Family == config.Debian {
202
		// https://askubuntu.com/a/742844
203
		if !o.ServerInfo.IsContainer() {
204
			deps = append(deps, dep{
205
				packName:      "reboot-notifier",
206
				required:      false,
207
				logFunc:       o.log.Warnf,
208
				additionalMsg: ". Install it if you want to detect whether not rebooted after kernel update. To install `reboot-notifier` on Debian, see https://feeding.cloud.geek.nz/posts/introducing-reboot-notifier/",
209
			})
210
		}
211
212
		// Changelogs will be fetched only in deep scan mode
213
		if o.getServerInfo().Mode.IsDeep() {
214
			// Debian needs aptitude to get changelogs.
215
			// Because unable to get changelogs via `apt-get changelog` on Debian.
216
			deps = append(deps, dep{
217
				packName: "aptitude",
218
				required: true,
219
				logFunc:  o.log.Errorf,
220
			})
221
		}
222
	}
223
224
	for _, dep := range deps {
225
		cmd := fmt.Sprintf("%s %s", dpkgQuery, dep.packName)
226
		msg := fmt.Sprintf("%s is not installed", dep.packName)
227
		r := o.exec(cmd, noSudo)
228
		if !r.isSuccess() {
229
			if dep.additionalMsg != "" {
230
				msg += dep.additionalMsg
231
			}
232
			dep.logFunc(msg)
233
			if dep.required {
234
				return xerrors.New(msg)
235
			}
236
			continue
237
		}
238
239
		_, status, _, _, _, _ := o.parseScannedPackagesLine(r.Stdout)
240
		if status != "ii" {
241
			if dep.additionalMsg != "" {
242
				msg += dep.additionalMsg
243
			}
244
			dep.logFunc(msg)
245
			if dep.required {
246
				return xerrors.New(msg)
247
			}
248
		}
249
250
	}
251
	o.log.Infof("Dependencies... Pass")
252
	return nil
253
}
254
255
func (o *debian) preCure() error {
256
	o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
257
	if err := o.detectIPAddr(); err != nil {
258
		o.log.Debugf("Failed to detect IP addresses: %s", err)
259
	}
260
	// Ignore this error as it just failed to detect the IP addresses
261
	return nil
262
}
263
264
func (o *debian) postScan() error {
265
	if o.getServerInfo().Mode.IsDeep() || o.getServerInfo().Mode.IsFastRoot() {
266
		return o.checkrestart()
267
	}
268
	return nil
269
}
270
271
func (o *debian) detectIPAddr() (err error) {
272
	o.ServerInfo.IPv4Addrs, o.ServerInfo.IPv6Addrs, err = o.ip()
273
	return err
274
}
275
276
func (o *debian) scanPackages() error {
277
	// collect the running kernel information
278
	release, version, err := o.runningKernel()
279
	if err != nil {
280
		o.log.Errorf("Failed to scan the running kernel version: %s", err)
281
		return err
282
	}
283
	rebootRequired, err := o.rebootRequired()
284
	if err != nil {
285
		o.log.Errorf("Failed to detect the kernel reboot required: %s", err)
286
		return err
287
	}
288
	o.Kernel = models.Kernel{
289
		Version:        version,
290
		Release:        release,
291
		RebootRequired: rebootRequired,
292
	}
293
294
	installed, updatable, srcPacks, err := o.scanInstalledPackages()
295
	if err != nil {
296
		o.log.Errorf("Failed to scan installed packages: %s", err)
297
		return err
298
	}
299
	o.Packages = installed
300
	o.SrcPackages = srcPacks
301
302
	if o.getServerInfo().Mode.IsOffline() {
303
		return nil
304
	}
305
306
	if o.getServerInfo().Mode.IsDeep() || o.Distro.Family == config.Raspbian {
307
		unsecures, err := o.scanUnsecurePackages(updatable)
308
		if err != nil {
309
			o.log.Errorf("Failed to scan vulnerable packages: %s", err)
310
			return err
311
		}
312
		o.VulnInfos = unsecures
313
		return nil
314
	}
315
	return nil
316
}
317
318
// https://askubuntu.com/a/742844
319
func (o *debian) rebootRequired() (bool, error) {
320
	r := o.exec("test -f /var/run/reboot-required", noSudo)
321
	switch r.ExitStatus {
322
	case 0:
323
		return true, nil
324
	case 1:
325
		return false, nil
326
	default:
327
		return false, xerrors.Errorf("Failed to check reboot reauired: %s", r)
328
	}
329
}
330
331
const dpkgQuery = `dpkg-query -W -f="\${binary:Package},\${db:Status-Abbrev},\${Version},\${Source},\${source:Version}\n"`
332
333
func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, models.SrcPackages, error) {
334
	updatable := models.Packages{}
335
	r := o.exec(dpkgQuery, noSudo)
336
	if !r.isSuccess() {
337
		return nil, nil, nil, xerrors.Errorf("Failed to SSH: %s", r)
338
	}
339
340
	installed, srcPacks, err := o.parseInstalledPackages(r.Stdout)
341
	if err != nil {
342
		return nil, nil, nil, err
343
	}
344
345
	if o.getServerInfo().Mode.IsOffline() || o.getServerInfo().Mode.IsFast() {
346
		return installed, updatable, srcPacks, nil
347
	}
348
349
	if err := o.aptGetUpdate(); err != nil {
350
		return nil, nil, nil, err
351
	}
352
	updatableNames, err := o.getUpdatablePackNames()
353
	if err != nil {
354
		return nil, nil, nil, err
355
	}
356
	for _, name := range updatableNames {
357
		for _, pack := range installed {
358
			if pack.Name == name {
359
				updatable[name] = pack
360
				break
361
			}
362
		}
363
	}
364
365
	// Fill the candidate versions of upgradable packages
366
	err = o.fillCandidateVersion(updatable)
367
	if err != nil {
368
		return nil, nil, nil, xerrors.Errorf("Failed to fill candidate versions. err: %s", err)
369
	}
370
	installed.MergeNewVersion(updatable)
371
372
	return installed, updatable, srcPacks, nil
373
}
374
375
func (o *debian) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
376
	installed, srcPacks := models.Packages{}, models.SrcPackages{}
377
378
	// e.g.
379
	// curl,ii ,7.38.0-4+deb8u2,,7.38.0-4+deb8u2
380
	// openssh-server,ii ,1:6.7p1-5+deb8u3,openssh,1:6.7p1-5+deb8u3
381
	// tar,ii ,1.27.1-2+b1,tar (1.27.1-2),1.27.1-2
382
	lines := strings.Split(stdout, "\n")
383
	for _, line := range lines {
384
		if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
385
			name, status, version, srcName, srcVersion, err := o.parseScannedPackagesLine(trimmed)
386
			if err != nil || len(status) < 2 {
387
				return nil, nil, xerrors.Errorf(
388
					"Debian: Failed to parse package line: %s", line)
389
			}
390
391
			packageStatus := status[1]
392
			// Package status:
393
			//     n = Not-installed
394
			//     c = Config-files
395
			//     H = Half-installed
396
			//     U = Unpacked
397
			//     F = Half-configured
398
			//     W = Triggers-awaiting
399
			//     t = Triggers-pending
400
			//     i = Installed
401
			if packageStatus != 'i' {
402
				o.log.Debugf("%s package status is '%c', ignoring", name, packageStatus)
403
				continue
404
			}
405
			installed[name] = models.Package{
406
				Name:    name,
407
				Version: version,
408
			}
409
410
			if srcName != "" && srcName != name {
411
				if pack, ok := srcPacks[srcName]; ok {
412
					pack.AddBinaryName(name)
413
					srcPacks[srcName] = pack
414
				} else {
415
					srcPacks[srcName] = models.SrcPackage{
416
						Name:        srcName,
417
						Version:     srcVersion,
418
						BinaryNames: []string{name},
419
					}
420
				}
421
			}
422
		}
423
	}
424
425
	// Remove "linux"
426
	// kernel-related packages are showed "linux" as source package name
427
	// If "linux" is left, oval detection will cause trouble, so delete.
428
	delete(srcPacks, "linux")
429
	// Remove duplicate
430
	for name := range installed {
431
		delete(srcPacks, name)
432
	}
433
	return installed, srcPacks, nil
434
}
435
436
func (o *debian) parseScannedPackagesLine(line string) (name, status, version, srcName, srcVersion string, err error) {
437
	ss := strings.Split(line, ",")
438
	if len(ss) == 5 {
439
		// remove :amd64, i386...
440
		name = ss[0]
441
		if i := strings.IndexRune(name, ':'); i >= 0 {
442
			name = name[:i]
443
		}
444
		status = strings.TrimSpace(ss[1])
445
		version = ss[2]
446
		// remove version. ex: tar (1.27.1-2)
447
		srcName = strings.Split(ss[3], " ")[0]
448
		srcVersion = ss[4]
449
		return
450
	}
451
452
	return "", "", "", "", "", xerrors.Errorf("Unknown format: %s", line)
453
}
454
455
func (o *debian) aptGetUpdate() error {
456
	o.log.Infof("apt-get update...")
457
	cmd := util.PrependProxyEnv("apt-get update")
458
	if r := o.exec(cmd, sudo); !r.isSuccess() {
459
		return xerrors.Errorf("Failed to apt-get update: %s", r)
460
	}
461
	return nil
462
}
463
464
func (o *debian) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) {
465
	// Setup changelog cache
466
	current := cache.Meta{
467
		Name:   o.getServerInfo().GetServerName(),
468
		Distro: o.getServerInfo().Distro,
469
		Packs:  updatable,
470
	}
471
472
	o.log.Debugf("Ensure changelog cache: %s", current.Name)
473
	meta, err := o.ensureChangelogCache(current)
474
	if err != nil {
475
		return nil, err
476
	}
477
478
	// Collect CVE information of upgradable packages
479
	vulnInfos, err := o.scanChangelogs(updatable, meta)
480
	if err != nil {
481
		return nil, xerrors.Errorf("Failed to scan unsecure packages. err: %s", err)
482
	}
483
484
	return vulnInfos, nil
485
}
486
487
func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) {
488
	// Search from cache
489
	cached, found, err := cache.DB.GetMeta(current.Name)
490
	if err != nil {
491
		return nil, xerrors.Errorf(
492
			"Failed to get meta. Please remove cache.db and then try again. err: %s", err)
493
	}
494
495
	if !found {
496
		o.log.Debugf("Not found in meta: %s", current.Name)
497
		err = cache.DB.EnsureBuckets(current)
498
		if err != nil {
499
			return nil, xerrors.Errorf("Failed to ensure buckets. err: %s", err)
500
		}
501
		return &current, nil
502
	}
503
504
	if current.Distro.Family != cached.Distro.Family ||
505
		current.Distro.Release != cached.Distro.Release {
506
		o.log.Debugf("Need to refesh meta: %s", current.Name)
507
		err = cache.DB.EnsureBuckets(current)
508
		if err != nil {
509
			return nil, xerrors.Errorf("Failed to ensure buckets. err: %s", err)
510
		}
511
		return &current, nil
512
513
	}
514
515
	o.log.Debugf("Reuse meta: %s", current.Name)
516
	if config.Conf.Debug {
517
		cache.DB.PrettyPrint(current)
518
	}
519
	return &cached, nil
520
}
521
522
func (o *debian) fillCandidateVersion(updatables models.Packages) (err error) {
523
	names := []string{}
524
	for name := range updatables {
525
		names = append(names, name)
526
	}
527
	cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " "))
528
	r := o.exec(cmd, noSudo)
529
	if !r.isSuccess() {
530
		return xerrors.Errorf("Failed to SSH: %s", r)
531
	}
532
	packAptPolicy := o.splitAptCachePolicy(r.Stdout)
533
	for k, v := range packAptPolicy {
534
		ver, err := o.parseAptCachePolicy(v, k)
535
		if err != nil {
536
			return xerrors.Errorf("Failed to parse %w", err)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
537
		}
538
		pack, ok := updatables[k]
539
		if !ok {
540
			return xerrors.Errorf("Not found: %s", k)
541
		}
542
		pack.NewVersion = ver.Candidate
543
		pack.Repository = ver.Repo
544
		updatables[k] = pack
545
	}
546
	return
547
}
548
549
func (o *debian) getUpdatablePackNames() (packNames []string, err error) {
550
	cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get dist-upgrade --dry-run")
551
	r := o.exec(cmd, noSudo)
552
	if r.isSuccess(0, 1) {
553
		return o.parseAptGetUpgrade(r.Stdout)
554
	}
555
	return packNames, xerrors.Errorf(
556
		"Failed to %s. status: %d, stdout: %s, stderr: %s",
557
		cmd, r.ExitStatus, r.Stdout, r.Stderr)
558
}
559
560
func (o *debian) parseAptGetUpgrade(stdout string) (updatableNames []string, err error) {
561
	startRe := regexp.MustCompile(`The following packages will be upgraded:`)
562
	stopRe := regexp.MustCompile(`^(\d+) upgraded.*`)
563
	startLineFound, stopLineFound := false, false
564
565
	lines := strings.Split(stdout, "\n")
566
	for _, line := range lines {
567
		if !startLineFound {
568
			if matche := startRe.MatchString(line); matche {
569
				startLineFound = true
570
			}
571
			continue
572
		}
573
		result := stopRe.FindStringSubmatch(line)
574
		if len(result) == 2 {
575
			nUpdatable, err := strconv.Atoi(result[1])
576
			if err != nil {
577
				return nil, xerrors.Errorf(
578
					"Failed to scan upgradable packages number. line: %s", line)
579
			}
580
			if nUpdatable != len(updatableNames) {
581
				return nil, xerrors.Errorf(
582
					"Failed to scan upgradable packages, expected: %s, detected: %d",
583
					result[1], len(updatableNames))
584
			}
585
			stopLineFound = true
586
			o.log.Debugf("Found the stop line. line: %s", line)
587
			break
588
		}
589
		updatableNames = append(updatableNames, strings.Fields(line)...)
590
	}
591
	if !startLineFound {
592
		// no upgrades
593
		return
594
	}
595
	if !stopLineFound {
596
		// There are upgrades, but not found the stop line.
597
		return nil, xerrors.New("Failed to scan upgradable packages")
598
	}
599
	return
600
}
601
602
// DetectedCveID has CveID, Confidence and DetectionMethod fields
603
// LenientMatching will be true if this vulnerability is not detected by accurate version matching.
604
// see https://github.com/future-architect/vuls/pull/328
605
type DetectedCveID struct {
606
	CveID      string
607
	Confidence models.Confidence
608
}
609
610
func (o *debian) scanChangelogs(updatablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) {
611
	type response struct {
612
		pack           *models.Package
613
		DetectedCveIDs []DetectedCveID
614
	}
615
	resChan := make(chan response, len(updatablePacks))
616
	errChan := make(chan error, len(updatablePacks))
617
	reqChan := make(chan models.Package, len(updatablePacks))
618
	defer close(resChan)
619
	defer close(errChan)
620
	defer close(reqChan)
621
622
	go func() {
623
		for _, pack := range updatablePacks {
624
			reqChan <- pack
625
		}
626
	}()
627
628
	timeout := time.After(30 * 60 * time.Second)
629
	concurrency := 10
630
	tasks := util.GenWorkers(concurrency)
631
	for range updatablePacks {
632
		tasks <- func() {
633
			select {
634
			case pack := <-reqChan:
635
				func(p models.Package) {
636
					changelog := o.getChangelogCache(meta, p)
637
					if 0 < len(changelog) {
638
						cveIDs, pack := o.getCveIDsFromChangelog(changelog, p.Name, p.Version)
639
						resChan <- response{pack, cveIDs}
640
						return
641
					}
642
643
					// if the changelog is not in cache or failed to get from local cache,
644
					// get the changelog of the package via internet.
645
					// After that, store it in the cache.
646
					if cveIDs, pack, err := o.fetchParseChangelog(p); err != nil {
647
						errChan <- err
648
					} else {
649
						resChan <- response{pack, cveIDs}
650
					}
651
				}(pack)
652
			}
653
		}
654
	}
655
656
	// { DetectedCveID{} : [package] }
657
	cvePackages := make(map[DetectedCveID][]string)
658
	errs := []error{}
659
	for i := 0; i < len(updatablePacks); i++ {
660
		select {
661
		case response := <-resChan:
662
			if response.pack == nil {
663
				continue
664
			}
665
			o.Packages[response.pack.Name] = *response.pack
666
			cves := response.DetectedCveIDs
667
			for _, cve := range cves {
668
				packNames, ok := cvePackages[cve]
669
				if ok {
670
					packNames = append(packNames, response.pack.Name)
671
				} else {
672
					packNames = []string{response.pack.Name}
673
				}
674
				cvePackages[cve] = packNames
675
			}
676
			o.log.Infof("(%d/%d) Scanned %s: %s",
677
				i+1, len(updatablePacks), response.pack.Name, cves)
678
		case err := <-errChan:
679
			errs = append(errs, err)
680
		case <-timeout:
681
			errs = append(errs, xerrors.New("Timeout scanPackageCveIDs"))
682
		}
683
	}
684
	if 0 < len(errs) {
685
		return nil, xerrors.Errorf("errs: %w", errs)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
686
	}
687
688
	var cveIDs []DetectedCveID
689
	for k := range cvePackages {
690
		cveIDs = append(cveIDs, k)
691
	}
692
	o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs)
693
	vinfos := models.VulnInfos{}
694
	for cveID, names := range cvePackages {
695
		affected := models.PackageFixStatuses{}
696
		for _, n := range names {
697
			affected = append(affected, models.PackageFixStatus{Name: n})
698
		}
699
700
		vinfos[cveID.CveID] = models.VulnInfo{
701
			CveID:            cveID.CveID,
702
			Confidences:      models.Confidences{cveID.Confidence},
703
			AffectedPackages: affected,
704
		}
705
	}
706
707
	// Update meta package information of changelog cache to the latest one.
708
	meta.Packs = updatablePacks
709
	if err := cache.DB.RefreshMeta(*meta); err != nil {
710
		return nil, err
711
	}
712
713
	return vinfos, nil
714
}
715
716
func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string {
717
	cachedPack, found := meta.Packs[pack.Name]
718
	if !found {
719
		o.log.Debugf("Not found in cache: %s", pack.Name)
720
		return ""
721
	}
722
723
	if cachedPack.NewVersion != pack.NewVersion {
724
		o.log.Debugf("Expired: %s, cache: %s, new: %s",
725
			pack.Name, cachedPack.NewVersion, pack.NewVersion)
726
		return ""
727
	}
728
	changelog, err := cache.DB.GetChangelog(meta.Name, pack.Name)
729
	if err != nil {
730
		o.log.Warnf("Failed to get changelog. bucket: %s, key:%s, err: %s",
731
			meta.Name, pack.Name, err)
732
		return ""
733
	}
734
	if len(changelog) == 0 {
735
		o.log.Debugf("Empty string: %s", pack.Name)
736
		return ""
737
	}
738
739
	o.log.Debugf("Hit: %s, %s, cache: %s, new: %s len: %d, %s...",
740
		meta.Name, pack.Name, cachedPack.NewVersion, pack.NewVersion, len(changelog), util.Truncate(changelog, 30))
741
	return changelog
742
}
743
744
func (o *debian) fetchParseChangelog(pack models.Package) ([]DetectedCveID, *models.Package, error) {
745
	cmd := ""
746
	switch o.Distro.Family {
747
	case config.Ubuntu, config.Raspbian:
748
		cmd = fmt.Sprintf(`PAGER=cat apt-get -q=2 changelog %s`, pack.Name)
749
	case config.Debian:
750
		cmd = fmt.Sprintf(`PAGER=cat aptitude -q=2 changelog %s`, pack.Name)
751
	}
752
	cmd = util.PrependProxyEnv(cmd)
753
754
	r := o.exec(cmd, noSudo)
755
	if !r.isSuccess() {
756
		o.log.Warnf("Failed to SSH: %s", r)
757
		// Ignore this Error.
758
		return nil, nil, nil
759
	}
760
761
	stdout := strings.Replace(r.Stdout, "\r", "", -1)
762
	cveIDs, clogFilledPack := o.getCveIDsFromChangelog(stdout, pack.Name, pack.Version)
763
764
	if clogFilledPack.Changelog.Method != models.FailedToGetChangelog {
765
		err := cache.DB.PutChangelog(
766
			o.getServerInfo().GetServerName(), pack.Name, pack.Changelog.Contents)
767
		if err != nil {
768
			return nil, nil, xerrors.New("Failed to put changelog into cache")
769
		}
770
	}
771
772
	// No error will be returned. Only logging.
773
	return cveIDs, clogFilledPack, nil
774
}
775
776
func (o *debian) getCveIDsFromChangelog(
777
	changelog, name, ver string) ([]DetectedCveID, *models.Package) {
778
779
	if cveIDs, pack, err := o.parseChangelog(
780
		changelog, name, ver, models.ChangelogExactMatch); err == nil {
781
		return cveIDs, pack
782
	}
783
784
	var verAfterColon string
785
786
	splittedByColon := strings.Split(ver, ":")
787
	if 1 < len(splittedByColon) {
788
		verAfterColon = splittedByColon[1]
789
		if cveIDs, pack, err := o.parseChangelog(
790
			changelog, name, verAfterColon, models.ChangelogLenientMatch); err == nil {
791
			return cveIDs, pack
792
		}
793
	}
794
795
	delim := []string{"+", "~", "build"}
796
	switch o.Distro.Family {
797
	case config.Ubuntu:
798
		delim = append(delim, config.Ubuntu)
799
	case config.Debian:
800
	case config.Raspbian:
801
	}
802
803
	for _, d := range delim {
804
		ss := strings.Split(ver, d)
805
		if 1 < len(ss) {
806
			if cveIDs, pack, err := o.parseChangelog(
807
				changelog, name, ss[0], models.ChangelogLenientMatch); err == nil {
808
				return cveIDs, pack
809
			}
810
		}
811
812
		ss = strings.Split(verAfterColon, d)
813
		if 1 < len(ss) {
814
			if cveIDs, pack, err := o.parseChangelog(
815
				changelog, name, ss[0], models.ChangelogLenientMatch); err == nil {
816
				return cveIDs, pack
817
			}
818
		}
819
	}
820
821
	// Only logging the error.
822
	o.log.Warnf("Failed to find the version in changelog: %s-%s", name, ver)
823
	o.log.Debugf("Changelog of %s-%s: %s", name, ver, changelog)
824
825
	// If the version is not in changelog, return entire changelog to put into cache
826
	pack := o.Packages[name]
827
	pack.Changelog = models.Changelog{
828
		Contents: changelog,
829
		Method:   models.FailedToFindVersionInChangelog,
830
	}
831
832
	return []DetectedCveID{}, &pack
833
}
834
835
var cveRe = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`)
836
837
// Collect CVE-IDs included in the changelog.
838
// The version specified in argument(versionOrLater) is used to compare.
839
func (o *debian) parseChangelog(changelog, name, ver string, confidence models.Confidence) ([]DetectedCveID, *models.Package, error) {
840
	installedVer, err := version.NewVersion(ver)
841
	if err != nil {
842
		return nil, nil, xerrors.Errorf("Failed to parse installed version: %s, err: %w", ver, err)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
843
	}
844
	buf, cveIDs := []string{}, []string{}
845
	scanner := bufio.NewScanner(strings.NewReader(changelog))
846
	found := false
847
	for scanner.Scan() {
848
		line := scanner.Text()
849
		buf = append(buf, line)
850
		if matches := cveRe.FindAllString(line, -1); 0 < len(matches) {
851
			for _, m := range matches {
852
				cveIDs = util.AppendIfMissing(cveIDs, m)
853
			}
854
		}
855
856
		ss := strings.Fields(line)
857
		if len(ss) < 2 {
858
			continue
859
		}
860
861
		if !strings.HasPrefix(ss[1], "(") || !strings.HasSuffix(ss[1], ")") {
862
			continue
863
		}
864
		clogVer, err := version.NewVersion(ss[1][1 : len(ss[1])-1])
865
		if err != nil {
866
			continue
867
		}
868
		if installedVer.Equal(clogVer) || installedVer.GreaterThan(clogVer) {
869
			found = true
870
			break
871
		}
872
	}
873
874
	if !found {
875
		pack := o.Packages[name]
876
		pack.Changelog = models.Changelog{
877
			Contents: "",
878
			Method:   models.FailedToFindVersionInChangelog,
879
		}
880
		return nil, &pack, xerrors.Errorf(
881
			"Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s",
882
			name, ver)
883
	}
884
885
	clog := models.Changelog{
886
		Contents: strings.Join(buf[0:len(buf)-1], "\n"),
887
		Method:   confidence.DetectionMethod,
888
	}
889
	pack := o.Packages[name]
890
	pack.Changelog = clog
891
892
	cves := []DetectedCveID{}
893
	for _, id := range cveIDs {
894
		cves = append(cves, DetectedCveID{id, confidence})
895
	}
896
897
	return cves, &pack, nil
898
}
899
900
func (o *debian) splitAptCachePolicy(stdout string) map[string]string {
901
	re := regexp.MustCompile(`(?m:^[^ \t]+:\r?\n)`)
902
	ii := re.FindAllStringIndex(stdout, -1)
903
	ri := []int{}
904
	for i := len(ii) - 1; 0 <= i; i-- {
905
		ri = append(ri, ii[i][0])
906
	}
907
	splitted := []string{}
908
	lasti := len(stdout)
909
	for _, i := range ri {
910
		splitted = append(splitted, stdout[i:lasti])
911
		lasti = i
912
	}
913
914
	packAptPolicy := map[string]string{}
915
	for _, r := range splitted {
916
		packName := r[:strings.Index(r, ":")]
917
		packAptPolicy[packName] = r
918
	}
919
	return packAptPolicy
920
}
921
922
type packCandidateVer struct {
923
	Name      string
924
	Installed string
925
	Candidate string
926
	Repo      string
927
}
928
929
// parseAptCachePolicy the stdout of parse pat-get cache policy
930
func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, error) {
931
	ver := packCandidateVer{Name: name}
932
	lines := strings.Split(stdout, "\n")
933
	isRepoline := false
934
	for _, line := range lines {
935
		fields := strings.Fields(line)
936
		if len(fields) < 2 {
937
			continue
938
		}
939
		switch fields[0] {
940
		case "Installed:":
941
			ver.Installed = fields[1]
942
		case "Candidate:":
943
			ver.Candidate = fields[1]
944
			goto nextline
945
		default:
946
			// nop
947
		}
948
		if ver.Candidate != "" && strings.Contains(line, ver.Candidate) {
949
			isRepoline = true
950
			goto nextline
951
		}
952
953
		if isRepoline {
954
			ss := strings.Split(strings.TrimSpace(line), " ")
955
			if len(ss) == 5 {
956
				ver.Repo = ss[2]
957
				return ver, nil
958
			}
959
		}
960
	nextline:
961
	}
962
	return ver, xerrors.Errorf("Unknown Format: %s", stdout)
963
}
964
965
func (o *debian) checkrestart() error {
966
	initName, err := o.detectInitSystem()
967
	if err != nil {
968
		o.log.Warn(err)
969
		// continue scanning
970
	}
971
972
	cmd := "LANGUAGE=en_US.UTF-8 checkrestart"
973
	r := o.exec(cmd, sudo)
974
	if !r.isSuccess() {
975
		return xerrors.Errorf(
976
			"Failed to %s. status: %d, stdout: %s, stderr: %s",
977
			cmd, r.ExitStatus, r.Stdout, r.Stderr)
978
	}
979
	packs, unknownServices := o.parseCheckRestart(r.Stdout)
980
	pidService := map[string]string{}
981
	if initName == upstart {
982
		for _, s := range unknownServices {
983
			cmd := "LANGUAGE=en_US.UTF-8 initctl status " + s
984
			r := o.exec(cmd, sudo)
985
			if !r.isSuccess() {
986
				continue
987
			}
988
			if ss := strings.Fields(r.Stdout); len(ss) == 4 && ss[2] == "process" {
989
				pidService[ss[3]] = s
990
			}
991
		}
992
	}
993
994
	for i, p := range packs {
995
		pack := o.Packages[p.Name]
996
		pack.NeedRestartProcs = p.NeedRestartProcs
997
		o.Packages[p.Name] = pack
998
999
		for j, proc := range p.NeedRestartProcs {
1000
			if proc.HasInit == false {
1001
				continue
1002
			}
1003
			packs[i].NeedRestartProcs[j].InitSystem = initName
1004
			if initName == systemd {
1005
				name, err := o.detectServiceName(proc.PID)
1006
				if err != nil {
1007
					o.log.Warn(err)
1008
					// continue scanning
1009
				}
1010
				packs[i].NeedRestartProcs[j].ServiceName = name
1011
			} else {
1012
				if proc.ServiceName == "" {
1013
					if ss := strings.Fields(r.Stdout); len(ss) == 4 && ss[2] == "process" {
1014
						if name, ok := pidService[ss[3]]; ok {
1015
							packs[i].NeedRestartProcs[j].ServiceName = name
1016
						}
1017
					}
1018
				}
1019
			}
1020
		}
1021
		o.Packages[p.Name] = p
1022
	}
1023
	return nil
1024
}
1025
1026
func (o *debian) parseCheckRestart(stdout string) (models.Packages, []string) {
1027
	services := []string{}
1028
	scanner := bufio.NewScanner(strings.NewReader(stdout))
1029
	for scanner.Scan() {
1030
		line := scanner.Text()
1031
		if strings.HasPrefix(line, "service") && strings.HasSuffix(line, "restart") {
1032
			ss := strings.Fields(line)
1033
			if len(ss) != 3 {
1034
				continue
1035
			}
1036
			services = append(services, ss[1])
1037
		}
1038
	}
1039
1040
	packs := models.Packages{}
1041
	packName := ""
1042
	hasInit := true
1043
	scanner = bufio.NewScanner(strings.NewReader(stdout))
1044
	for scanner.Scan() {
1045
		line := scanner.Text()
1046
		if strings.HasSuffix(line, "do not seem to have an associated init script to restart them:") {
1047
			hasInit = false
1048
			continue
1049
		}
1050
		if strings.HasSuffix(line, ":") && len(strings.Fields(line)) == 1 {
1051
			packName = strings.TrimSuffix(line, ":")
1052
			continue
1053
		}
1054
		if strings.HasPrefix(line, "\t") {
1055
			ss := strings.Fields(line)
1056
			if len(ss) != 2 {
1057
				continue
1058
			}
1059
1060
			serviceName := ""
1061
			for _, s := range services {
1062
				if packName == s {
1063
					serviceName = s
1064
				}
1065
			}
1066
			if p, ok := packs[packName]; ok {
1067
				p.NeedRestartProcs = append(p.NeedRestartProcs, models.NeedRestartProcess{
1068
					PID:         ss[0],
1069
					Path:        ss[1],
1070
					ServiceName: serviceName,
1071
					HasInit:     hasInit,
1072
				})
1073
				packs[packName] = p
1074
			} else {
1075
				packs[packName] = models.Package{
1076
					Name: packName,
1077
					NeedRestartProcs: []models.NeedRestartProcess{
1078
						{
1079
							PID:         ss[0],
1080
							Path:        ss[1],
1081
							ServiceName: serviceName,
1082
							HasInit:     hasInit,
1083
						},
1084
					},
1085
				}
1086
			}
1087
		}
1088
	}
1089
1090
	unknownServices := []string{}
1091
	for _, s := range services {
1092
		found := false
1093
		for _, p := range packs {
1094
			for _, proc := range p.NeedRestartProcs {
1095
				if proc.ServiceName == s {
1096
					found = true
1097
				}
1098
			}
1099
		}
1100
		if !found {
1101
			unknownServices = append(unknownServices, s)
1102
		}
1103
	}
1104
	return packs, unknownServices
1105
}
1106