diff --git a/GNUmakefile b/GNUmakefile index a0099e6a..19f5b36e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,7 +25,7 @@ GO_OFF := GO111MODULE=off go all: build -build: main.go pretest +build: main.go pretest fmt $(GO) build -a -ldflags "$(LDFLAGS)" -o vuls $< b: main.go pretest diff --git a/models/packages.go b/models/packages.go index 23b99198..de5e5582 100644 --- a/models/packages.go +++ b/models/packages.go @@ -185,8 +185,9 @@ type Changelog struct { // AffectedProcess keep a processes information affected by software update type AffectedProcess struct { - PID string `json:"pid"` - Name string `json:"name"` + PID string `json:"pid,omitempty"` + Name string `json:"name,omitempty"` + ListenPorts []string `json:"listenPorts,omitempty"` } // NeedRestartProcess keep a processes information affected by software update diff --git a/report/tui.go b/report/tui.go index e21892d1..827e9447 100644 --- a/report/tui.go +++ b/report/tui.go @@ -733,7 +733,8 @@ func setChangelogLayout(g *gocui.Gui) error { if len(pack.AffectedProcs) != 0 { for _, p := range pack.AffectedProcs { - lines = append(lines, fmt.Sprintf(" * PID: %s %s", p.PID, p.Name)) + lines = append(lines, fmt.Sprintf(" * PID: %s %s Port: %s", + p.PID, p.Name, p.ListenPorts)) } } else { // lines = append(lines, fmt.Sprintf(" * No affected process")) diff --git a/report/util.go b/report/util.go index e02265ae..a8e200f2 100644 --- a/report/util.go +++ b/report/util.go @@ -262,7 +262,7 @@ No CVE-IDs are found in updatable packages. if len(pack.AffectedProcs) != 0 { for _, p := range pack.AffectedProcs { data = append(data, []string{"", - fmt.Sprintf(" - PID: %s %s", p.PID, p.Name)}) + fmt.Sprintf(" - PID: %s %s, Port: %s", p.PID, p.Name, p.ListenPorts)}) } } } diff --git a/scan/amazon.go b/scan/amazon.go index 75fbce58..d9536cf8 100644 --- a/scan/amazon.go +++ b/scan/amazon.go @@ -85,6 +85,7 @@ func (o *amazon) sudoNoPasswdCmdsFastRoot() []cmd { {"stat /proc/1/exe", exitStatusZero}, {"ls -l /proc/1/exe", exitStatusZero}, {"cat /proc/1/maps", exitStatusZero}, + {"lsof -i -P", exitStatusZero}, } } diff --git a/scan/base.go b/scan/base.go index ae1a55e1..0f790b4e 100644 --- a/scan/base.go +++ b/scan/base.go @@ -781,3 +781,30 @@ func (l *base) parseGrepProcMap(stdout string) (soPaths []string) { } return soPaths } + +func (l *base) lsOfListen() (stdout string, err error) { + cmd := `lsof -i -P | grep LISTEN` + r := l.exec(util.PrependProxyEnv(cmd), sudo) + if !r.isSuccess() { + return "", xerrors.Errorf("Failed to SSH: %s", r) + } + return r.Stdout, nil +} + +func (l *base) parseLsOf(stdout string) map[string]string { + portPid := map[string]string{} + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + ss := strings.Fields(scanner.Text()) + if len(ss) < 10 { + continue + } + pid, ipPort := ss[1], ss[8] + sss := strings.Split(ipPort, ":") + if len(sss) != 2 { + continue + } + portPid[sss[1]] = pid + } + return portPid +} diff --git a/scan/base_test.go b/scan/base_test.go index 1ce90da5..d9219145 100644 --- a/scan/base_test.go +++ b/scan/base_test.go @@ -252,3 +252,43 @@ func Test_base_parseGrepProcMap(t *testing.T) { }) } } + +func Test_base_parseLsOf(t *testing.T) { + type args struct { + stdout string + } + tests := []struct { + name string + args args + wantPortPid map[string]string + }{ + { + name: "lsof", + args: args{ + stdout: `systemd-r 474 systemd-resolve 13u IPv4 11904 0t0 TCP localhost:53 (LISTEN) +sshd 644 root 3u IPv4 16714 0t0 TCP *:22 (LISTEN) +sshd 644 root 4u IPv6 16716 0t0 TCP *:22 (LISTEN) +squid 959 proxy 11u IPv6 16351 0t0 TCP *:3128 (LISTEN) +node 1498 ubuntu 21u IPv6 20132 0t0 TCP *:35401 (LISTEN) +node 1498 ubuntu 22u IPv6 20133 0t0 TCP *:44801 (LISTEN) +docker-pr 9135 root 4u IPv6 297133 0t0 TCP *:6379 (LISTEN)`, + }, + wantPortPid: map[string]string{ + "53": "474", + "22": "644", + "3128": "959", + "35401": "1498", + "44801": "1498", + "6379": "9135", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &base{} + if gotPortPid := l.parseLsOf(tt.args.stdout); !reflect.DeepEqual(gotPortPid, tt.wantPortPid) { + t.Errorf("base.parseLsOf() = %v, want %v", gotPortPid, tt.wantPortPid) + } + }) + } +} diff --git a/scan/centos.go b/scan/centos.go index dcb946fd..ccc2db8c 100644 --- a/scan/centos.go +++ b/scan/centos.go @@ -96,6 +96,7 @@ func (o *centos) sudoNoPasswdCmdsFastRoot() []cmd { {"stat /proc/1/exe", exitStatusZero}, {"ls -l /proc/1/exe", exitStatusZero}, {"cat /proc/1/maps", exitStatusZero}, + {"lsof -i -P", exitStatusZero}, } } return []cmd{ diff --git a/scan/debian.go b/scan/debian.go index cf76e248..8207c327 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -151,6 +151,7 @@ func (o *debian) checkIfSudoNoPasswd() error { "stat /proc/1/exe", "ls -l /proc/1/exe", "cat /proc/1/maps", + "lsof -i -P", } if !o.getServerInfo().Mode.IsOffline() { @@ -1152,6 +1153,16 @@ func (o *debian) dpkgPs() error { pidLoadedFiles[pid] = append(pidLoadedFiles[pid], ss...) } + pidListenPorts := map[string][]string{} + stdout, err = o.lsOfListen() + if err != nil { + return xerrors.Errorf("Failed to ls of: %w", err) + } + portPid := o.parseLsOf(stdout) + for port, pid := range portPid { + pidListenPorts[pid] = append(pidListenPorts[pid], port) + } + for pid, loadedFiles := range pidLoadedFiles { o.log.Debugf("dpkg -S %#v", loadedFiles) pkgNames, err := o.getPkgName(loadedFiles) @@ -1165,8 +1176,9 @@ func (o *debian) dpkgPs() error { procName = pidNames[pid] } proc := models.AffectedProcess{ - PID: pid, - Name: procName, + PID: pid, + Name: procName, + ListenPorts: pidListenPorts[pid], } for _, n := range pkgNames { diff --git a/scan/redhatbase.go b/scan/redhatbase.go index a6b80e0e..44e1e96a 100644 --- a/scan/redhatbase.go +++ b/scan/redhatbase.go @@ -278,7 +278,7 @@ func (o *redhatBase) scanInstalledPackages() (models.Packages, error) { Version: version, } - r := o.exec(rpmQa(o.Distro), noSudo) + r := o.exec(o.rpmQa(o.Distro), noSudo) if !r.isSuccess() { return nil, xerrors.Errorf("Scan packages failed: %s", r) } @@ -482,9 +482,9 @@ func (o *redhatBase) yumPs() error { if err != nil { return xerrors.Errorf("Failed to yum ps: %w", err) } + pidNames := o.parsePs(stdout) pidLoadedFiles := map[string][]string{} - // for pid, name := range pidNames { for pid := range pidNames { stdout := "" stdout, err = o.lsProcExe(pid) @@ -508,6 +508,16 @@ func (o *redhatBase) yumPs() error { pidLoadedFiles[pid] = append(pidLoadedFiles[pid], ss...) } + pidListenPorts := map[string][]string{} + stdout, err = o.lsOfListen() + if err != nil { + return xerrors.Errorf("Failed to ls of: %w", err) + } + portPid := o.parseLsOf(stdout) + for port, pid := range portPid { + pidListenPorts[pid] = append(pidListenPorts[pid], port) + } + for pid, loadedFiles := range pidLoadedFiles { o.log.Debugf("rpm -qf: %#v", loadedFiles) pkgNames, err := o.getPkgName(loadedFiles) @@ -526,8 +536,9 @@ func (o *redhatBase) yumPs() error { procName = pidNames[pid] } proc := models.AffectedProcess{ - PID: pid, - Name: procName, + PID: pid, + Name: procName, + ListenPorts: pidListenPorts[pid], } for fqpn := range uniq { @@ -634,7 +645,7 @@ func (o *redhatBase) procPathToFQPN(execCommand string) (string, error) { } func (o *redhatBase) getPkgName(paths []string) (pkgNames []string, err error) { - cmd := rpmQf(o.Distro) + strings.Join(paths, " ") + cmd := o.rpmQf(o.Distro) + strings.Join(paths, " ") r := o.exec(util.PrependProxyEnv(cmd), noSudo) if !r.isSuccess() { return nil, xerrors.Errorf("Failed to SSH: %s", r) @@ -652,3 +663,37 @@ func (o *redhatBase) getPkgName(paths []string) (pkgNames []string, err error) { } return pkgNames, nil } + +func (o *redhatBase) rpmQa(distro config.Distro) string { + const old = `rpm -qa --queryformat "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n"` + const new = `rpm -qa --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n"` + switch distro.Family { + case config.SUSEEnterpriseServer: + if v, _ := distro.MajorVersion(); v < 12 { + return old + } + return new + default: + if v, _ := distro.MajorVersion(); v < 6 { + return old + } + return new + } +} + +func (o *redhatBase) rpmQf(distro config.Distro) string { + const old = `rpm -qf --queryformat "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n" ` + const new = `rpm -qf --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n" ` + switch distro.Family { + case config.SUSEEnterpriseServer: + if v, _ := distro.MajorVersion(); v < 12 { + return old + } + return new + default: + if v, _ := distro.MajorVersion(); v < 6 { + return old + } + return new + } +} diff --git a/scan/rhel.go b/scan/rhel.go index 9d1177cf..bf815a1a 100644 --- a/scan/rhel.go +++ b/scan/rhel.go @@ -89,6 +89,7 @@ func (o *rhel) sudoNoPasswdCmdsFastRoot() []cmd { {"stat /proc/1/exe", exitStatusZero}, {"ls -l /proc/1/exe", exitStatusZero}, {"cat /proc/1/maps", exitStatusZero}, + {"lsof -i -P", exitStatusZero}, } } return []cmd{ diff --git a/scan/utils.go b/scan/utils.go index 7a8896c9..c91a55e2 100644 --- a/scan/utils.go +++ b/scan/utils.go @@ -50,37 +50,3 @@ func isRunningKernel(pack models.Package, family string, kernel models.Kernel) ( } return false, false } - -func rpmQa(distro config.Distro) string { - const old = `rpm -qa --queryformat "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n"` - const new = `rpm -qa --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n"` - switch distro.Family { - case config.SUSEEnterpriseServer: - if v, _ := distro.MajorVersion(); v < 12 { - return old - } - return new - default: - if v, _ := distro.MajorVersion(); v < 6 { - return old - } - return new - } -} - -func rpmQf(distro config.Distro) string { - const old = `rpm -qf --queryformat "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n" ` - const new = `rpm -qf --queryformat "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n" ` - switch distro.Family { - case config.SUSEEnterpriseServer: - if v, _ := distro.MajorVersion(); v < 12 { - return old - } - return new - default: - if v, _ := distro.MajorVersion(); v < 6 { - return old - } - return new - } -}