Passed
Pull Request — master (#810)
by kota
06:36
created

scan.*redhatBase.scanUsingYum   F

Complexity

Conditions 26

Size

Total Lines 101
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 69
dl 0
loc 101
rs 0
c 0
b 0
f 0
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like scan.*redhatBase.scanUsingYum often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
	"strings"
25
	"time"
26
27
	"github.com/future-architect/vuls/config"
28
	"github.com/future-architect/vuls/models"
29
	"github.com/future-architect/vuls/util"
30
	"golang.org/x/xerrors"
31
32
	ver "github.com/knqyf263/go-rpm-version"
33
)
34
35
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/redhat.rb
36
func detectRedhat(c config.ServerInfo) (bool, osTypeInterface) {
37
	if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() {
38
		util.Log.Warnf("Fedora not tested yet: %s", r)
39
		return true, &unknown{}
40
	}
41
42
	if r := exec(c, "ls /etc/oracle-release", noSudo); r.isSuccess() {
43
		// Need to discover Oracle Linux first, because it provides an
44
		// /etc/redhat-release that matches the upstream distribution
45
		if r := exec(c, "cat /etc/oracle-release", noSudo); r.isSuccess() {
46
			re := regexp.MustCompile(`(.*) release (\d[\d\.]*)`)
47
			result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
48
			if len(result) != 3 {
49
				util.Log.Warnf("Failed to parse Oracle Linux version: %s", r)
50
				return true, newOracle(c)
51
			}
52
53
			ora := newOracle(c)
54
			release := result[2]
55
			ora.setDistro(config.Oracle, release)
56
			return true, ora
57
		}
58
	}
59
60
	// https://bugzilla.redhat.com/show_bug.cgi?id=1332025
61
	// CentOS cloud image
62
	if r := exec(c, "ls /etc/centos-release", noSudo); r.isSuccess() {
63
		if r := exec(c, "cat /etc/centos-release", noSudo); r.isSuccess() {
64
			re := regexp.MustCompile(`(.*) release (\d[\d\.]*)`)
65
			result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
66
			if len(result) != 3 {
67
				util.Log.Warnf("Failed to parse CentOS version: %s", r)
68
				return true, newCentOS(c)
69
			}
70
71
			release := result[2]
72
			switch strings.ToLower(result[1]) {
73
			case "centos", "centos linux":
74
				cent := newCentOS(c)
75
				cent.setDistro(config.CentOS, release)
76
				return true, cent
77
			default:
78
				util.Log.Warnf("Failed to parse CentOS: %s", r)
79
			}
80
		}
81
	}
82
83
	if r := exec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() {
84
		// https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/
85
		// e.g.
86
		// $ cat /etc/redhat-release
87
		// CentOS release 6.5 (Final)
88
		if r := exec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() {
89
			re := regexp.MustCompile(`(.*) release (\d[\d\.]*)`)
90
			result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
91
			if len(result) != 3 {
92
				util.Log.Warnf("Failed to parse RedHat/CentOS version: %s", r)
93
				return true, newCentOS(c)
94
			}
95
96
			release := result[2]
97
			switch strings.ToLower(result[1]) {
98
			case "centos", "centos linux":
99
				cent := newCentOS(c)
100
				cent.setDistro(config.CentOS, release)
101
				return true, cent
102
			default:
103
				// RHEL
104
				rhel := newRHEL(c)
105
				rhel.setDistro(config.RedHat, release)
106
				return true, rhel
107
			}
108
		}
109
	}
110
111
	if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
112
		family := config.Amazon
113
		release := "unknown"
114
		if r := exec(c, "cat /etc/system-release", noSudo); r.isSuccess() {
115
			if strings.HasPrefix(r.Stdout, "Amazon Linux release 2") {
116
				fields := strings.Fields(r.Stdout)
117
				release = fmt.Sprintf("%s %s", fields[3], fields[4])
118
			} else if strings.HasPrefix(r.Stdout, "Amazon Linux 2") {
119
				fields := strings.Fields(r.Stdout)
120
				release = strings.Join(fields[2:], " ")
121
			} else {
122
				fields := strings.Fields(r.Stdout)
123
				if len(fields) == 5 {
124
					release = fields[4]
125
				}
126
			}
127
		}
128
		amazon := newAmazon(c)
129
		amazon.setDistro(family, release)
130
		return true, amazon
131
	}
132
133
	util.Log.Debugf("Not RedHat like Linux. servername: %s", c.ServerName)
134
	return false, &unknown{}
135
}
136
137
// inherit OsTypeInterface
138
type redhatBase struct {
139
	base
140
	sudo rootPriv
141
}
142
143
type rootPriv interface {
144
	repoquery() bool
145
	yumRepolist() bool
146
	yumUpdateInfo() bool
147
	yumChangelog() bool
148
}
149
150
type cmd struct {
151
	cmd                 string
152
	expectedStatusCodes []int
153
}
154
155
var exitStatusZero = []int{0}
156
157
func (o *redhatBase) execCheckIfSudoNoPasswd(cmds []cmd) error {
158
	for _, c := range cmds {
159
		cmd := util.PrependProxyEnv(c.cmd)
160
		o.log.Infof("Checking... sudo %s", cmd)
161
		r := o.exec(util.PrependProxyEnv(cmd), sudo)
162
		if !r.isSuccess(c.expectedStatusCodes...) {
163
			o.log.Errorf("Check sudo or proxy settings: %s", r)
164
			return xerrors.Errorf("Failed to sudo: %s", r)
165
		}
166
	}
167
	o.log.Infof("Sudo... Pass")
168
	return nil
169
}
170
171
func (o *redhatBase) execCheckDeps(packNames []string) error {
172
	for _, name := range packNames {
173
		cmd := "rpm -q " + name
174
		if r := o.exec(cmd, noSudo); !r.isSuccess() {
175
			msg := fmt.Sprintf("%s is not installed", name)
176
			o.log.Errorf(msg)
0 ignored issues
show
introduced by
can't check non-constant format in call to Errorf
Loading history...
177
			return xerrors.New(msg)
178
		}
179
	}
180
	o.log.Infof("Dependencies ... Pass")
181
	return nil
182
}
183
184
func (o *redhatBase) preCure() error {
185
	if err := o.detectIPAddr(); err != nil {
186
		o.log.Debugf("Failed to detect IP addresses: %s", err)
187
	}
188
	// Ignore this error as it just failed to detect the IP addresses
189
	return nil
190
}
191
192
func (o *redhatBase) postScan() error {
193
	if o.isExecYumPS() {
194
		if err := o.yumPS(); err != nil {
195
			return xerrors.Errorf("Failed to execute yum-ps. err: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
196
		}
197
	}
198
	if o.isExecNeedsRestarting() {
199
		if err := o.needsRestarting(); err != nil {
200
			return xerrors.Errorf("Failed to execute need-restarting: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
201
		}
202
	}
203
	return nil
204
}
205
206
func (o *redhatBase) detectIPAddr() (err error) {
207
	o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
208
	o.ServerInfo.IPv4Addrs, o.ServerInfo.IPv6Addrs, err = o.ip()
209
	return err
210
}
211
212
func (o *redhatBase) scanPackages() error {
213
	installed, err := o.scanInstalledPackages()
214
	if err != nil {
215
		o.log.Errorf("Failed to scan installed packages: %s", err)
216
		return err
217
	}
218
219
	rebootRequired, err := o.rebootRequired()
220
	if err != nil {
221
		o.log.Errorf("Failed to detect the kernel reboot required: %s", err)
222
		return err
223
	}
224
	o.Kernel.RebootRequired = rebootRequired
225
226
	if o.getServerInfo().Mode.IsOffline() {
227
		switch o.Distro.Family {
228
		case config.Amazon:
229
			// nop
230
		default:
231
			o.Packages = installed
232
			return nil
233
		}
234
	} else if o.Distro.Family == config.RedHat {
235
		if o.getServerInfo().Mode.IsFast() {
236
			o.Packages = installed
237
			return nil
238
		}
239
	}
240
241
	updatable, err := o.scanUpdatablePackages()
242
	if err != nil {
243
		o.log.Errorf("Failed to scan installed packages: %s", err)
244
		return err
245
	}
246
	installed.MergeNewVersion(updatable)
247
	o.Packages = installed
248
249
	var unsecures models.VulnInfos
250
	if unsecures, err = o.scanUnsecurePackages(updatable); err != nil {
251
		o.log.Errorf("Failed to scan vulnerable packages: %s", err)
252
		return err
253
	}
254
	o.VulnInfos = unsecures
255
	return nil
256
}
257
258
func (o *redhatBase) rebootRequired() (bool, error) {
259
	r := o.exec("rpm -q --last kernel", noSudo)
260
	scanner := bufio.NewScanner(strings.NewReader(r.Stdout))
261
	if !r.isSuccess(0, 1) {
262
		return false, xerrors.Errorf("Failed to detect the last installed kernel : %v", r)
263
	}
264
	if !r.isSuccess() || !scanner.Scan() {
265
		return false, nil
266
	}
267
	lastInstalledKernelVer := strings.Fields(scanner.Text())[0]
268
	running := fmt.Sprintf("kernel-%s", o.Kernel.Release)
269
	return running != lastInstalledKernelVer, nil
270
}
271
272
func (o *redhatBase) scanInstalledPackages() (models.Packages, error) {
273
	release, version, err := o.runningKernel()
274
	if err != nil {
275
		return nil, err
276
	}
277
	o.Kernel = models.Kernel{
278
		Release: release,
279
		Version: version,
280
	}
281
282
	r := o.exec(rpmQa(o.Distro), noSudo)
283
	if !r.isSuccess() {
284
		return nil, xerrors.Errorf("Scan packages failed: %s", r)
285
	}
286
	installed, _, err := o.parseInstalledPackages(r.Stdout)
287
	if err != nil {
288
		return nil, err
289
	}
290
	return installed, nil
291
}
292
293
func (o *redhatBase) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
294
	installed := models.Packages{}
295
	latestKernelRelease := ver.NewVersion("")
296
297
	// openssl 0 1.0.1e	30.el6.11 x86_64
298
	lines := strings.Split(stdout, "\n")
299
	for _, line := range lines {
300
		if trimed := strings.TrimSpace(line); len(trimed) != 0 {
301
			pack, err := o.parseInstalledPackagesLine(line)
302
			if err != nil {
303
				return nil, nil, err
304
			}
305
306
			// Kernel package may be isntalled multiple versions.
307
			// From the viewpoint of vulnerability detection,
308
			// pay attention only to the running kernel
309
			isKernel, running := isRunningKernel(pack, o.Distro.Family, o.Kernel)
310
			if isKernel {
311
				if o.Kernel.Release == "" {
312
					// When the running kernel release is unknown,
313
					// use the latest release among the installed release
314
					kernelRelease := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release))
315
					if kernelRelease.LessThan(latestKernelRelease) {
316
						continue
317
					}
318
					latestKernelRelease = kernelRelease
319
				} else if !running {
320
					o.log.Debugf("Not a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
321
					continue
322
				} else {
323
					o.log.Debugf("Found a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
324
				}
325
			}
326
			installed[pack.Name] = pack
327
		}
328
	}
329
	return installed, nil, nil
330
}
331
332
func (o *redhatBase) parseInstalledPackagesLine(line string) (models.Package, error) {
333
	fields := strings.Fields(line)
334
	if len(fields) != 5 {
335
		return models.Package{},
336
			xerrors.Errorf("Failed to parse package line: %s", line)
337
	}
338
	ver := ""
339
	epoch := fields[1]
340
	if epoch == "0" || epoch == "(none)" {
341
		ver = fields[2]
342
	} else {
343
		ver = fmt.Sprintf("%s:%s", epoch, fields[2])
344
	}
345
346
	return models.Package{
347
		Name:    fields[0],
348
		Version: ver,
349
		Release: fields[3],
350
		Arch:    fields[4],
351
	}, nil
352
}
353
354
func (o *redhatBase) yumMakeCache() error {
355
	cmd := `yum makecache`
356
	//TODO check yum makecache without root priv on RHEL
357
	r := o.exec(util.PrependProxyEnv(cmd), noSudo)
358
	if !r.isSuccess() {
359
		return xerrors.Errorf("Failed to SSH: %s", r)
360
	}
361
	return nil
362
}
363
364
func (o *redhatBase) scanUpdatablePackages() (models.Packages, error) {
365
	if err := o.yumMakeCache(); err != nil {
366
		return nil, xerrors.Errorf("Failed to `yum makecache`: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
367
	}
368
369
	cmd := `repoquery --all --pkgnarrow=updates --qf="%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{REPO}"`
370
	for _, repo := range o.getServerInfo().Enablerepo {
371
		cmd += " --enablerepo=" + repo
372
	}
373
374
	r := o.exec(util.PrependProxyEnv(cmd), o.sudo.repoquery())
375
	if !r.isSuccess() {
376
		return nil, xerrors.Errorf("Failed to SSH: %s", r)
377
	}
378
379
	// Collect Updateble packages, installed, candidate version and repository.
380
	return o.parseUpdatablePacksLines(r.Stdout)
381
}
382
383
// parseUpdatablePacksLines parse the stdout of repoquery to get package name, candidate version
384
func (o *redhatBase) parseUpdatablePacksLines(stdout string) (models.Packages, error) {
385
	updatable := models.Packages{}
386
	lines := strings.Split(stdout, "\n")
387
	for _, line := range lines {
388
		// TODO remove
389
		// if strings.HasPrefix(line, "Obsoleting") ||
390
		// strings.HasPrefix(line, "Security:") {
391
		// // see https://github.com/future-architect/vuls/issues/165
392
		// continue
393
		// }
394
		if len(strings.TrimSpace(line)) == 0 {
395
			continue
396
		} else if strings.HasPrefix(line, "Loading") {
397
			continue
398
		}
399
		pack, err := o.parseUpdatablePacksLine(line)
400
		if err != nil {
401
			return updatable, err
402
		}
403
		updatable[pack.Name] = pack
404
	}
405
	return updatable, nil
406
}
407
408
func (o *redhatBase) parseUpdatablePacksLine(line string) (models.Package, error) {
409
	fields := strings.Fields(line)
410
	if len(fields) < 5 {
411
		return models.Package{}, xerrors.Errorf("Unknown format: %s, fields: %s", line, fields)
412
	}
413
414
	ver := ""
415
	epoch := fields[1]
416
	if epoch == "0" {
417
		ver = fields[2]
418
	} else {
419
		ver = fmt.Sprintf("%s:%s", epoch, fields[2])
420
	}
421
422
	repos := strings.Join(fields[4:], " ")
423
424
	p := models.Package{
425
		Name:       fields[0],
426
		NewVersion: ver,
427
		NewRelease: fields[3],
428
		Repository: repos,
429
	}
430
	return p, nil
431
}
432
433
func (o *redhatBase) isExecScanUsingYum() bool {
434
	if o.getServerInfo().Mode.IsOffline() {
435
		return false
436
	}
437
	if o.Distro.Family == config.CentOS {
438
		// CentOS doesn't have security channel
439
		return false
440
	}
441
	if o.getServerInfo().Mode.IsFastRoot() || o.getServerInfo().Mode.IsDeep() {
442
		return true
443
	}
444
	return true
445
}
446
447
func (o *redhatBase) isExecFillChangelogs() bool {
448
	if o.getServerInfo().Mode.IsOffline() {
449
		return false
450
	}
451
	// Amazon linux has no changelos for updates
452
	return o.getServerInfo().Mode.IsDeep() &&
453
		o.Distro.Family != config.Amazon
454
}
455
456
func (o *redhatBase) isExecScanChangelogs() bool {
457
	if o.getServerInfo().Mode.IsOffline() ||
458
		o.getServerInfo().Mode.IsFast() ||
459
		o.getServerInfo().Mode.IsFastRoot() {
460
		return false
461
	}
462
	return true
463
}
464
465
func (o *redhatBase) isExecYumPS() bool {
466
	// RedHat has no yum-ps
467
	switch o.Distro.Family {
468
	case config.RedHat,
469
		config.OpenSUSE,
470
		config.OpenSUSELeap,
471
		config.SUSEEnterpriseServer,
472
		config.SUSEEnterpriseDesktop,
473
		config.SUSEOpenstackCloud:
474
		return false
475
	}
476
477
	// yum ps needs internet connection
478
	if o.getServerInfo().Mode.IsOffline() || o.getServerInfo().Mode.IsFast() {
479
		return false
480
	}
481
	return true
482
}
483
484
func (o *redhatBase) isExecNeedsRestarting() bool {
485
	switch o.Distro.Family {
486
	case config.OpenSUSE,
487
		config.OpenSUSELeap,
488
		config.SUSEEnterpriseServer,
489
		config.SUSEEnterpriseDesktop,
490
		config.SUSEOpenstackCloud:
491
		// TODO zypper ps
492
		// https://github.com/future-architect/vuls/issues/696
493
		return false
494
	case config.RedHat, config.CentOS, config.Oracle:
495
		majorVersion, err := o.Distro.MajorVersion()
496
		if err != nil || majorVersion < 6 {
497
			o.log.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
0 ignored issues
show
introduced by
printf argument o.Distro has invalid or unknown type
Loading history...
498
			return false
499
		}
500
501
		if o.getServerInfo().Mode.IsOffline() {
502
			return false
503
		} else if o.getServerInfo().Mode.IsFastRoot() ||
504
			o.getServerInfo().Mode.IsDeep() {
505
			return true
506
		}
507
		return false
508
	}
509
510
	if o.getServerInfo().Mode.IsFast() {
511
		return false
512
	}
513
	return true
514
}
515
516
func (o *redhatBase) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) {
517
	if o.isExecFillChangelogs() {
518
		if err := o.fillChangelogs(updatable); err != nil {
519
			return nil, err
520
		}
521
	}
522
523
	if o.isExecScanUsingYum() {
524
		return o.scanUsingYum(updatable)
525
	}
526
527
	// Parse changelog because CentOS does not have security channel...
528
	if o.isExecScanChangelogs() {
529
		return o.scanChangelogs(updatable)
530
	}
531
532
	return models.VulnInfos{}, nil
533
}
534
535
func (o *redhatBase) fillChangelogs(updatables models.Packages) error {
536
	names := []string{}
537
	for name := range updatables {
538
		names = append(names, name)
539
	}
540
541
	if err := o.fillDiffChangelogs(names); err != nil {
542
		return err
543
	}
544
545
	emptyChangelogPackNames := []string{}
546
	for _, pack := range o.Packages {
547
		if pack.NewVersion != "" && pack.Changelog.Contents == "" {
548
			emptyChangelogPackNames = append(emptyChangelogPackNames, pack.Name)
549
		}
550
	}
551
552
	i := 0
553
	for _, name := range emptyChangelogPackNames {
554
		i++
555
		o.log.Infof("(%d/%d) Fetched Changelogs %s", i, len(emptyChangelogPackNames), name)
556
		if err := o.fillDiffChangelogs([]string{name}); err != nil {
557
			return err
558
		}
559
	}
560
561
	return nil
562
}
563
564
func (o *redhatBase) getAvailableChangelogs(packNames []string) (map[string]string, error) {
565
	yumopts := ""
566
	if 0 < len(o.getServerInfo().Enablerepo) {
567
		yumopts = " --enablerepo=" + strings.Join(o.getServerInfo().Enablerepo, ",")
568
	}
569
	if config.Conf.SkipBroken {
570
		yumopts += " --skip-broken"
571
	}
572
	if o.hasYumColorOption() {
573
		yumopts += " --color=never"
574
	}
575
	cmd := `yum changelog all updates %s %s | grep -A 1000000 "==================== Updated Packages ===================="`
576
	cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " "))
0 ignored issues
show
introduced by
can't check non-constant format in call to Sprintf
Loading history...
577
578
	r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumChangelog())
579
	if !r.isSuccess(0, 1) {
580
		return nil, xerrors.Errorf("Failed to SSH: %s", r)
581
	}
582
583
	return o.divideChangelogsIntoEachPackages(r.Stdout), nil
584
}
585
586
// Divide available change logs of all updatable packages into each package's changelog
587
func (o *redhatBase) divideChangelogsIntoEachPackages(stdout string) map[string]string {
588
	changelogs := make(map[string]string)
589
	scanner := bufio.NewScanner(strings.NewReader(stdout))
590
591
	crlf, newBlock := false, true
592
	packNameVer, contents := "", []string{}
593
	for scanner.Scan() {
594
		line := scanner.Text()
595
		if strings.HasPrefix(line, "==================== Updated Packages ====================") {
596
			continue
597
		}
598
		if len(strings.TrimSpace(line)) != 0 && newBlock {
599
			left := strings.Fields(line)[0]
600
			// ss := strings.Split(left, ".")
601
			// packNameVer = strings.Join(ss[0:len(ss)-1], ".")
602
			packNameVer = left
603
			newBlock = false
604
			continue
605
		}
606
		if len(strings.TrimSpace(line)) == 0 {
607
			if crlf {
608
				changelogs[packNameVer] = strings.Join(contents, "\n")
609
				packNameVer = ""
610
				contents = []string{}
611
				newBlock = true
612
				crlf = false
613
			} else {
614
				contents = append(contents, line)
615
				crlf = true
616
			}
617
		} else {
618
			contents = append(contents, line)
619
			crlf = false
620
		}
621
	}
622
	if 0 < len(contents) {
623
		changelogs[packNameVer] = strings.Join(contents, "\n")
624
	}
625
	return changelogs
626
}
627
628
func (o *redhatBase) fillDiffChangelogs(packNames []string) error {
629
	changelogs, err := o.getAvailableChangelogs(packNames)
630
	if err != nil {
631
		return err
632
	}
633
634
	for s := range changelogs {
635
		// name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
636
		name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
637
			var epochNameVerRel string
638
			if index := strings.Index(p.NewVersion, ":"); 0 < index {
639
				epoch := p.NewVersion[0:index]
640
				ver := p.NewVersion[index+1 : len(p.NewVersion)]
641
				epochNameVerRel = fmt.Sprintf("%s:%s-%s", epoch, p.Name, ver)
642
			} else {
643
				epochNameVerRel = fmt.Sprintf("%s-%s", p.Name, p.NewVersion)
644
			}
645
			return strings.HasPrefix(s, epochNameVerRel)
646
		})
647
648
		if found {
649
			var detectionMethod string
650
			diff, err := o.getDiffChangelog(pack, changelogs[s])
651
			if err == nil {
652
				detectionMethod = models.ChangelogExactMatchStr
653
			} else {
654
				o.log.Debug(err)
655
				// Try without epoch
656
				if index := strings.Index(pack.Version, ":"); 0 < index {
657
					pack.Version = pack.Version[index+1 : len(pack.Version)]
658
					o.log.Debug("Try without epoch", pack)
659
					diff, err = o.getDiffChangelog(pack, changelogs[s])
660
					if err != nil {
661
						o.log.Debugf("Failed to find the version in changelog: %s-%s-%s",
662
							pack.Name, pack.Version, pack.Release)
663
						if len(diff) == 0 {
664
							detectionMethod = models.FailedToGetChangelog
665
						} else {
666
							detectionMethod = models.FailedToFindVersionInChangelog
667
							diff = ""
668
						}
669
					} else {
670
						o.log.Debugf("Found the version in changelog without epoch: %s-%s-%s",
671
							pack.Name, pack.Version, pack.Release)
672
						detectionMethod = models.ChangelogLenientMatchStr
673
					}
674
				} else {
675
					if len(diff) == 0 {
676
						detectionMethod = models.FailedToGetChangelog
677
					} else {
678
						detectionMethod = models.FailedToFindVersionInChangelog
679
						diff = ""
680
					}
681
				}
682
			}
683
684
			pack = o.Packages[name]
685
			pack.Changelog = models.Changelog{
686
				Contents: diff,
687
				Method:   models.DetectionMethod(detectionMethod),
688
			}
689
			o.Packages[name] = pack
690
		}
691
	}
692
	return nil
693
}
694
695
func (o *redhatBase) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) {
696
	installedVer := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release))
697
	scanner := bufio.NewScanner(strings.NewReader(availableChangelog))
698
	diff := []string{}
699
	found := false
700
	for scanner.Scan() {
701
		line := scanner.Text()
702
		if !strings.HasPrefix(line, "* ") {
703
			diff = append(diff, line)
704
			continue
705
		}
706
707
		// openssh on RHEL
708
		//   openssh-server-6.6.1p1-35.el7_3.x86_64   rhui-rhel-7-server-rhui-rpms
709
		//   Wed Mar  1 21:00:00 2017 Jakub Jelen <[email protected]> - 6.6.1p1-35 + 0.9.3-9
710
		ss := strings.Split(line, " + ")
711
		if 1 < len(ss) {
712
			line = ss[0]
713
		}
714
715
		ss = strings.Split(line, " ")
716
		if len(ss) < 2 {
717
			diff = append(diff, line)
718
			continue
719
		}
720
		v := ss[len(ss)-1]
721
		v = strings.TrimPrefix(v, "-")
722
		v = strings.TrimPrefix(v, "[")
723
		v = strings.TrimSuffix(v, "]")
724
725
		// On Amazon often end with email address. <[email protected]> Go to next line
726
		if strings.HasPrefix(v, "<") && strings.HasSuffix(v, ">") {
727
			diff = append(diff, line)
728
			continue
729
		}
730
731
		version := ver.NewVersion(v)
732
		if installedVer.Equal(version) || installedVer.GreaterThan(version) {
733
			found = true
734
			break
735
		}
736
		diff = append(diff, line)
737
	}
738
739
	if len(diff) == 0 || !found {
740
		return availableChangelog,
741
			xerrors.Errorf("Failed to find the version in changelog: %s-%s-%s",
742
				pack.Name, pack.Version, pack.Release)
743
	}
744
	return strings.TrimSpace(strings.Join(diff, "\n")), nil
745
}
746
747
func (o *redhatBase) scanChangelogs(updatable models.Packages) (models.VulnInfos, error) {
748
	packCveIDs := make(map[string][]string)
749
	for name := range updatable {
750
		cveIDs := []string{}
751
		pack := o.Packages[name]
752
		if pack.Changelog.Method == models.FailedToFindVersionInChangelog {
753
			continue
754
		}
755
		scanner := bufio.NewScanner(strings.NewReader(pack.Changelog.Contents))
756
		for scanner.Scan() {
757
			if matches := cveRe.FindAllString(scanner.Text(), -1); 0 < len(matches) {
758
				for _, m := range matches {
759
					cveIDs = util.AppendIfMissing(cveIDs, m)
760
				}
761
			}
762
		}
763
		packCveIDs[name] = cveIDs
764
	}
765
766
	// transform datastructure
767
	// - From
768
	//	  "packname": []{"CVE-2017-1111", ".../
769
	//
770
	// - To
771
	//	   map {
772
	//		 "CVE-2017-1111": "packname",
773
	//	   }
774
	vinfos := models.VulnInfos{}
775
	for name, cveIDs := range packCveIDs {
776
		for _, cid := range cveIDs {
777
			if v, ok := vinfos[cid]; ok {
778
				v.AffectedPackages = append(v.AffectedPackages, models.PackageFixStatus{Name: name})
779
				vinfos[cid] = v
780
			} else {
781
				vinfos[cid] = models.VulnInfo{
782
					CveID:            cid,
783
					AffectedPackages: models.PackageFixStatuses{{Name: name}},
784
					Confidences:      models.Confidences{models.ChangelogExactMatch},
785
				}
786
			}
787
		}
788
	}
789
	return vinfos, nil
790
}
791
792
type distroAdvisoryCveIDs struct {
793
	DistroAdvisory models.DistroAdvisory
794
	CveIDs         []string
795
}
796
797
// Scaning unsecure packages using yum-plugin-security.
798
// Amazon, RHEL, Oracle Linux
799
func (o *redhatBase) scanUsingYum(updatable models.Packages) (models.VulnInfos, error) {
800
	if o.Distro.Family == config.CentOS {
801
		// CentOS has no security channel.
802
		return nil, xerrors.New(
803
			"yum updateinfo is not suppported on CentOS")
804
	}
805
806
	// get advisoryID(RHSA, ALAS, ELSA) - package name,version
807
	major, err := (o.Distro.MajorVersion())
808
	if err != nil {
809
		return nil, xerrors.Errorf("Not implemented yet: %s, err: %w", o.Distro, err)
0 ignored issues
show
introduced by
printf argument o.Distro has invalid or unknown type
Loading history...
introduced by
unrecognized printf verb 'w'
Loading history...
810
	}
811
812
	var cmd string
813
	if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major > 5 {
814
		cmd = "yum repolist --color=never"
815
		r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumRepolist())
816
		if !r.isSuccess() {
817
			return nil, xerrors.Errorf("Failed to SSH: %s", r)
818
		}
819
	}
820
821
	if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 {
822
		cmd = "yum list-security --security"
823
		if o.hasYumColorOption() {
824
			cmd += " --color=never"
825
		}
826
	} else {
827
		cmd = "yum updateinfo list updates --security --color=never"
828
	}
829
	r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo())
830
	if !r.isSuccess() {
831
		return nil, xerrors.Errorf("Failed to SSH: %s", r)
832
	}
833
	advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout)
834
835
	dict := make(map[string]models.Packages)
836
	for _, advIDPackNames := range advIDPackNamesList {
837
		packages := models.Packages{}
838
		for _, packName := range advIDPackNames.PackNames {
839
			pack, found := updatable[packName]
840
			if !found {
841
				return nil, xerrors.Errorf(
842
					"Package not found. pack: %#v", packName)
843
			}
844
			packages[pack.Name] = pack
845
			continue
846
		}
847
		dict[advIDPackNames.AdvisoryID] = packages
848
	}
849
850
	// get advisoryID(RHSA, ALAS, ELSA) - CVE IDs
851
	if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 {
852
		cmd = "yum info-security"
853
		if o.hasYumColorOption() {
854
			cmd += " --color=never"
855
		}
856
	} else {
857
		cmd = "yum updateinfo updates --security --color=never"
858
	}
859
	r = o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo())
860
	if !r.isSuccess() {
861
		return nil, xerrors.Errorf("Failed to SSH: %s", r)
862
	}
863
	advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout)
864
	if err != nil {
865
		return nil, err
866
	}
867
868
	// All information collected.
869
	// Convert to VulnInfos.
870
	vinfos := models.VulnInfos{}
871
	for _, advIDCveIDs := range advisoryCveIDsList {
872
		for _, cveID := range advIDCveIDs.CveIDs {
873
			vinfo, found := vinfos[cveID]
874
			if found {
875
				advAppended := append(vinfo.DistroAdvisories, advIDCveIDs.DistroAdvisory)
876
				vinfo.DistroAdvisories = advAppended
877
878
				packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
879
				for _, pack := range packs {
880
					vinfo.AffectedPackages = append(vinfo.AffectedPackages,
881
						models.PackageFixStatus{Name: pack.Name})
882
				}
883
			} else {
884
				packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
885
				affected := models.PackageFixStatuses{}
886
				for _, p := range packs {
887
					affected = append(affected, models.PackageFixStatus{Name: p.Name})
888
				}
889
				vinfo = models.VulnInfo{
890
					CveID:            cveID,
891
					DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
892
					AffectedPackages: affected,
893
					Confidences:      models.Confidences{models.YumUpdateSecurityMatch},
894
				}
895
			}
896
			vinfos[cveID] = vinfo
897
		}
898
	}
899
	return vinfos, nil
900
}
901
902
var horizontalRulePattern = regexp.MustCompile(`^=+$`)
903
904
func (o *redhatBase) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
905
	sectionState := Outside
906
	lines := strings.Split(stdout, "\n")
907
	lines = append(lines, "=============")
908
909
	// Amazon Linux AMI Security Information
910
	advisory := models.DistroAdvisory{}
911
912
	cveIDsSetInThisSection := make(map[string]bool)
913
914
	// use this flag to Collect CVE IDs in CVEs field.
915
	inDesctiption, inCves := false, false
916
917
	for _, line := range lines {
918
		line = strings.TrimSpace(line)
919
920
		// find the new section pattern
921
		if horizontalRulePattern.MatchString(line) {
922
			// set previous section's result to return-variable
923
			if sectionState == Content {
924
				foundCveIDs := []string{}
925
				for cveID := range cveIDsSetInThisSection {
926
					foundCveIDs = append(foundCveIDs, cveID)
927
				}
928
				result = append(result, distroAdvisoryCveIDs{
929
					DistroAdvisory: advisory,
930
					CveIDs:         foundCveIDs,
931
				})
932
933
				// reset for next section.
934
				cveIDsSetInThisSection = make(map[string]bool)
935
				inDesctiption, inCves = false, false
936
				advisory = models.DistroAdvisory{}
937
			}
938
939
			// Go to next section
940
			sectionState = o.changeSectionState(sectionState)
941
			continue
942
		}
943
944
		switch sectionState {
945
		case Header:
946
			switch o.Distro.Family {
947
			case config.CentOS:
948
				// CentOS has no security channel.
949
				return result, xerrors.New(
950
					"yum updateinfo is not suppported on CentOS")
951
			case config.RedHat, config.Amazon, config.Oracle:
952
				// nop
953
			}
954
955
		case Content:
956
			if found := o.isDescriptionLine(line); found {
957
				inDesctiption, inCves = true, false
958
				ss := strings.Split(line, " : ")
959
				advisory.Description += fmt.Sprintf("%s\n",
960
					strings.Join(ss[1:], " : "))
961
				continue
962
			}
963
964
			// severity
965
			if severity, found := o.parseYumUpdateinfoToGetSeverity(line); found {
966
				advisory.Severity = severity
967
				continue
968
			}
969
970
			// No need to parse in description except severity
971
			if inDesctiption {
972
				if ss := strings.Split(line, ": "); 1 < len(ss) {
973
					advisory.Description += fmt.Sprintf("%s\n",
974
						strings.Join(ss[1:], ": "))
975
				}
976
				continue
977
			}
978
979
			if found := o.isCvesHeaderLine(line); found {
980
				inCves = true
981
				ss := strings.Split(line, "CVEs : ")
982
				line = strings.Join(ss[1:], " ")
983
				cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
984
				for _, cveID := range cveIDs {
985
					cveIDsSetInThisSection[cveID] = true
986
				}
987
				continue
988
			}
989
990
			if inCves {
991
				cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
992
				for _, cveID := range cveIDs {
993
					cveIDsSetInThisSection[cveID] = true
994
				}
995
			}
996
997
			advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line)
998
			if found {
999
				advisory.AdvisoryID = advisoryID
1000
				continue
1001
			}
1002
1003
			issued, found := o.parseYumUpdateinfoLineToGetIssued(line)
1004
			if found {
1005
				advisory.Issued = issued
1006
				continue
1007
			}
1008
1009
			updated, found := o.parseYumUpdateinfoLineToGetUpdated(line)
1010
			if found {
1011
				advisory.Updated = updated
1012
				continue
1013
			}
1014
		}
1015
	}
1016
	return
1017
}
1018
1019
// state
1020
const (
1021
	Outside = iota
1022
	Header  = iota
1023
	Content = iota
1024
)
1025
1026
func (o *redhatBase) changeSectionState(state int) (newState int) {
1027
	switch state {
1028
	case Outside, Content:
1029
		newState = Header
1030
	case Header:
1031
		newState = Content
1032
	}
1033
	return newState
1034
}
1035
1036
func (o *redhatBase) isCvesHeaderLine(line string) bool {
1037
	return strings.Contains(line, "CVEs : ")
1038
}
1039
1040
var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`)
1041
1042
func (o *redhatBase) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
1043
	return yumCveIDPattern.FindAllString(line, -1)
1044
}
1045
1046
var yumAdvisoryIDPattern = regexp.MustCompile(`^ *Update ID : (.*)$`)
1047
1048
func (o *redhatBase) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
1049
	result := yumAdvisoryIDPattern.FindStringSubmatch(line)
1050
	if len(result) != 2 {
1051
		return "", false
1052
	}
1053
	return strings.TrimSpace(result[1]), true
1054
}
1055
1056
var yumIssuedPattern = regexp.MustCompile(`^\s*Issued : (\d{4}-\d{2}-\d{2})`)
1057
1058
func (o *redhatBase) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
1059
	return o.parseYumUpdateinfoLineToGetDate(line, yumIssuedPattern)
1060
}
1061
1062
var yumUpdatedPattern = regexp.MustCompile(`^\s*Updated : (\d{4}-\d{2}-\d{2})`)
1063
1064
func (o *redhatBase) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
1065
	return o.parseYumUpdateinfoLineToGetDate(line, yumUpdatedPattern)
1066
}
1067
1068
func (o *redhatBase) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *regexp.Regexp) (date time.Time, found bool) {
1069
	result := regexpPattern.FindStringSubmatch(line)
1070
	if len(result) != 2 {
1071
		return date, false
1072
	}
1073
	t, err := time.Parse("2006-01-02", result[1])
1074
	if err != nil {
1075
		return date, false
1076
	}
1077
	return t, true
1078
}
1079
1080
var yumDescriptionPattern = regexp.MustCompile(`^\s*Description : `)
1081
1082
func (o *redhatBase) isDescriptionLine(line string) bool {
1083
	return yumDescriptionPattern.MatchString(line)
1084
}
1085
1086
var yumSeverityPattern = regexp.MustCompile(`^ *Severity : (.*)$`)
1087
1088
func (o *redhatBase) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
1089
	result := yumSeverityPattern.FindStringSubmatch(line)
1090
	if len(result) != 2 {
1091
		return "", false
1092
	}
1093
	return strings.TrimSpace(result[1]), true
1094
}
1095
1096
type advisoryIDPacks struct {
1097
	AdvisoryID string
1098
	PackNames  []string
1099
}
1100
1101
type advisoryIDPacksList []advisoryIDPacks
1102
1103
func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool) {
1104
	for _, a := range list {
1105
		if a.AdvisoryID == advisoryID {
1106
			return a, true
1107
		}
1108
	}
1109
	return advisoryIDPacks{}, false
1110
}
1111
func (o *redhatBase) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
1112
	fields := strings.Split(nameVerRel, ".")
1113
	archTrimed := strings.Join(fields[0:len(fields)-1], ".")
1114
1115
	fields = strings.Split(archTrimed, "-")
1116
	rel = fields[len(fields)-1]
1117
	ver = fields[len(fields)-2]
1118
	name = strings.Join(fields[0:(len(fields)-2)], "-")
1119
	return
1120
}
1121
1122
// parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS, ELSA), packages
1123
func (o *redhatBase) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
1124
	result := []advisoryIDPacks{}
1125
	lines := strings.Split(stdout, "\n")
1126
	for _, line := range lines {
1127
1128
		if !(strings.HasPrefix(line, "RHSA") ||
1129
			strings.HasPrefix(line, "ALAS") ||
1130
			strings.HasPrefix(line, "ELSA")) {
1131
			continue
1132
		}
1133
1134
		fields := strings.Fields(line)
1135
		if len(fields) != 3 {
1136
			return []advisoryIDPacks{}, xerrors.Errorf(
1137
				"Unknown format. line: %s", line)
1138
		}
1139
1140
		// extract fields
1141
		advisoryID := fields[0]
1142
		packVersion := fields[2]
1143
		packName, _, _ := o.extractPackNameVerRel(packVersion)
1144
1145
		found := false
1146
		for i, s := range result {
1147
			if s.AdvisoryID == advisoryID {
1148
				names := s.PackNames
1149
				names = append(names, packName)
1150
				result[i].PackNames = names
1151
				found = true
1152
				break
1153
			}
1154
		}
1155
		if !found {
1156
			result = append(result, advisoryIDPacks{
1157
				AdvisoryID: advisoryID,
1158
				PackNames:  []string{packName},
1159
			})
1160
		}
1161
	}
1162
	return result, nil
1163
}
1164
1165
func (o *redhatBase) yumPS() error {
1166
	cmd := "LANGUAGE=en_US.UTF-8 yum info yum"
1167
	r := o.exec(util.PrependProxyEnv(cmd), noSudo)
1168
	if !r.isSuccess() {
1169
		return xerrors.Errorf("Failed to SSH: %s", r)
1170
	}
1171
	if !o.checkYumPsInstalled(r.Stdout) {
1172
		switch o.Distro.Family {
1173
		case config.RedHat, config.Oracle:
1174
			return nil
1175
		default:
1176
			return xerrors.New("yum-plugin-ps is not installed")
1177
		}
1178
	}
1179
1180
	cmd = "LANGUAGE=en_US.UTF-8 yum -q ps all --color=never"
1181
	r = o.exec(util.PrependProxyEnv(cmd), sudo)
1182
	if !r.isSuccess() {
1183
		return xerrors.Errorf("Failed to SSH: %s", r)
1184
	}
1185
	packs := o.parseYumPS(r.Stdout)
1186
	for name, pack := range packs {
1187
		p := o.Packages[name]
1188
		p.AffectedProcs = pack.AffectedProcs
1189
		o.Packages[name] = p
1190
	}
1191
	return nil
1192
}
1193
1194
func (o *redhatBase) checkYumPsInstalled(stdout string) bool {
1195
	scanner := bufio.NewScanner(strings.NewReader(stdout))
1196
	for scanner.Scan() {
1197
		line := strings.TrimSpace(scanner.Text())
1198
		if strings.HasPrefix(line, "Loaded plugins: ") {
1199
			if strings.Contains(line, " ps,") || strings.HasSuffix(line, " ps") {
1200
				return true
1201
			}
1202
			return false
1203
		}
1204
	}
1205
	return false
1206
}
1207
1208
func (o *redhatBase) parseYumPS(stdout string) models.Packages {
1209
	packs := models.Packages{}
1210
	scanner := bufio.NewScanner(strings.NewReader(stdout))
1211
	isPackageLine, needToParseProcline := false, false
1212
	currentPackName := ""
1213
	for scanner.Scan() {
1214
		line := scanner.Text()
1215
		fields := strings.Fields(line)
1216
		if len(fields) == 0 ||
1217
			len(fields) == 1 && fields[0] == "ps" ||
1218
			len(fields) == 6 && fields[0] == "pid" {
1219
			continue
1220
		}
1221
1222
		isPackageLine = !strings.HasPrefix(line, " ")
1223
		if isPackageLine {
1224
			if 1 < len(fields) && fields[1] == "Upgrade" {
1225
				needToParseProcline = true
1226
1227
				// Search o.Packages to divide into name, version, release
1228
				name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
1229
					var epochNameVerRel string
1230
					if index := strings.Index(p.Version, ":"); 0 < index {
1231
						epoch := p.Version[0:index]
1232
						ver := p.Version[index+1 : len(p.Version)]
1233
						epochNameVerRel = fmt.Sprintf("%s:%s-%s-%s.%s",
1234
							epoch, p.Name, ver, p.Release, p.Arch)
1235
					} else {
1236
						epochNameVerRel = fmt.Sprintf("%s-%s-%s.%s",
1237
							p.Name, p.Version, p.Release, p.Arch)
1238
					}
1239
					return strings.HasPrefix(fields[0], epochNameVerRel)
1240
				})
1241
				if !found {
1242
					o.log.Errorf("`yum ps` package is not found: %s", line)
1243
					continue
1244
				}
1245
				packs[name] = pack
1246
				currentPackName = name
1247
			} else {
1248
				needToParseProcline = false
1249
			}
1250
		} else if needToParseProcline {
1251
			if 6 < len(fields) {
1252
				proc := models.AffectedProcess{
1253
					PID:  fields[0],
1254
					Name: fields[1],
1255
				}
1256
				pack := packs[currentPackName]
1257
				pack.AffectedProcs = append(pack.AffectedProcs, proc)
1258
				packs[currentPackName] = pack
1259
			} else {
1260
				o.log.Errorf("`yum ps` Unknown Format: %s", line)
1261
				continue
1262
			}
1263
		}
1264
	}
1265
	return packs
1266
}
1267
1268
func (o *redhatBase) needsRestarting() error {
1269
	initName, err := o.detectInitSystem()
1270
	if err != nil {
1271
		o.log.Warn(err)
1272
		// continue scanning
1273
	}
1274
1275
	cmd := "LANGUAGE=en_US.UTF-8 needs-restarting"
1276
	r := o.exec(cmd, sudo)
1277
	if !r.isSuccess() {
1278
		return xerrors.Errorf("Failed to SSH: %s", r)
1279
	}
1280
	procs := o.parseNeedsRestarting(r.Stdout)
1281
	for _, proc := range procs {
1282
		fqpn, err := o.procPathToFQPN(proc.Path)
1283
		if err != nil {
1284
			o.log.Warnf("Failed to detect a package name of need restarting process from the command path: %s, %s",
1285
				proc.Path, err)
1286
			continue
1287
		}
1288
		pack, err := o.Packages.FindByFQPN(fqpn)
1289
		if err != nil {
1290
			return err
1291
		}
1292
		if initName == systemd {
1293
			name, err := o.detectServiceName(proc.PID)
1294
			if err != nil {
1295
				o.log.Warn(err)
1296
				// continue scanning
1297
			}
1298
			proc.ServiceName = name
1299
			proc.InitSystem = systemd
1300
		}
1301
		pack.NeedRestartProcs = append(pack.NeedRestartProcs, proc)
1302
		o.Packages[pack.Name] = *pack
1303
	}
1304
	return nil
1305
}
1306
1307
func (o *redhatBase) parseNeedsRestarting(stdout string) (procs []models.NeedRestartProcess) {
1308
	scanner := bufio.NewScanner(strings.NewReader(stdout))
1309
	for scanner.Scan() {
1310
		line := scanner.Text()
1311
		line = strings.Replace(line, "\x00", " ", -1) // for CentOS6.9
1312
		ss := strings.Split(line, " : ")
1313
		if len(ss) < 2 {
1314
			continue
1315
		}
1316
		// https://unix.stackexchange.com/a/419375
1317
		if ss[0] == "1" {
1318
			continue
1319
		}
1320
1321
		path := ss[1]
1322
		if !strings.HasPrefix(path, "/") {
1323
			path = strings.Fields(path)[0]
1324
			// [ec2-user@ip-172-31-11-139 ~]$ sudo needs-restarting
1325
			// 2024 : auditd
1326
			// [ec2-user@ip-172-31-11-139 ~]$ type -p auditd
1327
			// /sbin/auditd
1328
			cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 which %s", path)
1329
			r := o.exec(cmd, sudo)
1330
			if !r.isSuccess() {
1331
				o.log.Warnf("Failed to exec which %s: %s", path, r)
1332
				continue
1333
			}
1334
			path = strings.TrimSpace(r.Stdout)
1335
		}
1336
1337
		procs = append(procs, models.NeedRestartProcess{
1338
			PID:     ss[0],
1339
			Path:    path,
1340
			HasInit: true,
1341
		})
1342
	}
1343
	return
1344
}
1345
1346
// procPathToFQPN returns Fully-Qualified-Package-Name from the command
1347
func (o *redhatBase) procPathToFQPN(execCommand string) (string, error) {
1348
	execCommand = strings.Replace(execCommand, "\x00", " ", -1) // for CentOS6.9
1349
	path := strings.Fields(execCommand)[0]
1350
	cmd := `LANGUAGE=en_US.UTF-8 rpm -qf --queryformat "%{NAME}-%{EPOCH}:%{VERSION}-%{RELEASE}.%{ARCH}\n" ` + path
1351
	r := o.exec(cmd, noSudo)
1352
	if !r.isSuccess() {
1353
		return "", xerrors.Errorf("Failed to SSH: %s", r)
1354
	}
1355
	fqpn := strings.TrimSpace(r.Stdout)
1356
	return strings.Replace(fqpn, "-(none):", "-", -1), nil
1357
}
1358
1359
func (o *redhatBase) hasYumColorOption() bool {
1360
	cmd := "yum --help | grep color"
1361
	r := o.exec(util.PrependProxyEnv(cmd), noSudo)
1362
	return len(r.Stdout) > 0
1363
}
1364