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
![]() |
|||
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: %w", err) |
||
0 ignored issues
–
show
|
|||
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
|
|||
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) scanUpdatablePackages() (models.Packages, error) { |
||
355 | cmd := `repoquery --all --pkgnarrow=updates --qf="%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{REPO}"` |
||
356 | for _, repo := range o.getServerInfo().Enablerepo { |
||
357 | cmd += " --enablerepo=" + repo |
||
358 | } |
||
359 | |||
360 | r := o.exec(util.PrependProxyEnv(cmd), o.sudo.repoquery()) |
||
361 | if !r.isSuccess() { |
||
362 | return nil, xerrors.Errorf("Failed to SSH: %s", r) |
||
363 | } |
||
364 | |||
365 | // Collect Updateble packages, installed, candidate version and repository. |
||
366 | return o.parseUpdatablePacksLines(r.Stdout) |
||
367 | } |
||
368 | |||
369 | // parseUpdatablePacksLines parse the stdout of repoquery to get package name, candidate version |
||
370 | func (o *redhatBase) parseUpdatablePacksLines(stdout string) (models.Packages, error) { |
||
371 | updatable := models.Packages{} |
||
372 | lines := strings.Split(stdout, "\n") |
||
373 | for _, line := range lines { |
||
374 | // TODO remove |
||
375 | // if strings.HasPrefix(line, "Obsoleting") || |
||
376 | // strings.HasPrefix(line, "Security:") { |
||
377 | // // see https://github.com/future-architect/vuls/issues/165 |
||
378 | // continue |
||
379 | // } |
||
380 | if len(strings.TrimSpace(line)) == 0 { |
||
381 | continue |
||
382 | } else if strings.HasPrefix(line, "Loading") { |
||
383 | continue |
||
384 | } |
||
385 | pack, err := o.parseUpdatablePacksLine(line) |
||
386 | if err != nil { |
||
387 | return updatable, err |
||
388 | } |
||
389 | updatable[pack.Name] = pack |
||
390 | } |
||
391 | return updatable, nil |
||
392 | } |
||
393 | |||
394 | func (o *redhatBase) parseUpdatablePacksLine(line string) (models.Package, error) { |
||
395 | fields := strings.Fields(line) |
||
396 | if len(fields) < 5 { |
||
397 | return models.Package{}, xerrors.Errorf("Unknown format: %s, fields: %s", line, fields) |
||
398 | } |
||
399 | |||
400 | ver := "" |
||
401 | epoch := fields[1] |
||
402 | if epoch == "0" { |
||
403 | ver = fields[2] |
||
404 | } else { |
||
405 | ver = fmt.Sprintf("%s:%s", epoch, fields[2]) |
||
406 | } |
||
407 | |||
408 | repos := strings.Join(fields[4:], " ") |
||
409 | |||
410 | p := models.Package{ |
||
411 | Name: fields[0], |
||
412 | NewVersion: ver, |
||
413 | NewRelease: fields[3], |
||
414 | Repository: repos, |
||
415 | } |
||
416 | return p, nil |
||
417 | } |
||
418 | |||
419 | func (o *redhatBase) isExecScanUsingYum() bool { |
||
420 | if o.getServerInfo().Mode.IsOffline() { |
||
421 | return false |
||
422 | } |
||
423 | if o.Distro.Family == config.CentOS { |
||
424 | // CentOS doesn't have security channel |
||
425 | return false |
||
426 | } |
||
427 | if o.getServerInfo().Mode.IsFastRoot() || o.getServerInfo().Mode.IsDeep() { |
||
428 | return true |
||
429 | } |
||
430 | return true |
||
431 | } |
||
432 | |||
433 | func (o *redhatBase) isExecFillChangelogs() bool { |
||
434 | if o.getServerInfo().Mode.IsOffline() { |
||
435 | return false |
||
436 | } |
||
437 | // Amazon linux has no changelos for updates |
||
438 | return o.getServerInfo().Mode.IsDeep() && |
||
439 | o.Distro.Family != config.Amazon |
||
440 | } |
||
441 | |||
442 | func (o *redhatBase) isExecScanChangelogs() bool { |
||
443 | if o.getServerInfo().Mode.IsOffline() || |
||
444 | o.getServerInfo().Mode.IsFast() || |
||
445 | o.getServerInfo().Mode.IsFastRoot() { |
||
446 | return false |
||
447 | } |
||
448 | return true |
||
449 | } |
||
450 | |||
451 | func (o *redhatBase) isExecYumPS() bool { |
||
452 | // RedHat has no yum-ps |
||
453 | switch o.Distro.Family { |
||
454 | case config.RedHat, |
||
455 | config.OpenSUSE, |
||
456 | config.OpenSUSELeap, |
||
457 | config.SUSEEnterpriseServer, |
||
458 | config.SUSEEnterpriseDesktop, |
||
459 | config.SUSEOpenstackCloud: |
||
460 | return false |
||
461 | } |
||
462 | |||
463 | // yum ps needs internet connection |
||
464 | if o.getServerInfo().Mode.IsOffline() || o.getServerInfo().Mode.IsFast() { |
||
465 | return false |
||
466 | } |
||
467 | return true |
||
468 | } |
||
469 | |||
470 | func (o *redhatBase) isExecNeedsRestarting() bool { |
||
471 | switch o.Distro.Family { |
||
472 | case config.OpenSUSE, |
||
473 | config.OpenSUSELeap, |
||
474 | config.SUSEEnterpriseServer, |
||
475 | config.SUSEEnterpriseDesktop, |
||
476 | config.SUSEOpenstackCloud: |
||
477 | // TODO zypper ps |
||
478 | // https://github.com/future-architect/vuls/issues/696 |
||
479 | return false |
||
480 | case config.RedHat, config.CentOS, config.Oracle: |
||
481 | majorVersion, err := o.Distro.MajorVersion() |
||
482 | if err != nil || majorVersion < 6 { |
||
483 | o.log.Errorf("Not implemented yet: %s, err: %s", o.Distro, err) |
||
0 ignored issues
–
show
|
|||
484 | return false |
||
485 | } |
||
486 | |||
487 | if o.getServerInfo().Mode.IsOffline() { |
||
488 | return false |
||
489 | } else if o.getServerInfo().Mode.IsFastRoot() || |
||
490 | o.getServerInfo().Mode.IsDeep() { |
||
491 | return true |
||
492 | } |
||
493 | return false |
||
494 | } |
||
495 | |||
496 | if o.getServerInfo().Mode.IsFast() { |
||
497 | return false |
||
498 | } |
||
499 | return true |
||
500 | } |
||
501 | |||
502 | func (o *redhatBase) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) { |
||
503 | if o.isExecFillChangelogs() { |
||
504 | if err := o.fillChangelogs(updatable); err != nil { |
||
505 | return nil, err |
||
506 | } |
||
507 | } |
||
508 | |||
509 | if o.isExecScanUsingYum() { |
||
510 | return o.scanUsingYum(updatable) |
||
511 | } |
||
512 | |||
513 | // Parse chnagelog because CentOS does not have security channel... |
||
514 | if o.isExecScanChangelogs() { |
||
515 | return o.scanChangelogs(updatable) |
||
516 | } |
||
517 | |||
518 | return models.VulnInfos{}, nil |
||
519 | } |
||
520 | |||
521 | func (o *redhatBase) fillChangelogs(updatables models.Packages) error { |
||
522 | names := []string{} |
||
523 | for name := range updatables { |
||
524 | names = append(names, name) |
||
525 | } |
||
526 | |||
527 | if err := o.fillDiffChangelogs(names); err != nil { |
||
528 | return err |
||
529 | } |
||
530 | |||
531 | emptyChangelogPackNames := []string{} |
||
532 | for _, pack := range o.Packages { |
||
533 | if pack.NewVersion != "" && pack.Changelog.Contents == "" { |
||
534 | emptyChangelogPackNames = append(emptyChangelogPackNames, pack.Name) |
||
535 | } |
||
536 | } |
||
537 | |||
538 | i := 0 |
||
539 | for _, name := range emptyChangelogPackNames { |
||
540 | i++ |
||
541 | o.log.Infof("(%d/%d) Fetched Changelogs %s", i, len(emptyChangelogPackNames), name) |
||
542 | if err := o.fillDiffChangelogs([]string{name}); err != nil { |
||
543 | return err |
||
544 | } |
||
545 | } |
||
546 | |||
547 | return nil |
||
548 | } |
||
549 | |||
550 | func (o *redhatBase) getAvailableChangelogs(packNames []string) (map[string]string, error) { |
||
551 | yumopts := "" |
||
552 | if 0 < len(o.getServerInfo().Enablerepo) { |
||
553 | yumopts = " --enablerepo=" + strings.Join(o.getServerInfo().Enablerepo, ",") |
||
554 | } |
||
555 | if config.Conf.SkipBroken { |
||
556 | yumopts += " --skip-broken" |
||
557 | } |
||
558 | if o.hasYumColorOption() { |
||
559 | yumopts += " --color=never" |
||
560 | } |
||
561 | cmd := `yum changelog all updates %s %s | grep -A 1000000 "==================== Updated Packages ===================="` |
||
562 | cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " ")) |
||
0 ignored issues
–
show
|
|||
563 | |||
564 | r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumChangelog()) |
||
565 | if !r.isSuccess(0, 1) { |
||
566 | return nil, xerrors.Errorf("Failed to SSH: %s", r) |
||
567 | } |
||
568 | |||
569 | return o.divideChangelogsIntoEachPackages(r.Stdout), nil |
||
570 | } |
||
571 | |||
572 | // Divide available change logs of all updatable packages into each package's changelog |
||
573 | func (o *redhatBase) divideChangelogsIntoEachPackages(stdout string) map[string]string { |
||
574 | changelogs := make(map[string]string) |
||
575 | scanner := bufio.NewScanner(strings.NewReader(stdout)) |
||
576 | |||
577 | crlf, newBlock := false, true |
||
578 | packNameVer, contents := "", []string{} |
||
579 | for scanner.Scan() { |
||
580 | line := scanner.Text() |
||
581 | if strings.HasPrefix(line, "==================== Updated Packages ====================") { |
||
582 | continue |
||
583 | } |
||
584 | if len(strings.TrimSpace(line)) != 0 && newBlock { |
||
585 | left := strings.Fields(line)[0] |
||
586 | // ss := strings.Split(left, ".") |
||
587 | // packNameVer = strings.Join(ss[0:len(ss)-1], ".") |
||
588 | packNameVer = left |
||
589 | newBlock = false |
||
590 | continue |
||
591 | } |
||
592 | if len(strings.TrimSpace(line)) == 0 { |
||
593 | if crlf { |
||
594 | changelogs[packNameVer] = strings.Join(contents, "\n") |
||
595 | packNameVer = "" |
||
596 | contents = []string{} |
||
597 | newBlock = true |
||
598 | crlf = false |
||
599 | } else { |
||
600 | contents = append(contents, line) |
||
601 | crlf = true |
||
602 | } |
||
603 | } else { |
||
604 | contents = append(contents, line) |
||
605 | crlf = false |
||
606 | } |
||
607 | } |
||
608 | if 0 < len(contents) { |
||
609 | changelogs[packNameVer] = strings.Join(contents, "\n") |
||
610 | } |
||
611 | return changelogs |
||
612 | } |
||
613 | |||
614 | func (o *redhatBase) fillDiffChangelogs(packNames []string) error { |
||
615 | changelogs, err := o.getAvailableChangelogs(packNames) |
||
616 | if err != nil { |
||
617 | return err |
||
618 | } |
||
619 | |||
620 | for s := range changelogs { |
||
621 | // name, pack, found := o.Packages.FindOne(func(p models.Package) bool { |
||
622 | name, pack, found := o.Packages.FindOne(func(p models.Package) bool { |
||
623 | var epochNameVerRel string |
||
624 | if index := strings.Index(p.NewVersion, ":"); 0 < index { |
||
625 | epoch := p.NewVersion[0:index] |
||
626 | ver := p.NewVersion[index+1 : len(p.NewVersion)] |
||
627 | epochNameVerRel = fmt.Sprintf("%s:%s-%s", epoch, p.Name, ver) |
||
628 | } else { |
||
629 | epochNameVerRel = fmt.Sprintf("%s-%s", p.Name, p.NewVersion) |
||
630 | } |
||
631 | return strings.HasPrefix(s, epochNameVerRel) |
||
632 | }) |
||
633 | |||
634 | if found { |
||
635 | var detectionMethod string |
||
636 | diff, err := o.getDiffChangelog(pack, changelogs[s]) |
||
637 | if err == nil { |
||
638 | detectionMethod = models.ChangelogExactMatchStr |
||
639 | } else { |
||
640 | o.log.Debug(err) |
||
641 | // Try without epoch |
||
642 | if index := strings.Index(pack.Version, ":"); 0 < index { |
||
643 | pack.Version = pack.Version[index+1 : len(pack.Version)] |
||
644 | o.log.Debug("Try without epoch", pack) |
||
645 | diff, err = o.getDiffChangelog(pack, changelogs[s]) |
||
646 | if err != nil { |
||
647 | o.log.Debugf("Failed to find the version in changelog: %s-%s-%s", |
||
648 | pack.Name, pack.Version, pack.Release) |
||
649 | if len(diff) == 0 { |
||
650 | detectionMethod = models.FailedToGetChangelog |
||
651 | } else { |
||
652 | detectionMethod = models.FailedToFindVersionInChangelog |
||
653 | diff = "" |
||
654 | } |
||
655 | } else { |
||
656 | o.log.Debugf("Found the version in changelog without epoch: %s-%s-%s", |
||
657 | pack.Name, pack.Version, pack.Release) |
||
658 | detectionMethod = models.ChangelogLenientMatchStr |
||
659 | } |
||
660 | } else { |
||
661 | if len(diff) == 0 { |
||
662 | detectionMethod = models.FailedToGetChangelog |
||
663 | } else { |
||
664 | detectionMethod = models.FailedToFindVersionInChangelog |
||
665 | diff = "" |
||
666 | } |
||
667 | } |
||
668 | } |
||
669 | |||
670 | pack = o.Packages[name] |
||
671 | pack.Changelog = models.Changelog{ |
||
672 | Contents: diff, |
||
673 | Method: models.DetectionMethod(detectionMethod), |
||
674 | } |
||
675 | o.Packages[name] = pack |
||
676 | } |
||
677 | } |
||
678 | return nil |
||
679 | } |
||
680 | |||
681 | func (o *redhatBase) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) { |
||
682 | installedVer := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release)) |
||
683 | scanner := bufio.NewScanner(strings.NewReader(availableChangelog)) |
||
684 | diff := []string{} |
||
685 | found := false |
||
686 | for scanner.Scan() { |
||
687 | line := scanner.Text() |
||
688 | if !strings.HasPrefix(line, "* ") { |
||
689 | diff = append(diff, line) |
||
690 | continue |
||
691 | } |
||
692 | |||
693 | // openssh on RHEL |
||
694 | // openssh-server-6.6.1p1-35.el7_3.x86_64 rhui-rhel-7-server-rhui-rpms |
||
695 | // Wed Mar 1 21:00:00 2017 Jakub Jelen <[email protected]> - 6.6.1p1-35 + 0.9.3-9 |
||
696 | ss := strings.Split(line, " + ") |
||
697 | if 1 < len(ss) { |
||
698 | line = ss[0] |
||
699 | } |
||
700 | |||
701 | ss = strings.Split(line, " ") |
||
702 | if len(ss) < 2 { |
||
703 | diff = append(diff, line) |
||
704 | continue |
||
705 | } |
||
706 | v := ss[len(ss)-1] |
||
707 | v = strings.TrimPrefix(v, "-") |
||
708 | v = strings.TrimPrefix(v, "[") |
||
709 | v = strings.TrimSuffix(v, "]") |
||
710 | |||
711 | // On Amazon often end with email address. <[email protected]> Go to next line |
||
712 | if strings.HasPrefix(v, "<") && strings.HasSuffix(v, ">") { |
||
713 | diff = append(diff, line) |
||
714 | continue |
||
715 | } |
||
716 | |||
717 | version := ver.NewVersion(v) |
||
718 | if installedVer.Equal(version) || installedVer.GreaterThan(version) { |
||
719 | found = true |
||
720 | break |
||
721 | } |
||
722 | diff = append(diff, line) |
||
723 | } |
||
724 | |||
725 | if len(diff) == 0 || !found { |
||
726 | return availableChangelog, |
||
727 | xerrors.Errorf("Failed to find the version in changelog: %s-%s-%s", |
||
728 | pack.Name, pack.Version, pack.Release) |
||
729 | } |
||
730 | return strings.TrimSpace(strings.Join(diff, "\n")), nil |
||
731 | } |
||
732 | |||
733 | func (o *redhatBase) scanChangelogs(updatable models.Packages) (models.VulnInfos, error) { |
||
734 | packCveIDs := make(map[string][]string) |
||
735 | for name := range updatable { |
||
736 | cveIDs := []string{} |
||
737 | pack := o.Packages[name] |
||
738 | if pack.Changelog.Method == models.FailedToFindVersionInChangelog { |
||
739 | continue |
||
740 | } |
||
741 | scanner := bufio.NewScanner(strings.NewReader(pack.Changelog.Contents)) |
||
742 | for scanner.Scan() { |
||
743 | if matches := cveRe.FindAllString(scanner.Text(), -1); 0 < len(matches) { |
||
744 | for _, m := range matches { |
||
745 | cveIDs = util.AppendIfMissing(cveIDs, m) |
||
746 | } |
||
747 | } |
||
748 | } |
||
749 | packCveIDs[name] = cveIDs |
||
750 | } |
||
751 | |||
752 | // transform datastructure |
||
753 | // - From |
||
754 | // "packname": []{"CVE-2017-1111", ".../ |
||
755 | // |
||
756 | // - To |
||
757 | // map { |
||
758 | // "CVE-2017-1111": "packname", |
||
759 | // } |
||
760 | vinfos := models.VulnInfos{} |
||
761 | for name, cveIDs := range packCveIDs { |
||
762 | for _, cid := range cveIDs { |
||
763 | if v, ok := vinfos[cid]; ok { |
||
764 | v.AffectedPackages = append(v.AffectedPackages, models.PackageFixStatus{Name: name}) |
||
765 | vinfos[cid] = v |
||
766 | } else { |
||
767 | vinfos[cid] = models.VulnInfo{ |
||
768 | CveID: cid, |
||
769 | AffectedPackages: models.PackageFixStatuses{{Name: name}}, |
||
770 | Confidences: models.Confidences{models.ChangelogExactMatch}, |
||
771 | } |
||
772 | } |
||
773 | } |
||
774 | } |
||
775 | return vinfos, nil |
||
776 | } |
||
777 | |||
778 | type distroAdvisoryCveIDs struct { |
||
779 | DistroAdvisory models.DistroAdvisory |
||
780 | CveIDs []string |
||
781 | } |
||
782 | |||
783 | // Scaning unsecure packages using yum-plugin-security. |
||
784 | // Amazon, RHEL, Oracle Linux |
||
785 | func (o *redhatBase) scanUsingYum(updatable models.Packages) (models.VulnInfos, error) { |
||
786 | if o.Distro.Family == config.CentOS { |
||
787 | // CentOS has no security channel. |
||
788 | return nil, xerrors.New( |
||
789 | "yum updateinfo is not suppported on CentOS") |
||
790 | } |
||
791 | |||
792 | // get advisoryID(RHSA, ALAS, ELSA) - package name,version |
||
793 | major, err := (o.Distro.MajorVersion()) |
||
794 | if err != nil { |
||
795 | return nil, xerrors.Errorf("Not implemented yet: %s, err: %w", o.Distro, err) |
||
0 ignored issues
–
show
|
|||
796 | } |
||
797 | |||
798 | var cmd string |
||
799 | if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major > 5 { |
||
800 | cmd = "yum repolist --color=never" |
||
801 | r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumRepolist()) |
||
802 | if !r.isSuccess() { |
||
803 | return nil, xerrors.Errorf("Failed to SSH: %s", r) |
||
804 | } |
||
805 | } |
||
806 | |||
807 | if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 { |
||
808 | cmd = "yum list-security --security" |
||
809 | if o.hasYumColorOption() { |
||
810 | cmd += " --color=never" |
||
811 | } |
||
812 | } else { |
||
813 | cmd = "yum updateinfo list updates --security --color=never" |
||
814 | } |
||
815 | r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo()) |
||
816 | if !r.isSuccess() { |
||
817 | return nil, xerrors.Errorf("Failed to SSH: %s", r) |
||
818 | } |
||
819 | advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout) |
||
820 | |||
821 | dict := make(map[string]models.Packages) |
||
822 | for _, advIDPackNames := range advIDPackNamesList { |
||
823 | packages := models.Packages{} |
||
824 | for _, packName := range advIDPackNames.PackNames { |
||
825 | pack, found := updatable[packName] |
||
826 | if !found { |
||
827 | return nil, xerrors.Errorf( |
||
828 | "Package not found. pack: %#v", packName) |
||
829 | } |
||
830 | packages[pack.Name] = pack |
||
831 | continue |
||
832 | } |
||
833 | dict[advIDPackNames.AdvisoryID] = packages |
||
834 | } |
||
835 | |||
836 | // get advisoryID(RHSA, ALAS, ELSA) - CVE IDs |
||
837 | if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 { |
||
838 | cmd = "yum info-security" |
||
839 | if o.hasYumColorOption() { |
||
840 | cmd += " --color=never" |
||
841 | } |
||
842 | } else { |
||
843 | cmd = "yum updateinfo updates --security --color=never" |
||
844 | } |
||
845 | r = o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo()) |
||
846 | if !r.isSuccess() { |
||
847 | return nil, xerrors.Errorf("Failed to SSH: %s", r) |
||
848 | } |
||
849 | advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout) |
||
850 | if err != nil { |
||
851 | return nil, err |
||
852 | } |
||
853 | |||
854 | // All information collected. |
||
855 | // Convert to VulnInfos. |
||
856 | vinfos := models.VulnInfos{} |
||
857 | for _, advIDCveIDs := range advisoryCveIDsList { |
||
858 | for _, cveID := range advIDCveIDs.CveIDs { |
||
859 | vinfo, found := vinfos[cveID] |
||
860 | if found { |
||
861 | advAppended := append(vinfo.DistroAdvisories, advIDCveIDs.DistroAdvisory) |
||
862 | vinfo.DistroAdvisories = advAppended |
||
863 | |||
864 | packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] |
||
865 | for _, pack := range packs { |
||
866 | vinfo.AffectedPackages = append(vinfo.AffectedPackages, |
||
867 | models.PackageFixStatus{Name: pack.Name}) |
||
868 | } |
||
869 | } else { |
||
870 | packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] |
||
871 | affected := models.PackageFixStatuses{} |
||
872 | for _, p := range packs { |
||
873 | affected = append(affected, models.PackageFixStatus{Name: p.Name}) |
||
874 | } |
||
875 | vinfo = models.VulnInfo{ |
||
876 | CveID: cveID, |
||
877 | DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory}, |
||
878 | AffectedPackages: affected, |
||
879 | Confidences: models.Confidences{models.YumUpdateSecurityMatch}, |
||
880 | } |
||
881 | } |
||
882 | vinfos[cveID] = vinfo |
||
883 | } |
||
884 | } |
||
885 | return vinfos, nil |
||
886 | } |
||
887 | |||
888 | var horizontalRulePattern = regexp.MustCompile(`^=+$`) |
||
889 | |||
890 | func (o *redhatBase) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) { |
||
891 | sectionState := Outside |
||
892 | lines := strings.Split(stdout, "\n") |
||
893 | lines = append(lines, "=============") |
||
894 | |||
895 | // Amazon Linux AMI Security Information |
||
896 | advisory := models.DistroAdvisory{} |
||
897 | |||
898 | cveIDsSetInThisSection := make(map[string]bool) |
||
899 | |||
900 | // use this flag to Collect CVE IDs in CVEs field. |
||
901 | inDesctiption, inCves := false, false |
||
902 | |||
903 | for _, line := range lines { |
||
904 | line = strings.TrimSpace(line) |
||
905 | |||
906 | // find the new section pattern |
||
907 | if horizontalRulePattern.MatchString(line) { |
||
908 | // set previous section's result to return-variable |
||
909 | if sectionState == Content { |
||
910 | foundCveIDs := []string{} |
||
911 | for cveID := range cveIDsSetInThisSection { |
||
912 | foundCveIDs = append(foundCveIDs, cveID) |
||
913 | } |
||
914 | result = append(result, distroAdvisoryCveIDs{ |
||
915 | DistroAdvisory: advisory, |
||
916 | CveIDs: foundCveIDs, |
||
917 | }) |
||
918 | |||
919 | // reset for next section. |
||
920 | cveIDsSetInThisSection = make(map[string]bool) |
||
921 | inDesctiption, inCves = false, false |
||
922 | advisory = models.DistroAdvisory{} |
||
923 | } |
||
924 | |||
925 | // Go to next section |
||
926 | sectionState = o.changeSectionState(sectionState) |
||
927 | continue |
||
928 | } |
||
929 | |||
930 | switch sectionState { |
||
931 | case Header: |
||
932 | switch o.Distro.Family { |
||
933 | case config.CentOS: |
||
934 | // CentOS has no security channel. |
||
935 | return result, xerrors.New( |
||
936 | "yum updateinfo is not suppported on CentOS") |
||
937 | case config.RedHat, config.Amazon, config.Oracle: |
||
938 | // nop |
||
939 | } |
||
940 | |||
941 | case Content: |
||
942 | if found := o.isDescriptionLine(line); found { |
||
943 | inDesctiption, inCves = true, false |
||
944 | ss := strings.Split(line, " : ") |
||
945 | advisory.Description += fmt.Sprintf("%s\n", |
||
946 | strings.Join(ss[1:], " : ")) |
||
947 | continue |
||
948 | } |
||
949 | |||
950 | // severity |
||
951 | if severity, found := o.parseYumUpdateinfoToGetSeverity(line); found { |
||
952 | advisory.Severity = severity |
||
953 | continue |
||
954 | } |
||
955 | |||
956 | // No need to parse in description except severity |
||
957 | if inDesctiption { |
||
958 | if ss := strings.Split(line, ": "); 1 < len(ss) { |
||
959 | advisory.Description += fmt.Sprintf("%s\n", |
||
960 | strings.Join(ss[1:], ": ")) |
||
961 | } |
||
962 | continue |
||
963 | } |
||
964 | |||
965 | if found := o.isCvesHeaderLine(line); found { |
||
966 | inCves = true |
||
967 | ss := strings.Split(line, "CVEs : ") |
||
968 | line = strings.Join(ss[1:], " ") |
||
969 | cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) |
||
970 | for _, cveID := range cveIDs { |
||
971 | cveIDsSetInThisSection[cveID] = true |
||
972 | } |
||
973 | continue |
||
974 | } |
||
975 | |||
976 | if inCves { |
||
977 | cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line) |
||
978 | for _, cveID := range cveIDs { |
||
979 | cveIDsSetInThisSection[cveID] = true |
||
980 | } |
||
981 | } |
||
982 | |||
983 | advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line) |
||
984 | if found { |
||
985 | advisory.AdvisoryID = advisoryID |
||
986 | continue |
||
987 | } |
||
988 | |||
989 | issued, found := o.parseYumUpdateinfoLineToGetIssued(line) |
||
990 | if found { |
||
991 | advisory.Issued = issued |
||
992 | continue |
||
993 | } |
||
994 | |||
995 | updated, found := o.parseYumUpdateinfoLineToGetUpdated(line) |
||
996 | if found { |
||
997 | advisory.Updated = updated |
||
998 | continue |
||
999 | } |
||
1000 | } |
||
1001 | } |
||
1002 | return |
||
1003 | } |
||
1004 | |||
1005 | // state |
||
1006 | const ( |
||
1007 | Outside = iota |
||
1008 | Header = iota |
||
1009 | Content = iota |
||
1010 | ) |
||
1011 | |||
1012 | func (o *redhatBase) changeSectionState(state int) (newState int) { |
||
1013 | switch state { |
||
1014 | case Outside, Content: |
||
1015 | newState = Header |
||
1016 | case Header: |
||
1017 | newState = Content |
||
1018 | } |
||
1019 | return newState |
||
1020 | } |
||
1021 | |||
1022 | func (o *redhatBase) isCvesHeaderLine(line string) bool { |
||
1023 | return strings.Contains(line, "CVEs : ") |
||
1024 | } |
||
1025 | |||
1026 | var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) |
||
1027 | |||
1028 | func (o *redhatBase) parseYumUpdateinfoLineToGetCveIDs(line string) []string { |
||
1029 | return yumCveIDPattern.FindAllString(line, -1) |
||
1030 | } |
||
1031 | |||
1032 | var yumAdvisoryIDPattern = regexp.MustCompile(`^ *Update ID : (.*)$`) |
||
1033 | |||
1034 | func (o *redhatBase) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) { |
||
1035 | result := yumAdvisoryIDPattern.FindStringSubmatch(line) |
||
1036 | if len(result) != 2 { |
||
1037 | return "", false |
||
1038 | } |
||
1039 | return strings.TrimSpace(result[1]), true |
||
1040 | } |
||
1041 | |||
1042 | var yumIssuedPattern = regexp.MustCompile(`^\s*Issued : (\d{4}-\d{2}-\d{2})`) |
||
1043 | |||
1044 | func (o *redhatBase) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) { |
||
1045 | return o.parseYumUpdateinfoLineToGetDate(line, yumIssuedPattern) |
||
1046 | } |
||
1047 | |||
1048 | var yumUpdatedPattern = regexp.MustCompile(`^\s*Updated : (\d{4}-\d{2}-\d{2})`) |
||
1049 | |||
1050 | func (o *redhatBase) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) { |
||
1051 | return o.parseYumUpdateinfoLineToGetDate(line, yumUpdatedPattern) |
||
1052 | } |
||
1053 | |||
1054 | func (o *redhatBase) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *regexp.Regexp) (date time.Time, found bool) { |
||
1055 | result := regexpPattern.FindStringSubmatch(line) |
||
1056 | if len(result) != 2 { |
||
1057 | return date, false |
||
1058 | } |
||
1059 | t, err := time.Parse("2006-01-02", result[1]) |
||
1060 | if err != nil { |
||
1061 | return date, false |
||
1062 | } |
||
1063 | return t, true |
||
1064 | } |
||
1065 | |||
1066 | var yumDescriptionPattern = regexp.MustCompile(`^\s*Description : `) |
||
1067 | |||
1068 | func (o *redhatBase) isDescriptionLine(line string) bool { |
||
1069 | return yumDescriptionPattern.MatchString(line) |
||
1070 | } |
||
1071 | |||
1072 | var yumSeverityPattern = regexp.MustCompile(`^ *Severity : (.*)$`) |
||
1073 | |||
1074 | func (o *redhatBase) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) { |
||
1075 | result := yumSeverityPattern.FindStringSubmatch(line) |
||
1076 | if len(result) != 2 { |
||
1077 | return "", false |
||
1078 | } |
||
1079 | return strings.TrimSpace(result[1]), true |
||
1080 | } |
||
1081 | |||
1082 | type advisoryIDPacks struct { |
||
1083 | AdvisoryID string |
||
1084 | PackNames []string |
||
1085 | } |
||
1086 | |||
1087 | type advisoryIDPacksList []advisoryIDPacks |
||
1088 | |||
1089 | func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool) { |
||
1090 | for _, a := range list { |
||
1091 | if a.AdvisoryID == advisoryID { |
||
1092 | return a, true |
||
1093 | } |
||
1094 | } |
||
1095 | return advisoryIDPacks{}, false |
||
1096 | } |
||
1097 | func (o *redhatBase) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) { |
||
1098 | fields := strings.Split(nameVerRel, ".") |
||
1099 | archTrimed := strings.Join(fields[0:len(fields)-1], ".") |
||
1100 | |||
1101 | fields = strings.Split(archTrimed, "-") |
||
1102 | rel = fields[len(fields)-1] |
||
1103 | ver = fields[len(fields)-2] |
||
1104 | name = strings.Join(fields[0:(len(fields)-2)], "-") |
||
1105 | return |
||
1106 | } |
||
1107 | |||
1108 | // parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS, ELSA), packages |
||
1109 | func (o *redhatBase) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) { |
||
1110 | result := []advisoryIDPacks{} |
||
1111 | lines := strings.Split(stdout, "\n") |
||
1112 | for _, line := range lines { |
||
1113 | |||
1114 | if !(strings.HasPrefix(line, "RHSA") || |
||
1115 | strings.HasPrefix(line, "ALAS") || |
||
1116 | strings.HasPrefix(line, "ELSA")) { |
||
1117 | continue |
||
1118 | } |
||
1119 | |||
1120 | fields := strings.Fields(line) |
||
1121 | if len(fields) != 3 { |
||
1122 | return []advisoryIDPacks{}, xerrors.Errorf( |
||
1123 | "Unknown format. line: %s", line) |
||
1124 | } |
||
1125 | |||
1126 | // extract fields |
||
1127 | advisoryID := fields[0] |
||
1128 | packVersion := fields[2] |
||
1129 | packName, _, _ := o.extractPackNameVerRel(packVersion) |
||
1130 | |||
1131 | found := false |
||
1132 | for i, s := range result { |
||
1133 | if s.AdvisoryID == advisoryID { |
||
1134 | names := s.PackNames |
||
1135 | names = append(names, packName) |
||
1136 | result[i].PackNames = names |
||
1137 | found = true |
||
1138 | break |
||
1139 | } |
||
1140 | } |
||
1141 | if !found { |
||
1142 | result = append(result, advisoryIDPacks{ |
||
1143 | AdvisoryID: advisoryID, |
||
1144 | PackNames: []string{packName}, |
||
1145 | }) |
||
1146 | } |
||
1147 | } |
||
1148 | return result, nil |
||
1149 | } |
||
1150 | |||
1151 | func (o *redhatBase) yumPS() error { |
||
1152 | cmd := "LANGUAGE=en_US.UTF-8 yum info yum" |
||
1153 | r := o.exec(util.PrependProxyEnv(cmd), noSudo) |
||
1154 | if !r.isSuccess() { |
||
1155 | return xerrors.Errorf("Failed to SSH: %s", r) |
||
1156 | } |
||
1157 | if !o.checkYumPsInstalled(r.Stdout) { |
||
1158 | switch o.Distro.Family { |
||
1159 | case config.RedHat, config.Oracle: |
||
1160 | return nil |
||
1161 | default: |
||
1162 | return xerrors.New("yum-plugin-ps is not installed") |
||
1163 | } |
||
1164 | } |
||
1165 | |||
1166 | cmd = "LANGUAGE=en_US.UTF-8 yum -q ps all --color=never" |
||
1167 | r = o.exec(util.PrependProxyEnv(cmd), sudo) |
||
1168 | if !r.isSuccess() { |
||
1169 | return xerrors.Errorf("Failed to SSH: %s", r) |
||
1170 | } |
||
1171 | packs := o.parseYumPS(r.Stdout) |
||
1172 | for name, pack := range packs { |
||
1173 | p := o.Packages[name] |
||
1174 | p.AffectedProcs = pack.AffectedProcs |
||
1175 | o.Packages[name] = p |
||
1176 | } |
||
1177 | return nil |
||
1178 | } |
||
1179 | |||
1180 | func (o *redhatBase) checkYumPsInstalled(stdout string) bool { |
||
1181 | scanner := bufio.NewScanner(strings.NewReader(stdout)) |
||
1182 | for scanner.Scan() { |
||
1183 | line := strings.TrimSpace(scanner.Text()) |
||
1184 | if strings.HasPrefix(line, "Loaded plugins: ") { |
||
1185 | if strings.Contains(line, " ps,") || strings.HasSuffix(line, " ps") { |
||
1186 | return true |
||
1187 | } |
||
1188 | return false |
||
1189 | } |
||
1190 | } |
||
1191 | return false |
||
1192 | } |
||
1193 | |||
1194 | func (o *redhatBase) parseYumPS(stdout string) models.Packages { |
||
1195 | packs := models.Packages{} |
||
1196 | scanner := bufio.NewScanner(strings.NewReader(stdout)) |
||
1197 | isPackageLine, needToParseProcline := false, false |
||
1198 | currentPackName := "" |
||
1199 | for scanner.Scan() { |
||
1200 | line := scanner.Text() |
||
1201 | fields := strings.Fields(line) |
||
1202 | if len(fields) == 0 || |
||
1203 | len(fields) == 1 && fields[0] == "ps" || |
||
1204 | len(fields) == 6 && fields[0] == "pid" { |
||
1205 | continue |
||
1206 | } |
||
1207 | |||
1208 | isPackageLine = !strings.HasPrefix(line, " ") |
||
1209 | if isPackageLine { |
||
1210 | if 1 < len(fields) && fields[1] == "Upgrade" { |
||
1211 | needToParseProcline = true |
||
1212 | |||
1213 | // Search o.Packages to divide into name, version, release |
||
1214 | name, pack, found := o.Packages.FindOne(func(p models.Package) bool { |
||
1215 | var epochNameVerRel string |
||
1216 | if index := strings.Index(p.Version, ":"); 0 < index { |
||
1217 | epoch := p.Version[0:index] |
||
1218 | ver := p.Version[index+1 : len(p.Version)] |
||
1219 | epochNameVerRel = fmt.Sprintf("%s:%s-%s-%s.%s", |
||
1220 | epoch, p.Name, ver, p.Release, p.Arch) |
||
1221 | } else { |
||
1222 | epochNameVerRel = fmt.Sprintf("%s-%s-%s.%s", |
||
1223 | p.Name, p.Version, p.Release, p.Arch) |
||
1224 | } |
||
1225 | return strings.HasPrefix(fields[0], epochNameVerRel) |
||
1226 | }) |
||
1227 | if !found { |
||
1228 | o.log.Errorf("`yum ps` package is not found: %s", line) |
||
1229 | continue |
||
1230 | } |
||
1231 | packs[name] = pack |
||
1232 | currentPackName = name |
||
1233 | } else { |
||
1234 | needToParseProcline = false |
||
1235 | } |
||
1236 | } else if needToParseProcline { |
||
1237 | if 6 < len(fields) { |
||
1238 | proc := models.AffectedProcess{ |
||
1239 | PID: fields[0], |
||
1240 | Name: fields[1], |
||
1241 | } |
||
1242 | pack := packs[currentPackName] |
||
1243 | pack.AffectedProcs = append(pack.AffectedProcs, proc) |
||
1244 | packs[currentPackName] = pack |
||
1245 | } else { |
||
1246 | o.log.Errorf("`yum ps` Unknown Format: %s", line) |
||
1247 | continue |
||
1248 | } |
||
1249 | } |
||
1250 | } |
||
1251 | return packs |
||
1252 | } |
||
1253 | |||
1254 | func (o *redhatBase) needsRestarting() error { |
||
1255 | initName, err := o.detectInitSystem() |
||
1256 | if err != nil { |
||
1257 | o.log.Warn(err) |
||
1258 | // continue scanning |
||
1259 | } |
||
1260 | |||
1261 | cmd := "LANGUAGE=en_US.UTF-8 needs-restarting" |
||
1262 | r := o.exec(cmd, sudo) |
||
1263 | if !r.isSuccess() { |
||
1264 | return xerrors.Errorf("Failed to SSH: %s", r) |
||
1265 | } |
||
1266 | procs := o.parseNeedsRestarting(r.Stdout) |
||
1267 | for _, proc := range procs { |
||
1268 | fqpn, err := o.procPathToFQPN(proc.Path) |
||
1269 | if err != nil { |
||
1270 | o.log.Warnf("Failed to detect a package name of need restarting process from the command path: %s, %s", |
||
1271 | proc.Path, err) |
||
1272 | continue |
||
1273 | } |
||
1274 | pack, err := o.Packages.FindByFQPN(fqpn) |
||
1275 | if err != nil { |
||
1276 | return err |
||
1277 | } |
||
1278 | if initName == systemd { |
||
1279 | name, err := o.detectServiceName(proc.PID) |
||
1280 | if err != nil { |
||
1281 | o.log.Warn(err) |
||
1282 | // continue scanning |
||
1283 | } |
||
1284 | proc.ServiceName = name |
||
1285 | proc.InitSystem = systemd |
||
1286 | } |
||
1287 | pack.NeedRestartProcs = append(pack.NeedRestartProcs, proc) |
||
1288 | o.Packages[pack.Name] = *pack |
||
1289 | } |
||
1290 | return nil |
||
1291 | } |
||
1292 | |||
1293 | func (o *redhatBase) parseNeedsRestarting(stdout string) (procs []models.NeedRestartProcess) { |
||
1294 | scanner := bufio.NewScanner(strings.NewReader(stdout)) |
||
1295 | for scanner.Scan() { |
||
1296 | line := scanner.Text() |
||
1297 | line = strings.Replace(line, "\x00", " ", -1) // for CentOS6.9 |
||
1298 | ss := strings.Split(line, " : ") |
||
1299 | if len(ss) < 2 { |
||
1300 | continue |
||
1301 | } |
||
1302 | // https://unix.stackexchange.com/a/419375 |
||
1303 | if ss[0] == "1" { |
||
1304 | continue |
||
1305 | } |
||
1306 | |||
1307 | path := ss[1] |
||
1308 | if !strings.HasPrefix(path, "/") { |
||
1309 | path = strings.Fields(path)[0] |
||
1310 | // [ec2-user@ip-172-31-11-139 ~]$ sudo needs-restarting |
||
1311 | // 2024 : auditd |
||
1312 | // [ec2-user@ip-172-31-11-139 ~]$ type -p auditd |
||
1313 | // /sbin/auditd |
||
1314 | cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 which %s", path) |
||
1315 | r := o.exec(cmd, sudo) |
||
1316 | if !r.isSuccess() { |
||
1317 | o.log.Warnf("Failed to exec which %s: %s", path, r) |
||
1318 | continue |
||
1319 | } |
||
1320 | path = strings.TrimSpace(r.Stdout) |
||
1321 | } |
||
1322 | |||
1323 | procs = append(procs, models.NeedRestartProcess{ |
||
1324 | PID: ss[0], |
||
1325 | Path: path, |
||
1326 | HasInit: true, |
||
1327 | }) |
||
1328 | } |
||
1329 | return |
||
1330 | } |
||
1331 | |||
1332 | // procPathToFQPN returns Fully-Qualified-Package-Name from the command |
||
1333 | func (o *redhatBase) procPathToFQPN(execCommand string) (string, error) { |
||
1334 | execCommand = strings.Replace(execCommand, "\x00", " ", -1) // for CentOS6.9 |
||
1335 | path := strings.Fields(execCommand)[0] |
||
1336 | cmd := `LANGUAGE=en_US.UTF-8 rpm -qf --queryformat "%{NAME}-%{EPOCH}:%{VERSION}-%{RELEASE}.%{ARCH}\n" ` + path |
||
1337 | r := o.exec(cmd, noSudo) |
||
1338 | if !r.isSuccess() { |
||
1339 | return "", xerrors.Errorf("Failed to SSH: %s", r) |
||
1340 | } |
||
1341 | fqpn := strings.TrimSpace(r.Stdout) |
||
1342 | return strings.Replace(fqpn, "-(none):", "-", -1), nil |
||
1343 | } |
||
1344 | |||
1345 | func (o *redhatBase) hasYumColorOption() bool { |
||
1346 | cmd := "yum --help | grep color" |
||
1347 | r := o.exec(util.PrependProxyEnv(cmd), noSudo) |
||
1348 | return len(r.Stdout) > 0 |
||
1349 | } |
||
1350 |