Add: source code

This commit is contained in:
kota kanbe
2016-04-01 14:36:50 +09:00
parent 9ee9641a8a
commit f4fb0b5463
39 changed files with 7546 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.vscode
coverage.out
issues/
*.txt
vendor/
log/
.gitmodules
vuls
*.sqlite3

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
TODO

52
Makefile Normal file
View File

@@ -0,0 +1,52 @@
.PHONY: \
all \
vendor \
lint \
vet \
fmt \
fmtcheck \
pretest \
test \
integration \
cov \
clean
SRCS = $(shell git ls-files '*.go')
PKGS = ./. ./db ./config ./models ./report ./cveapi ./scan ./util ./commands
all: test
vendor:
@ go get -v github.com/mjibson/party
party -d external -c -u
lint:
@ go get -v github.com/golang/lint/golint
$(foreach file,$(SRCS),golint $(file) || exit;)
vet:
@-go get -v golang.org/x/tools/cmd/vet
$(foreach pkg,$(PKGS),go vet $(pkg);)
fmt:
gofmt -w $(SRCS)
fmtcheck:
$(foreach file,$(SRCS),gofmt -d $(file);)
pretest: lint vet fmtcheck
test: pretest
$(foreach pkg,$(PKGS),go test -v $(pkg) || exit;)
unused :
$(foreach pkg,$(PKGS),unused $(pkg);)
cov:
@ go get -v github.com/axw/gocov/gocov
@ go get golang.org/x/tools/cmd/cover
gocov test | gocov report
clean:
$(foreach pkg,$(PKGS),go clean $(pkg) || exit;)

158
commands/discover.go Normal file
View File

@@ -0,0 +1,158 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package commands
import (
"flag"
"fmt"
"os"
"strings"
"text/template"
"github.com/google/subcommands"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
ps "github.com/kotakanbe/go-pingscanner"
)
// DiscoverCmd is Subcommand of host discovery mode
type DiscoverCmd struct {
}
// Name return subcommand name
func (*DiscoverCmd) Name() string { return "discover" }
// Synopsis return synopsis
func (*DiscoverCmd) Synopsis() string { return "Host discovery in the CIDR." }
// Usage return usage
func (*DiscoverCmd) Usage() string {
return `discover:
discover 192.168.0.0/24
`
}
// SetFlags set flag
func (p *DiscoverCmd) SetFlags(f *flag.FlagSet) {
}
// Execute execute
func (p *DiscoverCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
// validate
if len(f.Args()) == 0 {
return subcommands.ExitUsageError
}
for _, cidr := range f.Args() {
scanner := ps.PingScanner{
CIDR: cidr,
PingOptions: []string{
"-c1",
"-t1",
},
NumOfConcurrency: 100,
}
hosts, err := scanner.Scan()
if err != nil {
logrus.Errorf("Host Discovery failed. err: %s", err)
return subcommands.ExitFailure
}
if len(hosts) < 1 {
logrus.Errorf("Active hosts not found in %s.", cidr)
return subcommands.ExitSuccess
} else if err := printConfigToml(hosts); err != nil {
logrus.Errorf("Failed to parse template. err: %s", err)
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}
// Output the tmeplate of config.toml
func printConfigToml(ips []string) (err error) {
const tomlTempale = `
[slack]
hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz"
channel = "#channel-name"
#channel = "#{servername}"
iconEmoji = ":ghost:"
authUser = "username"
notifyUsers = ["@username"]
[mail]
smtpAddr = "smtp.gmail.com"
smtpPort = 465
user = "username"
password = "password"
from = "from@address.com"
to = ["to@address.com"]
cc = ["cc@address.com"]
subjectPrefix = "[vuls]"
[default]
#port = "22"
#user = "username"
#password = "password"
#keyPath = "/home/username/.ssh/id_rsa"
#keyPassword = "password"
[servers]
{{- $names:= .Names}}
{{range $i, $ip := .IPs}}
[servers.{{index $names $i}}]
host = "{{$ip}}"
#port = "22"
#user = "root"
#password = "password"
#keyPath = "/home/username/.ssh/id_rsa"
#keyPassword = "password"
#cpeNames = [
# "cpe:/a:rubyonrails:ruby_on_rails:4.2.1",
#]
{{end}}
`
var tpl *template.Template
if tpl, err = template.New("tempalte").Parse(tomlTempale); err != nil {
return
}
type activeHosts struct {
IPs []string
Names []string
}
a := activeHosts{IPs: ips}
names := []string{}
for _, ip := range ips {
// TOML section header must not contain "."
name := strings.Replace(ip, ".", "-", -1)
names = append(names, name)
}
a.Names = names
fmt.Println("# Create config.toml using below and then ./vuls --config=/path/to/config.toml")
if err = tpl.Execute(os.Stdout, a); err != nil {
return
}
return
}

131
commands/prepare.go Normal file
View File

@@ -0,0 +1,131 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package commands
import (
"flag"
"os"
"github.com/Sirupsen/logrus"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/scan"
"github.com/future-architect/vuls/util"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
// PrepareCmd is Subcommand of host discovery mode
type PrepareCmd struct {
debug bool
configPath string
useUnattendedUpgrades bool
}
// Name return subcommand name
func (*PrepareCmd) Name() string { return "prepare" }
// Synopsis return synopsis
func (*PrepareCmd) Synopsis() string {
// return "Install packages Ubuntu: unattended-upgrade, CentOS: yum-plugin-security)"
return `Install required packages to scan.
CentOS: yum-plugin-security, yum-plugin-changelog
Amazon: None
RHEL: TODO
Ubuntu: None
`
}
// Usage return usage
func (*PrepareCmd) Usage() string {
return `prepare:
prepare [-config=/path/to/config.toml] [-debug]
`
}
// SetFlags set flag
func (p *PrepareCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.debug, "debug", false, "debug mode")
defaultConfPath := os.Getenv("PWD") + "/config.toml"
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
f.BoolVar(
&p.useUnattendedUpgrades,
"use-unattended-upgrades",
false,
"[Depricated] For Ubuntu, install unattended-upgrades",
)
}
// Execute execute
func (p *PrepareCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
logrus.Infof("Begin Preparing (config: %s)", p.configPath)
err := c.Load(p.configPath)
if err != nil {
logrus.Errorf("Error loading %s, %s", p.configPath, err)
return subcommands.ExitUsageError
}
target := make(map[string]c.ServerInfo)
for _, arg := range f.Args() {
found := false
for servername, info := range c.Conf.Servers {
if servername == arg {
target[servername] = info
found = true
break
}
}
if !found {
logrus.Errorf("%s is not in config", arg)
return subcommands.ExitUsageError
}
}
if 0 < len(f.Args()) {
c.Conf.Servers = target
}
c.Conf.Debug = p.debug
c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades
// Set up custom logger
logger := util.NewCustomLogger(c.ServerInfo{})
logger.Info("Detecting OS... ")
err = scan.InitServers(logger)
if err != nil {
logger.Errorf("Failed to init servers. err: %s", err)
return subcommands.ExitFailure
}
logger.Info("Installing...")
if errs := scan.Prepare(); 0 < len(errs) {
for _, e := range errs {
logger.Errorf("Failed: %s.", e)
}
return subcommands.ExitFailure
}
logger.Info("Success")
return subcommands.ExitSuccess
}

241
commands/scan.go Normal file
View File

@@ -0,0 +1,241 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package commands
import (
"flag"
"os"
"github.com/Sirupsen/logrus"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/cveapi"
"github.com/future-architect/vuls/db"
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/scan"
"github.com/future-architect/vuls/util"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
// ScanCmd is Subcommand of host discovery mode
type ScanCmd struct {
lang string
debug bool
debugSQL bool
configPath string
dbpath string
cveDictionaryURL string
cvssScoreOver float64
httpProxy string
useYumPluginSecurity bool
useUnattendedUpgrades bool
// reporting
reportSlack bool
reportMail bool
}
// Name return subcommand name
func (*ScanCmd) Name() string { return "scan" }
// Synopsis return synopsis
func (*ScanCmd) Synopsis() string { return "Scan vulnerabilities." }
// Usage return usage
func (*ScanCmd) Usage() string {
return `scan:
scan
[-lang=en|ja]
[-config=/path/to/config.toml]
[-dbpath=/path/to/vuls.sqlite3]
[-cve-dictionary-url=http://127.0.0.1:1323]
[-cvss-over=7]
[-report-slack]
[-report-mail]
[-http-proxy=http://192.168.0.1:8080]
[-debug]
[-debug-sql]
`
}
// SetFlags set flag
func (p *ScanCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.lang, "lang", "en", "[en|ja]")
f.BoolVar(&p.debug, "debug", false, "debug mode")
f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
defaultConfPath := os.Getenv("PWD") + "/config.toml"
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
defaultDBPath := os.Getenv("PWD") + "/vuls.sqlite3"
f.StringVar(&p.dbpath, "dbpath", defaultDBPath, "/path/to/sqlite3")
defaultURL := "http://127.0.0.1:1323"
f.StringVar(
&p.cveDictionaryURL,
"cve-dictionary-url",
defaultURL,
"http://CVE.Dictionary")
f.Float64Var(
&p.cvssScoreOver,
"cvss-over",
0,
"-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))")
f.StringVar(
&p.httpProxy,
"http-proxy",
"",
"http://proxy-url:port (default: empty)",
)
f.BoolVar(&p.reportSlack, "report-slack", false, "Slack report")
f.BoolVar(&p.reportMail, "report-mail", false, "Email report")
f.BoolVar(
&p.useYumPluginSecurity,
"use-yum-plugin-security",
false,
"[Depricated] For CentOS 5. Scan by yum-plugin-security or not (use yum check-update by default)",
)
f.BoolVar(
&p.useUnattendedUpgrades,
"use-unattended-upgrades",
false,
"[Depricated] For Ubuntu. Scan by unattended-upgrades or not (use apt-get upgrade --dry-run by default)",
)
}
// Execute execute
func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
logrus.Infof("Begin scannig (config: %s)", p.configPath)
err := c.Load(p.configPath)
if err != nil {
logrus.Errorf("Error loading %s, %s", p.configPath, err)
return subcommands.ExitUsageError
}
target := make(map[string]c.ServerInfo)
for _, arg := range f.Args() {
found := false
for servername, info := range c.Conf.Servers {
if servername == arg {
target[servername] = info
found = true
break
}
}
if !found {
logrus.Errorf("%s is not in config", arg)
return subcommands.ExitUsageError
}
}
if 0 < len(f.Args()) {
c.Conf.Servers = target
}
c.Conf.Lang = p.lang
c.Conf.Debug = p.debug
c.Conf.DebugSQL = p.debugSQL
// logger
Log := util.NewCustomLogger(c.ServerInfo{})
// report
reports := []report.ResultWriter{
report.TextWriter{},
report.LogrusWriter{},
}
if p.reportSlack {
reports = append(reports, report.SlackWriter{})
}
if p.reportMail {
reports = append(reports, report.MailWriter{})
}
c.Conf.DBPath = p.dbpath
c.Conf.CveDictionaryURL = p.cveDictionaryURL
c.Conf.HTTPProxy = p.httpProxy
c.Conf.UseYumPluginSecurity = p.useYumPluginSecurity
c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades
Log.Info("Validating Config...")
if !c.Conf.Validate() {
return subcommands.ExitUsageError
}
if ok, err := cveapi.CveClient.CheckHealth(); !ok {
Log.Errorf("CVE HTTP server is not running. %#v", cveapi.CveClient)
Log.Fatal(err)
return subcommands.ExitFailure
}
Log.Info("Detecting OS... ")
err = scan.InitServers(Log)
if err != nil {
Log.Errorf("Failed to init servers. err: %s", err)
return subcommands.ExitFailure
}
Log.Info("Scanning vulnerabilities... ")
if errs := scan.Scan(); 0 < len(errs) {
for _, e := range errs {
Log.Errorf("Failed to scan. err: %s.", e)
}
return subcommands.ExitFailure
}
scanResults, err := scan.GetScanResults()
if err != nil {
Log.Fatal(err)
return subcommands.ExitFailure
}
Log.Info("Reporting...")
filtered := scanResults.FilterByCvssOver()
for _, w := range reports {
if err := w.Write(filtered); err != nil {
Log.Fatalf("Failed to output report, err: %s", err)
return subcommands.ExitFailure
}
}
Log.Info("Insert to DB...")
if err := db.OpenDB(); err != nil {
Log.Errorf("Failed to open DB. datafile: %s, err: %s", c.Conf.DBPath, err)
return subcommands.ExitFailure
}
if err := db.MigrateDB(); err != nil {
Log.Errorf("Failed to migrate. err: %s", err)
return subcommands.ExitFailure
}
if err := db.Insert(scanResults); err != nil {
Log.Fatalf("Failed to insert. dbpath: %s, err: %s", c.Conf.DBPath, err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}

68
commands/tui.go Normal file
View File

@@ -0,0 +1,68 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package commands
import (
"flag"
"fmt"
"os"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/report"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
// TuiCmd is Subcommand of host discovery mode
type TuiCmd struct {
lang string
debugSQL bool
dbpath string
}
// Name return subcommand name
func (*TuiCmd) Name() string { return "tui" }
// Synopsis return synopsis
func (*TuiCmd) Synopsis() string { return "Run Tui view to anayze vulnerabilites." }
// Usage return usage
func (*TuiCmd) Usage() string {
return `tui:
tui [-dbpath=/path/to/vuls.sqlite3]
`
}
// SetFlags set flag
func (p *TuiCmd) SetFlags(f *flag.FlagSet) {
// f.StringVar(&p.lang, "lang", "en", "[en|ja]")
f.BoolVar(&p.debugSQL, "debug-sql", false, "debug SQL")
defaultDBPath := os.Getenv("PWD") + "/vuls.sqlite3"
f.StringVar(&p.dbpath, "dbpath", defaultDBPath,
fmt.Sprintf("/path/to/sqlite3 (default: %s)", defaultDBPath))
}
// Execute execute
func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
c.Conf.Lang = "en"
c.Conf.DebugSQL = p.debugSQL
c.Conf.DBPath = p.dbpath
return report.RunTui()
}

32
config/color.go Normal file
View File

@@ -0,0 +1,32 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
var (
// Colors has ansi color list
Colors = []string{
"\033[32m", // green
"\033[33m", // yellow
"\033[36m", // cyan
"\033[35m", // magenta
"\033[31m", // red
"\033[34m", // blue
}
// ResetColor is reset color
ResetColor = "\033[0m"
)

221
config/config.go Normal file
View File

@@ -0,0 +1,221 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import (
"fmt"
"strings"
log "github.com/Sirupsen/logrus"
valid "github.com/asaskevich/govalidator"
)
// Conf has Configuration
var Conf Config
//Config is struct of Configuration
type Config struct {
Debug bool
DebugSQL bool
Lang string
Mail smtpConf
Slack SlackConf
Default ServerInfo
Servers map[string]ServerInfo
CveDictionaryURL string `valid:"url"`
CvssScoreOver float64
HTTPProxy string `valid:"url"`
DBPath string
// CpeNames []string
// SummaryMode bool
UseYumPluginSecurity bool
UseUnattendedUpgrades bool
}
// Validate configuration
func (c Config) Validate() bool {
errs := []error{}
if len(c.DBPath) != 0 {
if ok, _ := valid.IsFilePath(c.DBPath); !ok {
errs = append(errs, fmt.Errorf(
"SQLite3 DB path must be a *Absolute* file path. dbpath: %s", c.DBPath))
}
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
}
if mailerrs := c.Mail.Validate(); 0 < len(mailerrs) {
errs = append(errs, mailerrs...)
}
if slackerrs := c.Slack.Validate(); 0 < len(slackerrs) {
errs = append(errs, slackerrs...)
}
for _, err := range errs {
log.Error(err)
}
return len(errs) == 0
}
// smtpConf is smtp config
type smtpConf struct {
SMTPAddr string
SMTPPort string `valid:"port"`
User string
Password string
From string
To []string
Cc []string
SubjectPrefix string
UseThisTime bool
}
func checkEmails(emails []string) (errs []error) {
for _, addr := range emails {
if len(addr) == 0 {
return
}
if ok := valid.IsEmail(addr); !ok {
errs = append(errs, fmt.Errorf("Invalid email address. email: %s", addr))
}
}
return
}
// Validate SMTP configuration
func (c *smtpConf) Validate() (errs []error) {
if !c.UseThisTime {
return
}
// Check Emails fromat
emails := []string{}
emails = append(emails, c.From)
emails = append(emails, c.To...)
emails = append(emails, c.Cc...)
if emailErrs := checkEmails(emails); 0 < len(emailErrs) {
errs = append(errs, emailErrs...)
}
if len(c.SMTPAddr) == 0 {
errs = append(errs, fmt.Errorf("smtpAddr must not be empty"))
}
if len(c.SMTPPort) == 0 {
errs = append(errs, fmt.Errorf("smtpPort must not be empty"))
}
if len(c.To) == 0 {
errs = append(errs, fmt.Errorf("To required at least one address"))
}
if len(c.From) == 0 {
errs = append(errs, fmt.Errorf("From required at least one address"))
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
}
return
}
// SlackConf is slack config
type SlackConf struct {
HookURL string `valid:"url"`
Channel string `json:"channel"`
IconEmoji string `json:"icon_emoji"`
AuthUser string `json:"username"`
NotifyUsers []string
Text string `json:"text"`
UseThisTime bool
}
// Validate validates configuration
func (c *SlackConf) Validate() (errs []error) {
if !c.UseThisTime {
return
}
if len(c.HookURL) == 0 {
errs = append(errs, fmt.Errorf("hookURL must not be empty"))
}
if len(c.Channel) == 0 {
errs = append(errs, fmt.Errorf("channel must not be empty"))
} else {
if !(strings.HasPrefix(c.Channel, "#") ||
c.Channel == "${servername}") {
errs = append(errs, fmt.Errorf(
"channel's prefix must be '#', channel: %s", c.Channel))
}
}
if len(c.AuthUser) == 0 {
errs = append(errs, fmt.Errorf("authUser must not be empty"))
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
}
// TODO check if slack configration is valid
return
}
// ServerInfo has SSH Info, additional CPE packages to scan.
type ServerInfo struct {
ServerName string
User string
Password string
Host string
Port string
KeyPath string
KeyPassword string
SudoOpt SudoOption
CpeNames []string
// DebugLog Color
LogMsgAnsiColor string
}
// SudoOption is flag of sudo option.
type SudoOption struct {
// echo pass | sudo -S ls
ExecBySudo bool
// echo pass | sudo sh -C 'ls'
ExecBySudoSh bool
}

29
config/jsonloader.go Normal file
View File

@@ -0,0 +1,29 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import "fmt"
// JSONLoader loads configuration
type JSONLoader struct {
}
// Load load the configuraiton JSON file specified by path arg.
func (c JSONLoader) Load(path string) (err error) {
return fmt.Errorf("Not implement yet")
}

33
config/loader.go Normal file
View File

@@ -0,0 +1,33 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
// Load loads configuration
func Load(path string) error {
//TODO if path's suffix .toml
var loader Loader
loader = TOMLLoader{}
return loader.Load(path)
}
// Loader is interface of concrete loader
type Loader interface {
Load(string) error
}

98
config/tomlloader.go Normal file
View File

@@ -0,0 +1,98 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import (
"fmt"
"os"
"github.com/BurntSushi/toml"
log "github.com/Sirupsen/logrus"
"github.com/k0kubun/pp"
)
// TOMLLoader loads config
type TOMLLoader struct {
}
// Load load the configuraiton TOML file specified by path arg.
func (c TOMLLoader) Load(pathToToml string) (err error) {
var conf Config
if _, err := toml.DecodeFile(pathToToml, &conf); err != nil {
log.Error("Load config failed.", err)
return err
}
Conf.Mail = conf.Mail
Conf.Slack = conf.Slack
d := conf.Default
Conf.Default = d
servers := make(map[string]ServerInfo)
i := 0
for name, v := range conf.Servers {
s := ServerInfo{ServerName: name}
s.User = v.User
if s.User == "" {
s.User = d.User
}
s.Password = v.Password
if s.Password == "" {
s.Password = d.Password
}
s.Host = v.Host
s.Port = v.Port
if s.Port == "" {
s.Port = d.Port
}
s.KeyPath = v.KeyPath
if s.KeyPath == "" {
s.KeyPath = d.KeyPath
}
if s.KeyPath != "" {
if _, err := os.Stat(s.KeyPath); err != nil {
return fmt.Errorf(
"config.toml is invalid. keypath: %s not exists", s.KeyPath)
}
}
s.KeyPassword = v.KeyPassword
if s.KeyPassword == "" {
s.KeyPassword = d.KeyPassword
}
s.CpeNames = v.CpeNames
if len(s.CpeNames) == 0 {
s.CpeNames = d.CpeNames
}
s.LogMsgAnsiColor = Colors[i%len(conf.Servers)]
i++
servers[name] = s
}
log.Debug("Config loaded.")
log.Debugf("%s", pp.Sprintf("%v", servers))
Conf.Servers = servers
return
}

240
cveapi/cve_client.go Normal file
View File

@@ -0,0 +1,240 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cveapi
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"time"
"github.com/cenkalti/backoff"
"github.com/parnurzeal/gorequest"
log "github.com/Sirupsen/logrus"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/util"
cve "github.com/kotakanbe/go-cve-dictionary/models"
)
// CveClient is api client of CVE disctionary service.
var CveClient cvedictClient
type cvedictClient struct {
// httpProxy string
baseURL string
}
func (api *cvedictClient) initialize() {
api.baseURL = config.Conf.CveDictionaryURL
}
func (api cvedictClient) CheckHealth() (ok bool, err error) {
api.initialize()
url := fmt.Sprintf("%s/health", api.baseURL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if len(errs) > 0 || resp.StatusCode != 200 {
return false, fmt.Errorf("Failed to request to CVE server. url: %s, errs: %v",
url,
errs,
)
}
return true, nil
}
type response struct {
Key string
CveDetail cve.CveDetail
}
func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) {
api.baseURL = config.Conf.CveDictionaryURL
reqChan := make(chan string, len(cveIDs))
resChan := make(chan response, len(cveIDs))
errChan := make(chan error, len(cveIDs))
defer close(reqChan)
defer close(resChan)
defer close(errChan)
go func() {
for _, cveID := range cveIDs {
reqChan <- cveID
}
}()
concurrency := 10
tasks := util.GenWorkers(concurrency)
for range cveIDs {
tasks <- func() {
select {
case cveID := <-reqChan:
url, err := util.URLPathJoin(api.baseURL, "cves", cveID)
if err != nil {
errChan <- err
} else {
log.Debugf("HTTP Request to %s", url)
api.httpGet(cveID, url, resChan, errChan)
}
}
}
}
timeout := time.After(2 * 60 * time.Second)
var errs []error
for range cveIDs {
select {
case res := <-resChan:
if len(res.CveDetail.CveID) == 0 {
cveDetails = append(cveDetails, cve.CveDetail{
CveID: res.Key,
})
} else {
cveDetails = append(cveDetails, res.CveDetail)
}
case err := <-errChan:
errs = append(errs, err)
case <-timeout:
return []cve.CveDetail{}, fmt.Errorf("Timeout Fetching CVE")
}
}
if len(errs) != 0 {
return []cve.CveDetail{},
fmt.Errorf("Failed to fetch CVE. err: %v", errs)
}
// order by CVE ID desc
sort.Sort(cveDetails)
return
}
func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errChan chan<- error) {
var body string
var errs []error
var resp *http.Response
f := func() (err error) {
resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
if len(errs) > 0 || resp.StatusCode != 200 {
errChan <- fmt.Errorf("HTTP error. errs: %v, url: %s", errs, url)
}
return nil
}
notify := func(err error, t time.Duration) {
log.Warnf("Failed to get. retrying in %s seconds. err: %s", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
errChan <- fmt.Errorf("HTTP Error %s", err)
}
cveDetail := cve.CveDetail{}
if err := json.Unmarshal([]byte(body), &cveDetail); err != nil {
errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err)
}
resChan <- response{
key,
cveDetail,
}
}
// func (api cvedictClient) httpGet(key, url string, query map[string]string, resChan chan<- response, errChan chan<- error) {
// var body string
// var errs []error
// var resp *http.Response
// f := func() (err error) {
// req := gorequest.New().SetDebug(true).Proxy(api.httpProxy).Get(url)
// for key := range query {
// req = req.Query(fmt.Sprintf("%s=%s", key, query[key])).Set("Content-Type", "application/x-www-form-urlencoded")
// }
// pp.Println(req)
// resp, body, errs = req.End()
// if len(errs) > 0 || resp.StatusCode != 200 {
// errChan <- fmt.Errorf("HTTP error. errs: %v, url: %s", errs, url)
// }
// return nil
// }
// notify := func(err error, t time.Duration) {
// log.Warnf("Failed to get. retrying in %s seconds. err: %s", t, err)
// }
// err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
// if err != nil {
// errChan <- fmt.Errorf("HTTP Error %s", err)
// }
// // resChan <- body
// cveDetail := cve.CveDetail{}
// if err := json.Unmarshal([]byte(body), &cveDetail); err != nil {
// errChan <- fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err)
// }
// resChan <- response{
// key,
// cveDetail,
// }
// }
type responseGetCveDetailByCpeName struct {
CpeName string
CveDetails []cve.CveDetail
}
func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDetail, error) {
api.baseURL = config.Conf.CveDictionaryURL
url, err := util.URLPathJoin(api.baseURL, "cpes")
if err != nil {
return []cve.CveDetail{}, err
}
query := map[string]string{"name": cpeName}
log.Debugf("HTTP Request to %s, query: %#v", url, query)
return api.httpPost(cpeName, url, query)
}
func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cve.CveDetail, error) {
var body string
var errs []error
var resp *http.Response
f := func() (err error) {
req := gorequest.New().SetDebug(config.Conf.Debug).Post(url)
for key := range query {
req = req.Send(fmt.Sprintf("%s=%s", key, query[key])).Type("json")
}
resp, body, errs = req.End()
if len(errs) > 0 || resp.StatusCode != 200 {
return fmt.Errorf("HTTP error. errs: %v, url: %s", errs, url)
}
return nil
}
notify := func(err error, t time.Duration) {
log.Warnf("Failed to get. retrying in %s seconds. err: %s", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
return []cve.CveDetail{}, fmt.Errorf("HTTP Error %s", err)
}
cveDetails := []cve.CveDetail{}
if err := json.Unmarshal([]byte(body), &cveDetails); err != nil {
return []cve.CveDetail{},
fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err)
}
return cveDetails, nil
}

272
db/db.go Normal file
View File

@@ -0,0 +1,272 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package db
import (
"fmt"
"sort"
"time"
"github.com/future-architect/vuls/config"
m "github.com/future-architect/vuls/models"
"github.com/jinzhu/gorm"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
cve "github.com/kotakanbe/go-cve-dictionary/models"
)
var db *gorm.DB
// OpenDB opens Database
func OpenDB() (err error) {
db, err = gorm.Open("sqlite3", config.Conf.DBPath)
if err != nil {
err = fmt.Errorf("Failed to open DB. datafile: %s, err: %s", config.Conf.DBPath, err)
return
}
db.LogMode(config.Conf.DebugSQL)
return
}
// MigrateDB migrates Database
func MigrateDB() error {
if err := db.AutoMigrate(
&m.ScanHistory{},
&m.ScanResult{},
// &m.NWLink{},
&m.CveInfo{},
&m.CpeName{},
&m.PackageInfo{},
&m.DistroAdvisory{},
&cve.CveDetail{},
&cve.Jvn{},
&cve.Nvd{},
&cve.Reference{},
&cve.Cpe{},
).Error; err != nil {
return fmt.Errorf("Failed to migrate. err: %s", err)
}
errMsg := "Failed to create index. err: %s"
// if err := db.Model(&m.NWLink{}).
// AddIndex("idx_n_w_links_scan_result_id", "scan_result_id").Error; err != nil {
// return fmt.Errorf(errMsg, err)
// }
if err := db.Model(&m.CveInfo{}).
AddIndex("idx_cve_infos_scan_result_id", "scan_result_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&m.CpeName{}).
AddIndex("idx_cpe_names_cve_info_id", "cve_info_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&m.PackageInfo{}).
AddIndex("idx_package_infos_cve_info_id", "cve_info_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&m.DistroAdvisory{}).
//TODO check table name
AddIndex("idx_distro_advisories_cve_info_id", "cve_info_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.CveDetail{}).
AddIndex("idx_cve_detail_cve_info_id", "cve_info_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.CveDetail{}).
AddIndex("idx_cve_detail_cveid", "cve_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Nvd{}).
AddIndex("idx_nvds_cve_detail_id", "cve_detail_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Jvn{}).
AddIndex("idx_jvns_cve_detail_id", "cve_detail_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Cpe{}).
AddIndex("idx_cpes_jvn_id", "jvn_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Reference{}).
AddIndex("idx_references_jvn_id", "jvn_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Cpe{}).
AddIndex("idx_cpes_nvd_id", "nvd_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
if err := db.Model(&cve.Reference{}).
AddIndex("idx_references_nvd_id", "nvd_id").Error; err != nil {
return fmt.Errorf(errMsg, err)
}
return nil
}
// Insert inserts scan results into DB
func Insert(results []m.ScanResult) error {
for _, r := range results {
r.KnownCves = resetGormIDs(r.KnownCves)
r.UnknownCves = resetGormIDs(r.UnknownCves)
}
history := m.ScanHistory{
ScanResults: results,
ScannedAt: time.Now(),
}
db = db.Set("gorm:save_associations", false)
if err := db.Create(&history).Error; err != nil {
return err
}
for _, scanResult := range history.ScanResults {
scanResult.ScanHistoryID = history.ID
if err := db.Create(&scanResult).Error; err != nil {
return err
}
if err := insertCveInfos(scanResult.ID, scanResult.KnownCves); err != nil {
return err
}
if err := insertCveInfos(scanResult.ID, scanResult.UnknownCves); err != nil {
return err
}
}
return nil
}
func insertCveInfos(scanResultID uint, infos []m.CveInfo) error {
for _, cveInfo := range infos {
cveInfo.ScanResultID = scanResultID
if err := db.Create(&cveInfo).Error; err != nil {
return err
}
for _, pack := range cveInfo.Packages {
pack.CveInfoID = cveInfo.ID
if err := db.Create(&pack).Error; err != nil {
return err
}
}
for _, distroAdvisory := range cveInfo.DistroAdvisories {
distroAdvisory.CveInfoID = cveInfo.ID
if err := db.Create(&distroAdvisory).Error; err != nil {
return err
}
}
for _, cpeName := range cveInfo.CpeNames {
cpeName.CveInfoID = cveInfo.ID
if err := db.Create(&cpeName).Error; err != nil {
return err
}
}
db = db.Set("gorm:save_associations", true)
cveDetail := cveInfo.CveDetail
cveDetail.CveInfoID = cveInfo.ID
if err := db.Create(&cveDetail).Error; err != nil {
return err
}
db = db.Set("gorm:save_associations", false)
}
return nil
}
func resetGormIDs(infos []m.CveInfo) []m.CveInfo {
for i := range infos {
infos[i].CveDetail.ID = 0
// NVD
infos[i].CveDetail.Nvd.ID = 0
for j := range infos[i].CveDetail.Nvd.Cpes {
infos[i].CveDetail.Nvd.Cpes[j].ID = 0
}
for j := range infos[i].CveDetail.Nvd.References {
infos[i].CveDetail.Nvd.References[j].ID = 0
}
// JVN
infos[i].CveDetail.Jvn.ID = 0
for j := range infos[i].CveDetail.Jvn.Cpes {
infos[i].CveDetail.Jvn.Cpes[j].ID = 0
}
for j := range infos[i].CveDetail.Jvn.References {
infos[i].CveDetail.Jvn.References[j].ID = 0
}
//Packages
for j := range infos[i].Packages {
infos[i].Packages[j].ID = 0
infos[i].Packages[j].CveInfoID = 0
}
}
return infos
}
// SelectLatestScanHistory select latest scan history from DB
func SelectLatestScanHistory() (m.ScanHistory, error) {
scanHistory := m.ScanHistory{}
db.Order("scanned_at desc").First(&scanHistory)
if scanHistory.ID == 0 {
return m.ScanHistory{}, fmt.Errorf("No scanHistory records.")
}
results := []m.ScanResult{}
db.Model(&scanHistory).Related(&results, "ScanResults")
scanHistory.ScanResults = results
for i, r := range results {
// nw := []m.NWLink{}
// db.Model(&r).Related(&nw, "NWLinks")
// scanHistory.ScanResults[i].NWLinks = nw
knownCves := selectCveInfos(&r, "KnownCves")
sort.Sort(m.CveInfos(knownCves))
scanHistory.ScanResults[i].KnownCves = knownCves
}
return scanHistory, nil
}
func selectCveInfos(result *m.ScanResult, fieldName string) []m.CveInfo {
cveInfos := []m.CveInfo{}
db.Model(&result).Related(&cveInfos, fieldName)
for i, cveInfo := range cveInfos {
cveDetail := cve.CveDetail{}
db.Model(&cveInfo).Related(&cveDetail, "CveDetail")
id := cveDetail.CveID
filledCveDetail := cvedb.Get(id, db)
cveInfos[i].CveDetail = filledCveDetail
packs := []m.PackageInfo{}
db.Model(&cveInfo).Related(&packs, "Packages")
cveInfos[i].Packages = packs
advisories := []m.DistroAdvisory{}
db.Model(&cveInfo).Related(&advisories, "DistroAdvisories")
cveInfos[i].DistroAdvisories = advisories
names := []m.CpeName{}
db.Model(&cveInfo).Related(&names, "CpeNames")
cveInfos[i].CpeNames = names
}
return cveInfos
}

44
main.go Normal file
View File

@@ -0,0 +1,44 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"flag"
"os"
"golang.org/x/net/context"
"github.com/future-architect/vuls/commands"
"github.com/google/subcommands"
_ "github.com/mattn/go-sqlite3"
)
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&commands.DiscoverCmd{}, "discover")
subcommands.Register(&commands.TuiCmd{}, "tui")
subcommands.Register(&commands.ScanCmd{}, "scan")
subcommands.Register(&commands.PrepareCmd{}, "prepare")
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
}

244
models/models.go Normal file
View File

@@ -0,0 +1,244 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package models
import (
"fmt"
"sort"
"time"
"github.com/future-architect/vuls/config"
"github.com/jinzhu/gorm"
cve "github.com/kotakanbe/go-cve-dictionary/models"
)
// ScanHistory is the history of Scanning.
type ScanHistory struct {
gorm.Model
ScanResults []ScanResult
ScannedAt time.Time
}
// ScanResults is slice of ScanResult.
type ScanResults []ScanResult
// FilterByCvssOver is filter function.
func (results ScanResults) FilterByCvssOver() (filtered ScanResults) {
for _, result := range results {
cveInfos := []CveInfo{}
for _, cveInfo := range result.KnownCves {
if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) {
cveInfos = append(cveInfos, cveInfo)
}
}
result.KnownCves = cveInfos
filtered = append(filtered, result)
}
return
}
// ScanResult has the result of scanned CVE information.
type ScanResult struct {
gorm.Model
ScanHistoryID uint
ServerName string // TOML Section key
// Hostname string
Family string
Release string
// Fqdn string
// NWLinks []NWLink
KnownCves []CveInfo
UnknownCves []CveInfo
}
// CveSummary summarize the number of CVEs group by CVSSv2 Severity
func (r ScanResult) CveSummary() string {
var high, middle, low, unknown int
cves := append(r.KnownCves, r.UnknownCves...)
for _, cveInfo := range cves {
score := cveInfo.CveDetail.CvssScore(config.Conf.Lang)
switch {
case 7.0 < score:
high++
case 4.0 < score:
middle++
case 0 < score:
low++
default:
unknown++
}
}
return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d ?:%d)",
high+middle+low+unknown,
high, middle, low, unknown,
)
}
// NWLink has network link information.
type NWLink struct {
gorm.Model
ScanResultID uint
IPAddress string
Netmask string
DevName string
LinkState string
}
// CveInfos is for sorting
type CveInfos []CveInfo
func (c CveInfos) Len() int {
return len(c)
}
func (c CveInfos) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c CveInfos) Less(i, j int) bool {
lang := config.Conf.Lang
return c[i].CveDetail.CvssScore(lang) > c[j].CveDetail.CvssScore(lang)
}
// CveInfo has Cve Information.
type CveInfo struct {
gorm.Model
ScanResultID uint
CveDetail cve.CveDetail
Packages []PackageInfo
DistroAdvisories []DistroAdvisory
CpeNames []CpeName
}
// CpeName has CPE name
type CpeName struct {
gorm.Model
CveInfoID uint
Name string
}
// PackageInfoList is slice of PackageInfo
type PackageInfoList []PackageInfo
// Exists returns true if exists the name
func (ps PackageInfoList) Exists(name string) bool {
for _, p := range ps {
if p.Name == name {
return true
}
}
return false
}
// UniqByName be uniq by name.
func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) {
set := make(map[string]PackageInfo)
for _, p := range ps {
set[p.Name] = p
}
//sort by key
keys := []string{}
for key := range set {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
distincted = append(distincted, set[key])
}
return
}
// FindByName search PackageInfo by name
func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found bool) {
for _, p := range ps {
if p.Name == name {
return p, true
}
}
return PackageInfo{}, false
}
// Find search PackageInfo by name-version-release
// func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) {
// for _, p := range ps {
// joined := p.Name
// if 0 < len(p.Version) {
// joined = fmt.Sprintf("%s-%s", joined, p.Version)
// }
// if 0 < len(p.Release) {
// joined = fmt.Sprintf("%s-%s", joined, p.Release)
// }
// if joined == nameVersionRelease {
// return p, true
// }
// }
// return PackageInfo{}, false
// }
// PackageInfo has installed packages.
type PackageInfo struct {
gorm.Model
CveInfoID uint
Name string
Version string
Release string
NewVersion string
NewRelease string
}
// ToStringCurrentVersion returns package name-version-release
func (p PackageInfo) ToStringCurrentVersion() string {
str := p.Name
if 0 < len(p.Version) {
str = fmt.Sprintf("%s-%s", str, p.Version)
}
if 0 < len(p.Release) {
str = fmt.Sprintf("%s-%s", str, p.Release)
}
return str
}
// ToStringNewVersion returns package name-version-release
func (p PackageInfo) ToStringNewVersion() string {
str := p.Name
if 0 < len(p.NewVersion) {
str = fmt.Sprintf("%s-%s", str, p.NewVersion)
}
if 0 < len(p.NewRelease) {
str = fmt.Sprintf("%s-%s", str, p.NewRelease)
}
return str
}
// DistroAdvisory has Amazon Linux AMI Security Advisory information.
//TODO Rename to DistroAdvisory
type DistroAdvisory struct {
gorm.Model
CveInfoID uint
AdvisoryID string
Severity string
Issued time.Time
Updated time.Time
}

54
models/models_test.go Normal file
View File

@@ -0,0 +1,54 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package models
import "testing"
func TestPackageInfosUniqByName(t *testing.T) {
var test = struct {
in PackageInfoList
out PackageInfoList
}{
PackageInfoList{
{
Name: "hoge",
},
{
Name: "fuga",
},
{
Name: "hoge",
},
},
PackageInfoList{
{
Name: "hoge",
},
{
Name: "fuga",
},
},
}
actual := test.in.UniqByName()
for i, ePack := range test.out {
if actual[i].Name == ePack.Name {
t.Errorf("expected %#v, actual %#v", ePack.Name, actual[i].Name)
}
}
}

37
report/json.go Normal file
View File

@@ -0,0 +1,37 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"encoding/json"
"fmt"
"github.com/future-architect/vuls/models"
)
// JSONWriter writes report as JSON format
type JSONWriter struct{}
func (w JSONWriter) Write(scanResults []models.ScanResult) (err error) {
var j []byte
if j, err = json.MarshalIndent(scanResults, "", " "); err != nil {
return
}
fmt.Println(string(j))
return nil
}

51
report/logrus.go Normal file
View File

@@ -0,0 +1,51 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"os"
"github.com/Sirupsen/logrus"
"github.com/future-architect/vuls/models"
formatter "github.com/kotakanbe/logrus-prefixed-formatter"
)
// LogrusWriter write to logfile
type LogrusWriter struct {
}
func (w LogrusWriter) Write(scanResults []models.ScanResult) error {
path := "/var/log/vuls/report.log"
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
log := logrus.New()
log.Formatter = &formatter.TextFormatter{}
log.Out = f
log.Level = logrus.InfoLevel
for _, s := range scanResults {
text, err := toPlainText(s)
if err != nil {
return err
}
log.Infof(text)
}
return nil
}

70
report/mail.go Normal file
View File

@@ -0,0 +1,70 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"crypto/tls"
"fmt"
"strconv"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"gopkg.in/gomail.v2"
)
// MailWriter send mail
type MailWriter struct{}
func (w MailWriter) Write(scanResults []models.ScanResult) (err error) {
conf := config.Conf
for _, s := range scanResults {
m := gomail.NewMessage()
m.SetHeader("From", conf.Mail.From)
m.SetHeader("To", conf.Mail.To...)
m.SetHeader("Cc", conf.Mail.Cc...)
subject := fmt.Sprintf("%s%s %s",
conf.Mail.SubjectPrefix,
s.ServerName,
s.CveSummary(),
)
m.SetHeader("Subject", subject)
var body string
if body, err = toPlainText(s); err != nil {
return err
}
m.SetBody("text/plain", body)
port, _ := strconv.Atoi(conf.Mail.SMTPPort)
d := gomail.NewPlainDialer(
conf.Mail.SMTPAddr,
port,
conf.Mail.User,
conf.Mail.Password,
)
d.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
}
if err := d.DialAndSend(m); err != nil {
panic(err)
}
}
return nil
}

236
report/slack.go Normal file
View File

@@ -0,0 +1,236 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"encoding/json"
"fmt"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/parnurzeal/gorequest"
)
type field struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
type attachment struct {
Title string `json:"title"`
TitleLink string `json:"title_link"`
Fallback string `json:"fallback"`
Text string `json:"text"`
Pretext string `json:"pretext"`
Color string `json:"color"`
Fields []*field `json:"fields"`
MrkdwnIn []string `json:"mrkdwn_in"`
}
type message struct {
Text string `json:"text"`
Username string `json:"username"`
IconEmoji string `json:"icon_emoji"`
Channel string `json:"channel"`
Attachments []*attachment `json:"attachments"`
}
// SlackWriter send report to slack
type SlackWriter struct{}
func (w SlackWriter) Write(scanResults []models.ScanResult) error {
conf := config.Conf.Slack
for _, s := range scanResults {
channel := conf.Channel
if channel == "${servername}" {
channel = fmt.Sprintf("#%s", s.ServerName)
}
msg := message{
Text: msgText(s),
Username: conf.AuthUser,
IconEmoji: conf.IconEmoji,
Channel: channel,
Attachments: toSlackAttachments(s),
}
bytes, _ := json.Marshal(msg)
jsonBody := string(bytes)
f := func() (err error) {
resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).
Send(string(jsonBody)).End()
if resp.StatusCode != 200 {
log.Errorf("Resonse body: %s", body)
if len(errs) > 0 {
return errs[0]
}
}
return nil
}
notify := func(err error, t time.Duration) {
log.Warn("Retrying in ", t)
}
if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil {
return fmt.Errorf("HTTP Error: %s", err)
}
}
return nil
}
func msgText(r models.ScanResult) string {
notifyUsers := ""
if 0 < len(r.KnownCves) || 0 < len(r.UnknownCves) {
notifyUsers = getNotifyUsers(config.Conf.Slack.NotifyUsers)
}
hostinfo := fmt.Sprintf(
"*%s* (%s %s)",
r.ServerName,
r.Family,
r.Release,
)
return fmt.Sprintf("%s\n%s\n>%s", notifyUsers, hostinfo, r.CveSummary())
}
func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) {
scanResult.KnownCves = append(scanResult.KnownCves, scanResult.UnknownCves...)
for _, cveInfo := range scanResult.KnownCves {
cveID := cveInfo.CveDetail.CveID
curentPackages := []string{}
for _, p := range cveInfo.Packages {
curentPackages = append(curentPackages, p.ToStringCurrentVersion())
}
for _, cpename := range cveInfo.CpeNames {
curentPackages = append(curentPackages, cpename.Name)
}
newPackages := []string{}
for _, p := range cveInfo.Packages {
newPackages = append(newPackages, p.ToStringNewVersion())
}
a := attachment{
Title: cveID,
TitleLink: fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID),
Text: attachmentText(cveInfo, scanResult.Family),
MrkdwnIn: []string{"text", "pretext"},
Fields: []*field{
{
// Title: "Current Package/CPE",
Title: "Installed",
Value: strings.Join(curentPackages, "\n"),
Short: true,
},
{
Title: "Candidate",
Value: strings.Join(newPackages, "\n"),
Short: true,
},
},
Color: color(cveInfo.CveDetail.CvssScore(config.Conf.Lang)),
}
attaches = append(attaches, &a)
}
return
}
// https://api.slack.com/docs/attachments
func color(cvssScore float64) string {
switch {
case 7 <= cvssScore:
return "danger"
case 4 <= cvssScore && cvssScore < 7:
return "warning"
case cvssScore < 0:
return "#C0C0C0"
default:
return "good"
}
}
func attachmentText(cveInfo models.CveInfo, osFamily string) string {
linkText := links(cveInfo, osFamily)
switch {
case config.Conf.Lang == "ja" &&
cveInfo.CveDetail.Jvn.ID != 0 &&
0 < cveInfo.CveDetail.CvssScore("ja"):
jvn := cveInfo.CveDetail.Jvn
return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s",
cveInfo.CveDetail.CvssScore(config.Conf.Lang),
jvn.Severity,
fmt.Sprintf(cvssV2CalcURLTemplate, cveInfo.CveDetail.CveID, jvn.Vector),
jvn.Vector,
jvn.Title,
linkText,
)
case 0 < cveInfo.CveDetail.CvssScore("en"):
nvd := cveInfo.CveDetail.Nvd
return fmt.Sprintf("*%4.1f (%s)* <%s|%s>\n%s\n%s",
cveInfo.CveDetail.CvssScore(config.Conf.Lang),
nvd.Severity(),
fmt.Sprintf(cvssV2CalcURLTemplate, cveInfo.CveDetail.CveID, nvd.CvssVector()),
nvd.CvssVector(),
nvd.Summary,
linkText,
)
default:
nvd := cveInfo.CveDetail.Nvd
return fmt.Sprintf("?\n%s\n%s", nvd.Summary, linkText)
}
}
func links(cveInfo models.CveInfo, osFamily string) string {
links := []string{}
cveID := cveInfo.CveDetail.CveID
if config.Conf.Lang == "ja" && 0 < len(cveInfo.CveDetail.Jvn.Link()) {
jvn := fmt.Sprintf("<%s|JVN>", cveInfo.CveDetail.Jvn.Link())
links = append(links, jvn)
}
links = append(links, fmt.Sprintf("<%s|CVEDetails>",
fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)))
links = append(links, fmt.Sprintf("<%s|MITRE>",
fmt.Sprintf("%s%s", mitreBaseURL, cveID)))
dlinks := distroLinks(cveInfo, osFamily)
for _, link := range dlinks {
links = append(links,
fmt.Sprintf("<%s|%s>", link.url, link.title))
}
return strings.Join(links, " / ")
}
// See testcase
func getNotifyUsers(notifyUsers []string) string {
slackStyleTexts := []string{}
for _, username := range notifyUsers {
slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))
}
return strings.Join(slackStyleTexts, " ")
}

23
report/slack_test.go Normal file
View File

@@ -0,0 +1,23 @@
package report
import "testing"
func TestGetNotifyUsers(t *testing.T) {
var tests = []struct {
in []string
expected string
}{
{
[]string{"@user1", "@user2"},
"<@user1> <@user2>",
},
}
for _, tt := range tests {
actual := getNotifyUsers(tt.in)
if tt.expected != actual {
t.Errorf("expected %s, actual %s", tt.expected, actual)
}
}
}

38
report/stdout.go Normal file
View File

@@ -0,0 +1,38 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"fmt"
"github.com/future-architect/vuls/models"
)
// TextWriter write to stdout
type TextWriter struct{}
func (w TextWriter) Write(scanResults []models.ScanResult) error {
for _, s := range scanResults {
text, err := toPlainText(s)
if err != nil {
return err
}
fmt.Println(text)
}
return nil
}

770
report/tui.go Normal file
View File

@@ -0,0 +1,770 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"bytes"
"fmt"
"strings"
"text/template"
"time"
log "github.com/Sirupsen/logrus"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/db"
"github.com/future-architect/vuls/models"
"github.com/google/subcommands"
"github.com/gosuri/uitable"
"github.com/jroimartin/gocui"
cve "github.com/kotakanbe/go-cve-dictionary/models"
)
var scanHistory models.ScanHistory
var currentScanResult models.ScanResult
var currentCveInfo int
var currentDetailLimitY int
// RunTui execute main logic
func RunTui() subcommands.ExitStatus {
var err error
scanHistory, err = latestScanHistory()
if err != nil {
log.Fatal(err)
return subcommands.ExitFailure
}
g := gocui.NewGui()
if err := g.Init(); err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetLayout(layout)
if err := keybindings(g); err != nil {
log.Panicln(err)
}
g.SelBgColor = gocui.ColorGreen
g.SelFgColor = gocui.ColorBlack
g.Cursor = true
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
func latestScanHistory() (latest models.ScanHistory, err error) {
if err := db.OpenDB(); err != nil {
return latest, fmt.Errorf(
"Failed to open DB. datafile: %s, err: %s", config.Conf.DBPath, err)
}
latest, err = db.SelectLatestScanHistory()
return
}
func keybindings(g *gocui.Gui) (err error) {
errs := []error{}
// Move beetween views
errs = append(errs, g.SetKeybinding("side", gocui.KeyTab, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlH, gocui.ModNone, previousView))
// errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlL, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowRight, gocui.ModAlt, nextView))
errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeySpace, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlN, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlP, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
// errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, showMsg))
// summary
errs = append(errs, g.SetKeybinding("summary", gocui.KeyTab, gocui.ModNone, nextView))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlQ, gocui.ModNone, previousView))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlH, gocui.ModNone, previousView))
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlL, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowLeft, gocui.ModAlt, previousView))
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModAlt, nextView))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("summary", gocui.KeySpace, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyEnter, gocui.ModNone, nextView))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
// detail
errs = append(errs, g.SetKeybinding("detail", gocui.KeyTab, gocui.ModNone, nextView))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlQ, gocui.ModNone, previousView))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlH, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlL, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModAlt, previousView))
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowLeft, gocui.ModAlt, nextView))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("detail", gocui.KeySpace, gocui.ModNone, cursorPageDown))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, nextView))
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, showMsg))
//TODO Help Ctrl-h
errs = append(errs, g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit))
// errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, getLine))
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
for _, e := range errs {
if e != nil {
return e
}
}
return nil
}
func nextView(g *gocui.Gui, v *gocui.View) error {
if v == nil {
return g.SetCurrentView("side")
}
switch v.Name() {
case "side":
return g.SetCurrentView("summary")
case "summary":
return g.SetCurrentView("detail")
case "detail":
return g.SetCurrentView("side")
default:
return g.SetCurrentView("summary")
}
}
func previousView(g *gocui.Gui, v *gocui.View) error {
if v == nil {
return g.SetCurrentView("side")
}
switch v.Name() {
case "side":
return g.SetCurrentView("side")
case "summary":
return g.SetCurrentView("side")
case "detail":
return g.SetCurrentView("summary")
default:
return g.SetCurrentView("side")
}
}
func movable(v *gocui.View, nextY int) (ok bool, yLimit int) {
switch v.Name() {
case "side":
yLimit = len(scanHistory.ScanResults) - 1
if yLimit < nextY {
return false, yLimit
}
return true, yLimit
case "summary":
yLimit = len(currentScanResult.KnownCves) - 1
if yLimit < nextY {
return false, yLimit
}
return true, yLimit
case "detail":
if currentDetailLimitY < nextY {
return false, currentDetailLimitY
}
return true, currentDetailLimitY
default:
return true, 0
}
}
func pageUpDownJumpCount(v *gocui.View) int {
var jump int
switch v.Name() {
case "side", "summary":
jump = 8
case "detail":
jump = 30
default:
jump = 8
}
return jump
}
// redraw views
func onMovingCursorRedrawView(g *gocui.Gui, v *gocui.View) error {
switch v.Name() {
case "summary":
if err := redrawDetail(g); err != nil {
return err
}
case "side":
if err := changeHost(g, v); err != nil {
return err
}
}
return nil
}
func cursorDown(g *gocui.Gui, v *gocui.View) error {
if v != nil {
cx, cy := v.Cursor()
ox, oy := v.Origin()
// ok, := movable(v, oy+cy+1)
// _, maxY := v.Size()
ok, _ := movable(v, oy+cy+1)
// log.Info(cy, oy, maxY, yLimit)
if !ok {
return nil
}
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
}
onMovingCursorRedrawView(g, v)
}
return nil
}
func cursorMoveTop(g *gocui.Gui, v *gocui.View) error {
if v != nil {
cx, _ := v.Cursor()
v.SetCursor(cx, 0)
}
onMovingCursorRedrawView(g, v)
return nil
}
func cursorMoveBottom(g *gocui.Gui, v *gocui.View) error {
if v != nil {
_, maxY := v.Size()
cx, _ := v.Cursor()
v.SetCursor(cx, maxY-1)
}
onMovingCursorRedrawView(g, v)
return nil
}
func cursorMoveMiddle(g *gocui.Gui, v *gocui.View) error {
if v != nil {
_, maxY := v.Size()
cx, _ := v.Cursor()
v.SetCursor(cx, maxY/2)
}
onMovingCursorRedrawView(g, v)
return nil
}
func cursorPageDown(g *gocui.Gui, v *gocui.View) error {
jump := pageUpDownJumpCount(v)
if v != nil {
cx, cy := v.Cursor()
ox, oy := v.Origin()
ok, yLimit := movable(v, oy+cy+jump)
_, maxY := v.Size()
if !ok {
if yLimit < maxY {
v.SetCursor(cx, yLimit)
} else {
v.SetCursor(cx, maxY-1)
v.SetOrigin(ox, yLimit-maxY+1)
}
} else if yLimit < oy+jump+maxY {
if yLimit < maxY {
v.SetCursor(cx, yLimit)
} else {
v.SetOrigin(ox, yLimit-maxY+1)
v.SetCursor(cx, maxY-1)
}
} else {
v.SetCursor(cx, cy)
v.SetOrigin(ox, oy+jump)
}
onMovingCursorRedrawView(g, v)
}
return nil
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
if v != nil {
ox, oy := v.Origin()
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
return err
}
}
}
onMovingCursorRedrawView(g, v)
return nil
}
func cursorPageUp(g *gocui.Gui, v *gocui.View) error {
jump := pageUpDownJumpCount(v)
if v != nil {
cx, _ := v.Cursor()
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy-jump); err != nil {
v.SetOrigin(ox, 0)
v.SetCursor(cx, 0)
}
onMovingCursorRedrawView(g, v)
}
return nil
}
func previousSummary(g *gocui.Gui, v *gocui.View) error {
if v != nil {
// cursor to summary
if err := g.SetCurrentView("summary"); err != nil {
return err
}
// move next line
if err := cursorUp(g, g.CurrentView()); err != nil {
return err
}
// cursor to detail
if err := g.SetCurrentView("detail"); err != nil {
return err
}
}
return nil
}
func nextSummary(g *gocui.Gui, v *gocui.View) error {
if v != nil {
// cursor to summary
if err := g.SetCurrentView("summary"); err != nil {
return err
}
// move next line
if err := cursorDown(g, g.CurrentView()); err != nil {
return err
}
// cursor to detail
if err := g.SetCurrentView("detail"); err != nil {
return err
}
}
return nil
}
func changeHost(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView("summary"); err != nil {
return err
}
if err := g.DeleteView("detail"); err != nil {
return err
}
_, cy := v.Cursor()
l, err := v.Line(cy)
if err != nil {
return err
}
serverName := strings.TrimSpace(l)
for _, r := range scanHistory.ScanResults {
if serverName == r.ServerName {
currentScanResult = r
break
}
}
if err := setSummaryLayout(g); err != nil {
return err
}
if err := setDetailLayout(g); err != nil {
return err
}
return nil
}
func redrawDetail(g *gocui.Gui) error {
if err := g.DeleteView("detail"); err != nil {
return err
}
if err := setDetailLayout(g); err != nil {
return err
}
return nil
}
func getLine(g *gocui.Gui, v *gocui.View) error {
var l string
var err error
_, cy := v.Cursor()
if l, err = v.Line(cy); err != nil {
l = ""
}
maxX, maxY := g.Size()
if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
fmt.Fprintln(v, l)
if err := g.SetCurrentView("msg"); err != nil {
return err
}
}
return nil
}
func showMsg(g *gocui.Gui, v *gocui.View) error {
jump := 8
_, cy := v.Cursor()
_, oy := v.Origin()
ok, yLimit := movable(v, oy+cy+jump)
// maxX, maxY := v.Size()
_, maxY := v.Size()
l := fmt.Sprintf("cy: %d, oy: %d, maxY: %d, yLimit: %d, curCve %d, ok: %v", cy, oy, maxY, yLimit, currentCveInfo, ok)
// if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
if v, err := g.SetView("msg", 10, maxY/2, 10+50, maxY/2+2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
fmt.Fprintln(v, l)
if err := g.SetCurrentView("msg"); err != nil {
return err
}
}
return nil
}
func delMsg(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView("msg"); err != nil {
return err
}
if err := g.SetCurrentView("summary"); err != nil {
return err
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func layout(g *gocui.Gui) error {
if err := setSideLayout(g); err != nil {
return err
}
if err := setSummaryLayout(g); err != nil {
return err
}
if err := setDetailLayout(g); err != nil {
return err
}
return nil
}
func setSideLayout(g *gocui.Gui) error {
_, maxY := g.Size()
if v, err := g.SetView("side", -1, -1, 30, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Highlight = true
for _, result := range scanHistory.ScanResults {
fmt.Fprintln(v, result.ServerName)
}
currentScanResult = scanHistory.ScanResults[0]
if err := g.SetCurrentView("side"); err != nil {
return err
}
}
return nil
}
func setSummaryLayout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("summary", 30, -1, maxX, int(float64(maxY)*0.2)); err != nil {
if err != gocui.ErrUnknownView {
return err
}
lines := summaryLines(currentScanResult)
fmt.Fprintf(v, lines)
v.Highlight = true
v.Editable = false
v.Wrap = false
}
return nil
}
func summaryLines(data models.ScanResult) string {
stable := uitable.New()
stable.MaxColWidth = 1000
stable.Wrap = false
indexFormat := ""
if len(data.KnownCves) < 10 {
indexFormat = "[%1d]"
} else if len(data.KnownCves) < 100 {
indexFormat = "[%2d]"
} else {
indexFormat = "[%3d]"
}
for i, d := range data.KnownCves {
var cols []string
// packs := []string{}
// for _, pack := range d.Packages {
// packs = append(packs, pack.Name)
// }
if config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore() {
summary := d.CveDetail.Jvn.Title
cols = []string{
fmt.Sprintf(indexFormat, i+1),
d.CveDetail.CveID,
fmt.Sprintf("| %-4.1f(%s)",
d.CveDetail.CvssScore(config.Conf.Lang),
d.CveDetail.Jvn.Severity,
),
// strings.Join(packs, ","),
summary,
}
} else {
summary := d.CveDetail.Nvd.Summary
var cvssScore string
if d.CveDetail.CvssScore("en") <= 0 {
cvssScore = "| ?"
} else {
cvssScore = fmt.Sprintf("| %-4.1f(%s)",
d.CveDetail.CvssScore(config.Conf.Lang),
d.CveDetail.Nvd.Severity(),
)
}
cols = []string{
fmt.Sprintf(indexFormat, i+1),
d.CveDetail.CveID,
cvssScore,
summary,
}
}
icols := make([]interface{}, len(cols))
for j := range cols {
icols[j] = cols[j]
}
stable.AddRow(icols...)
}
// ignore UnknownCves
return fmt.Sprintf("%s", stable)
}
func setDetailLayout(g *gocui.Gui) error {
maxX, maxY := g.Size()
summaryView, err := g.View("summary")
if err != nil {
return err
}
_, cy := summaryView.Cursor()
_, oy := summaryView.Origin()
currentCveInfo = cy + oy
if v, err := g.SetView("detail", 30, int(float64(maxY)*0.2), maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
// text := report.ToPlainTextDetailsLangEn(
// currentScanResult.KnownCves[currentCveInfo],
// currentScanResult.Family)
//TODO error handling
text, err := detailLines()
if err != nil {
return err
}
fmt.Fprint(v, text)
v.Editable = false
v.Wrap = true
currentDetailLimitY = len(strings.Split(text, "\n")) - 1
}
return nil
}
type dataForTmpl struct {
CveID string
CvssScore string
CvssVector string
CvssSeverity string
Summary string
VulnSiteLinks []string
References []cve.Reference
Packages []string
CpeNames []models.CpeName
PublishedDate time.Time
LastModifiedDate time.Time
}
func detailLines() (string, error) {
cveInfo := currentScanResult.KnownCves[currentCveInfo]
cveID := cveInfo.CveDetail.CveID
tmpl, err := template.New("detail").Parse(detailTemplate())
if err != nil {
return "", err
}
var cvssSeverity, cvssVector, summary string
var refs []cve.Reference
switch {
case config.Conf.Lang == "ja" &&
0 < cveInfo.CveDetail.Jvn.CvssScore():
jvn := cveInfo.CveDetail.Jvn
cvssSeverity = jvn.Severity
cvssVector = jvn.Vector
summary = fmt.Sprintf("%s\n%s", jvn.Title, jvn.Summary)
refs = jvn.References
default:
nvd := cveInfo.CveDetail.Nvd
cvssSeverity = nvd.Severity()
cvssVector = nvd.CvssVector()
summary = nvd.Summary
refs = nvd.References
}
links := []string{
fmt.Sprintf("[NVD]( %s )", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID)),
fmt.Sprintf("[MITRE]( %s )", fmt.Sprintf("%s%s", mitreBaseURL, cveID)),
fmt.Sprintf("[CveDetais]( %s )", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID)),
fmt.Sprintf("[CVSSv2 Caluclator]( %s )", fmt.Sprintf(cvssV2CalcURLTemplate, cveID, cvssVector)),
}
dlinks := distroLinks(cveInfo, currentScanResult.Family)
for _, link := range dlinks {
links = append(links, fmt.Sprintf("[%s]( %s )", link.title, link.url))
}
var cvssScore string
if cveInfo.CveDetail.CvssScore(config.Conf.Lang) == -1 {
cvssScore = "?"
} else {
cvssScore = fmt.Sprintf("%4.1f", cveInfo.CveDetail.CvssScore(config.Conf.Lang))
}
packages := []string{}
for _, pack := range cveInfo.Packages {
packages = append(packages,
fmt.Sprintf(
"%s -> %s",
pack.ToStringCurrentVersion(),
pack.ToStringNewVersion()))
}
data := dataForTmpl{
CveID: cveID,
CvssScore: cvssScore,
CvssSeverity: cvssSeverity,
CvssVector: cvssVector,
Summary: summary,
VulnSiteLinks: links,
References: refs,
Packages: packages,
CpeNames: cveInfo.CpeNames,
}
buf := bytes.NewBuffer(nil) // create empty buffer
if err := tmpl.Execute(buf, data); err != nil {
return "", err
}
return string(buf.Bytes()), nil
}
// * {{.Name}}-{{.Version}}-{{.Release}}
func detailTemplate() string {
return `
{{.CveID}}
==============
CVSS Score
--------------
{{.CvssScore}} ({{.CvssSeverity}}) {{.CvssVector}}
Summary
--------------
{{.Summary }}
Package/CPE
--------------
{{range $pack := .Packages -}}
* {{$pack}}
{{end -}}
{{range .CpeNames -}}
* {{.Name}}
{{end}}
Links
--------------
{{range $link := .VulnSiteLinks -}}
* {{$link}}
{{end}}
References
--------------
{{range .References -}}
* [{{.Source}}]( {{.Link}} )
{{end}}
`
}

334
report/util.go Normal file
View File

@@ -0,0 +1,334 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"bytes"
"fmt"
"strings"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/gosuri/uitable"
)
func toPlainText(scanResult models.ScanResult) (string, error) {
hostinfo := fmt.Sprintf(
"%s (%s %s)",
scanResult.ServerName,
scanResult.Family,
scanResult.Release,
)
var buffer bytes.Buffer
for i := 0; i < len(hostinfo); i++ {
buffer.WriteString("=")
}
header := fmt.Sprintf("%s\n%s", hostinfo, buffer.String())
if len(scanResult.KnownCves) == 0 && len(scanResult.UnknownCves) == 0 {
return fmt.Sprintf(`
%s
No unsecure packages.
`, header), nil
}
summary := ToPlainTextSummary(scanResult)
scoredReport, unscoredReport := []string{}, []string{}
scoredReport, unscoredReport = toPlainTextDetails(scanResult, scanResult.Family)
scored := strings.Join(scoredReport, "\n\n")
unscored := strings.Join(unscoredReport, "\n\n")
detail := fmt.Sprintf(`
%s
%s
`,
scored,
unscored,
)
text := fmt.Sprintf("%s\n%s\n%s\n", header, summary, detail)
return text, nil
}
// ToPlainTextSummary format summary for plain text.
func ToPlainTextSummary(r models.ScanResult) string {
stable := uitable.New()
stable.MaxColWidth = 84
stable.Wrap = true
cves := append(r.KnownCves, r.UnknownCves...)
for _, d := range cves {
var scols []string
switch {
case config.Conf.Lang == "ja" &&
d.CveDetail.Jvn.ID != 0 &&
0 < d.CveDetail.CvssScore("ja"):
summary := d.CveDetail.Jvn.Title
scols = []string{
d.CveDetail.CveID,
fmt.Sprintf("%-4.1f (%s)",
d.CveDetail.CvssScore(config.Conf.Lang),
d.CveDetail.Jvn.Severity,
),
summary,
}
case 0 < d.CveDetail.CvssScore("en"):
summary := d.CveDetail.Nvd.Summary
scols = []string{
d.CveDetail.CveID,
fmt.Sprintf("%-4.1f",
d.CveDetail.CvssScore(config.Conf.Lang),
),
summary,
}
default:
scols = []string{
d.CveDetail.CveID,
"?",
d.CveDetail.Nvd.Summary,
}
}
cols := make([]interface{}, len(scols))
for i := range cols {
cols[i] = scols[i]
}
stable.AddRow(cols...)
}
return fmt.Sprintf("%s", stable)
}
//TODO Distro Advisory
func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) {
for _, cve := range data.KnownCves {
switch config.Conf.Lang {
case "en":
if cve.CveDetail.Nvd.ID != 0 {
scoredReport = append(
scoredReport, toPlainTextDetailsLangEn(cve, osFamily))
} else {
scoredReport = append(
scoredReport, toPlainTextUnknownCve(cve, osFamily))
}
case "ja":
if cve.CveDetail.Jvn.ID != 0 {
scoredReport = append(
scoredReport, toPlainTextDetailsLangJa(cve, osFamily))
} else if cve.CveDetail.Nvd.ID != 0 {
scoredReport = append(
scoredReport, toPlainTextDetailsLangEn(cve, osFamily))
} else {
scoredReport = append(
scoredReport, toPlainTextUnknownCve(cve, osFamily))
}
}
}
for _, cve := range data.UnknownCves {
unscoredReport = append(
unscoredReport, toPlainTextUnknownCve(cve, osFamily))
}
return
}
func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string {
cveID := cveInfo.CveDetail.CveID
dtable := uitable.New()
dtable.MaxColWidth = 100
dtable.Wrap = true
dtable.AddRow(cveID)
dtable.AddRow("-------------")
dtable.AddRow("Score", "?")
dtable.AddRow("NVD",
fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
dtable.AddRow("CVE Details",
fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
dlinks := distroLinks(cveInfo, osFamily)
for _, link := range dlinks {
dtable.AddRow(link.title, link.url)
}
return fmt.Sprintf("%s", dtable)
}
func toPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string {
cveDetail := cveInfo.CveDetail
cveID := cveDetail.CveID
jvn := cveDetail.Jvn
dtable := uitable.New()
//TODO resize
dtable.MaxColWidth = 100
dtable.Wrap = true
dtable.AddRow(cveID)
dtable.AddRow("-------------")
if score := cveDetail.Jvn.CvssScore(); 0 < score {
dtable.AddRow("Score",
fmt.Sprintf("%4.1f (%s)",
cveDetail.Jvn.CvssScore(),
jvn.Severity,
))
} else {
dtable.AddRow("Score", "?")
}
dtable.AddRow("Vector", jvn.Vector)
dtable.AddRow("Title", jvn.Title)
dtable.AddRow("Description", jvn.Summary)
dtable.AddRow("JVN", jvn.Link())
dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID))
dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("ja"))
dlinks := distroLinks(cveInfo, osFamily)
for _, link := range dlinks {
dtable.AddRow(link.title, link.url)
}
dtable = addPackageInfos(dtable, cveInfo.Packages)
dtable = addCpeNames(dtable, cveInfo.CpeNames)
return fmt.Sprintf("%s", dtable)
}
func toPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string {
cveDetail := d.CveDetail
cveID := cveDetail.CveID
nvd := cveDetail.Nvd
dtable := uitable.New()
//TODO resize
dtable.MaxColWidth = 100
dtable.Wrap = true
dtable.AddRow(cveID)
dtable.AddRow("-------------")
if score := cveDetail.Nvd.CvssScore(); 0 < score {
dtable.AddRow("Score",
fmt.Sprintf("%4.1f (%s)",
cveDetail.Nvd.CvssScore(),
nvd.Severity(),
))
} else {
dtable.AddRow("Score", "?")
}
dtable.AddRow("Vector", nvd.CvssVector())
dtable.AddRow("Summary", nvd.Summary)
dtable.AddRow("NVD", fmt.Sprintf("%s?vulnId=%s", nvdBaseURL, cveID))
dtable.AddRow("MITRE", fmt.Sprintf("%s%s", mitreBaseURL, cveID))
dtable.AddRow("CVE Details", fmt.Sprintf("%s/%s", cveDetailsBaseURL, cveID))
dtable.AddRow("CVSS Claculator", cveDetail.CvssV2CalculatorLink("en"))
links := distroLinks(d, osFamily)
for _, link := range links {
dtable.AddRow(link.title, link.url)
}
dtable = addPackageInfos(dtable, d.Packages)
dtable = addCpeNames(dtable, d.CpeNames)
return fmt.Sprintf("%s\n", dtable)
}
type distroLink struct {
title string
url string
}
// addVendorSite add Vendor site of the CVE to table
func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink {
cveID := cveInfo.CveDetail.CveID
switch osFamily {
case "rhel", "centos":
links := []distroLink{
{
"RHEL-CVE",
fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID),
},
}
for _, advisory := range cveInfo.DistroAdvisories {
aidURL := strings.Replace(advisory.AdvisoryID, ":", "-", -1)
links = append(links, distroLink{
// "RHEL-errata",
advisory.AdvisoryID,
fmt.Sprintf(redhatRHSABaseBaseURL, aidURL),
})
}
return links
case "amazon":
links := []distroLink{
{
"RHEL-CVE",
fmt.Sprintf("%s/%s", redhatSecurityBaseURL, cveID),
},
}
for _, advisory := range cveInfo.DistroAdvisories {
links = append(links, distroLink{
// "Amazon-ALAS",
advisory.AdvisoryID,
fmt.Sprintf(amazonSecurityBaseURL, advisory.AdvisoryID),
})
}
return links
case "ubuntu":
return []distroLink{
{
"Ubuntu-CVE",
fmt.Sprintf("%s/%s", ubuntuSecurityBaseURL, cveID),
},
//TODO Ubuntu USN
}
case "debian":
return []distroLink{
{
"Debian-CVE",
fmt.Sprintf("%s/%s", debianTrackerBaseURL, cveID),
},
// TODO Debian dsa
}
default:
return []distroLink{}
}
}
//TODO
// addPackageInfos add package information related the CVE to table
func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table {
for i, p := range packs {
var title string
if i == 0 {
title = "Package/CPE"
}
ver := fmt.Sprintf(
"%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion())
table.AddRow(title, ver)
}
return table
}
func addCpeNames(table *uitable.Table, names []models.CpeName) *uitable.Table {
for _, p := range names {
table.AddRow("CPE", fmt.Sprintf("%s", p.Name))
}
return table
}

39
report/writer.go Normal file
View File

@@ -0,0 +1,39 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import "github.com/future-architect/vuls/models"
const (
nvdBaseURL = "https://web.nvd.nist.gov/view/vuln/detail"
mitreBaseURL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
cveDetailsBaseURL = "http://www.cvedetails.com/cve"
cvssV2CalcURLTemplate = "https://nvd.nist.gov/cvss/v2-calculator?name=%s&vector=%s"
redhatSecurityBaseURL = "https://access.redhat.com/security/cve"
redhatRHSABaseBaseURL = "https://rhn.redhat.com/errata/%s.html"
amazonSecurityBaseURL = "https://alas.aws.amazon.com/%s.html"
ubuntuSecurityBaseURL = "http://people.ubuntu.com/~ubuntu-security/cve"
debianTrackerBaseURL = "https://security-tracker.debian.org/tracker"
)
// ResultWriter Interface
type ResultWriter interface {
Write([]models.ScanResult) error
}

655
scan/debian.go Normal file
View File

@@ -0,0 +1,655 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/cveapi"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// inherit OsTypeInterface
type debian struct {
linux
}
// NewDebian is constructor
func newDebian(c config.ServerInfo) *debian {
d := &debian{}
d.log = util.NewCustomLogger(c)
return d
}
// Ubuntu, Debian
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb
func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface) {
deb = newDebian(c)
// set sudo option flag
c.SudoOpt = config.SudoOption{ExecBySudo: true}
deb.setServerInfo(c)
if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() {
Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port)
return false, deb
}
if r := sshExec(c, "lsb_release -ir", noSudo); r.isSuccess() {
// e.g.
// root@fa3ec524be43:/# lsb_release -ir
// Distributor ID: Ubuntu
// Release: 14.04
re, _ := regexp.Compile(
`(?s)^Distributor ID:\s*(.+?)\n*Release:\s*(.+?)$`)
result := re.FindStringSubmatch(trim(r.Stdout))
if len(result) == 0 {
deb.setDistributionInfo("debian/ubuntu", "unknown")
Log.Warnf(
"Unknown Debian/Ubuntu version. lsb_release -ir: %s, Host: %s:%s",
r.Stdout, c.Host, c.Port)
} else {
distro := strings.ToLower(trim(result[1]))
deb.setDistributionInfo(distro, trim(result[2]))
}
return true, deb
}
if r := sshExec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() {
// e.g.
// DISTRIB_ID=Ubuntu
// DISTRIB_RELEASE=14.04
// DISTRIB_CODENAME=trusty
// DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS"
re, _ := regexp.Compile(
`(?s)^DISTRIB_ID=(.+?)\n*DISTRIB_RELEASE=(.+?)\n.*$`)
result := re.FindStringSubmatch(trim(r.Stdout))
if len(result) == 0 {
Log.Warnf(
"Unknown Debian/Ubuntu. cat /etc/lsb-release: %s, Host: %s:%s",
r.Stdout, c.Host, c.Port)
deb.setDistributionInfo("debian/ubuntu", "unknown")
} else {
distro := strings.ToLower(trim(result[1]))
deb.setDistributionInfo(distro, trim(result[2]))
}
return true, deb
}
// Debian
cmd := "cat /etc/debian_version"
if r := sshExec(c, cmd, noSudo); r.isSuccess() {
deb.setDistributionInfo("debian", trim(r.Stdout))
return true, deb
}
Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port)
return false, deb
}
func trim(str string) string {
return strings.TrimSpace(str)
}
func (o *debian) install() error {
// apt-get update
o.log.Infof("apt-get update...")
cmd := util.PrependProxyEnv("apt-get update")
if r := o.ssh(cmd, sudo); !r.isSuccess() {
msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
if o.Family == "debian" {
// install aptitude
cmd = util.PrependProxyEnv("apt-get install --force-yes -y aptitude")
if r := o.ssh(cmd, sudo); !r.isSuccess() {
msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
o.log.Infof("Installed: aptitude")
}
// install unattended-upgrades
if !config.Conf.UseUnattendedUpgrades {
return nil
}
if r := o.ssh("type unattended-upgrade", noSudo); r.isSuccess() {
o.log.Infof(
"Ignored: unattended-upgrade already installed")
return nil
}
cmd = util.PrependProxyEnv(
"apt-get install --force-yes -y unattended-upgrades")
if r := o.ssh(cmd, sudo); !r.isSuccess() {
msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
o.log.Infof("Installed: unattended-upgrades")
return nil
}
func (o *debian) scanPackages() error {
var err error
var packs []models.PackageInfo
if packs, err = o.scanInstalledPackages(); err != nil {
o.log.Errorf("Failed to scan installed packages")
return err
}
o.setPackages(packs)
var unsecurePacks []CvePacksInfo
if unsecurePacks, err = o.scanUnsecurePackages(packs); err != nil {
o.log.Errorf("Failed to scan valnerable packages")
return err
}
o.setUnsecurePackages(unsecurePacks)
return nil
}
func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) {
r := o.ssh("dpkg-query -W", noSudo)
if !r.isSuccess() {
return packs, fmt.Errorf(
"Failed to scan packages. status: %d, stdout:%s, stderr: %s",
r.ExitStatus, r.Stdout, r.Stderr)
}
// e.g.
// curl 7.19.7-40.el6_6.4
// openldap 2.4.39-8.el6
lines := strings.Split(r.Stdout, "\n")
for _, line := range lines {
if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
name, version, err := o.parseScanedPackagesLine(trimmed)
if err != nil {
return nil, fmt.Errorf(
"Debian: Failed to parse package line: %s", line)
}
packs = append(packs, models.PackageInfo{
Name: name,
Version: version,
})
}
}
return
}
func (o *debian) parseScanedPackagesLine(line string) (name, version string, err error) {
re, _ := regexp.Compile(`^([^\t']+)\t(.+)$`)
result := re.FindStringSubmatch(line)
if len(result) == 3 {
// remove :amd64, i386...
name = regexp.MustCompile(":.+").ReplaceAllString(result[1], "")
version = result[2]
return
}
return "", "", fmt.Errorf("Unknown format: %s", line)
}
// unattended-upgrade command need to check security upgrades).
func (o *debian) checkRequiredPackagesInstalled() error {
if o.Family == "debian" {
if r := o.ssh("test -f /usr/bin/aptitude", sudo); !r.isSuccess() {
msg := "aptitude is not installed"
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
}
if !config.Conf.UseUnattendedUpgrades {
return nil
}
if r := o.ssh("type unattended-upgrade", noSudo); !r.isSuccess() {
msg := "unattended-upgrade is not installed"
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
return nil
}
//TODO return whether already expired.
func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInfo, error) {
// cmd := prependProxyEnv(conf.HTTPProxy, "apt-get update | cat; echo 1")
cmd := util.PrependProxyEnv("apt-get update")
if r := o.ssh(cmd, sudo); !r.isSuccess() {
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr,
)
}
var upgradablePackNames []string
var err error
if config.Conf.UseUnattendedUpgrades {
upgradablePackNames, err = o.GetUnsecurePackNamesUsingUnattendedUpgrades()
if err != nil {
return []CvePacksInfo{}, err
}
} else {
upgradablePackNames, err = o.GetUpgradablePackNames()
if err != nil {
return []CvePacksInfo{}, err
}
}
// Convert package name to PackageInfo struct
var unsecurePacks []models.PackageInfo
for _, name := range upgradablePackNames {
for _, pack := range packs {
if pack.Name == name {
unsecurePacks = append(unsecurePacks, pack)
break
}
}
}
unsecurePacks, err = o.fillCandidateVersion(unsecurePacks)
if err != nil {
return nil, err
}
// Collect CVE information of upgradable packages
cvePacksInfos, err := o.scanPackageCveInfos(unsecurePacks)
if err != nil {
return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err)
}
return cvePacksInfos, nil
}
func (o *debian) fillCandidateVersion(packs []models.PackageInfo) ([]models.PackageInfo, error) {
reqChan := make(chan models.PackageInfo, len(packs))
resChan := make(chan models.PackageInfo, len(packs))
errChan := make(chan error, len(packs))
defer close(resChan)
defer close(errChan)
defer close(reqChan)
go func() {
for _, pack := range packs {
reqChan <- pack
}
}()
timeout := time.After(5 * 60 * time.Second)
concurrency := 5
tasks := util.GenWorkers(concurrency)
for range packs {
tasks <- func() {
select {
case pack := <-reqChan:
func(p models.PackageInfo) {
cmd := fmt.Sprintf("apt-cache policy %s", p.Name)
r := o.ssh(cmd, sudo)
if !r.isSuccess() {
errChan <- fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
return
}
ver, err := o.parseAptCachePolicy(r.Stdout, p.Name)
if err != nil {
errChan <- fmt.Errorf("Failed to parse %s", err)
}
p.NewVersion = ver.Candidate
resChan <- p
}(pack)
}
}
}
result := []models.PackageInfo{}
for i := 0; i < len(packs); i++ {
select {
case pack := <-resChan:
result = append(result, pack)
o.log.Infof("(%d/%d) Upgradable: %s-%s -> %s",
i+1, len(packs), pack.Name, pack.Version, pack.NewVersion)
case err := <-errChan:
return nil, err
case <-timeout:
return nil, fmt.Errorf("Timeout fillCandidateVersion.")
}
}
return result, nil
}
func (o *debian) GetUnsecurePackNamesUsingUnattendedUpgrades() (packNames []string, err error) {
cmd := util.PrependProxyEnv("unattended-upgrades --dry-run -d 2>&1 ")
release, err := strconv.ParseFloat(o.Release, 64)
if err != nil {
return packNames, fmt.Errorf(
"OS Release Version is invalid, %s, %s", o.Family, o.Release)
}
switch {
case release < 12:
return packNames, fmt.Errorf(
"Support expired. %s, %s", o.Family, o.Release)
case 12 < release && release < 14:
cmd += `| grep 'pkgs that look like they should be upgraded:' |
sed -e 's/pkgs that look like they should be upgraded://g'`
case 14 < release:
cmd += `| grep 'Packages that will be upgraded:' |
sed -e 's/Packages that will be upgraded://g'`
default:
return packNames, fmt.Errorf(
"Not supported yet. %s, %s", o.Family, o.Release)
}
r := o.ssh(cmd, sudo)
if r.isSuccess(0, 1) {
packNames = strings.Split(strings.TrimSpace(r.Stdout), " ")
return packNames, nil
}
return packNames, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
func (o *debian) GetUpgradablePackNames() (packNames []string, err error) {
cmd := util.PrependProxyEnv("apt-get upgrade --dry-run")
r := o.ssh(cmd, sudo)
if r.isSuccess(0, 1) {
return o.parseAptGetUpgrade(r.Stdout)
}
return packNames, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, err error) {
startRe, _ := regexp.Compile(`The following packages will be upgraded:`)
stopRe, _ := regexp.Compile(`^(\d+) upgraded.*`)
startLineFound, stopLineFound := false, false
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if !startLineFound {
if matche := startRe.MatchString(line); matche {
startLineFound = true
}
continue
}
result := stopRe.FindStringSubmatch(line)
if len(result) == 2 {
numUpgradablePacks, err := strconv.Atoi(result[1])
if err != nil {
return nil, fmt.Errorf(
"Failed to scan upgradable packages number. line: %s", line)
}
if numUpgradablePacks != len(upgradableNames) {
return nil, fmt.Errorf(
"Failed to scan upgradable packages, expected: %s, detected: %d",
result[1], len(upgradableNames))
}
stopLineFound = true
o.log.Debugf("Found the stop line. line: %s", line)
break
}
upgradableNames = append(upgradableNames, strings.Fields(line)...)
}
if !startLineFound {
// no upgrades
return
}
if !stopLineFound {
// There are upgrades, but not found the stop line.
return nil, fmt.Errorf("Failed to scan upgradable packages")
}
return
}
func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePacksList CvePacksList, err error) {
// { CVE ID: [packageInfo] }
cvePackages := make(map[string][]models.PackageInfo)
type strarray []string
resChan := make(chan struct {
models.PackageInfo
strarray
}, len(unsecurePacks))
errChan := make(chan error, len(unsecurePacks))
reqChan := make(chan models.PackageInfo, len(unsecurePacks))
defer close(resChan)
defer close(errChan)
defer close(reqChan)
go func() {
for _, pack := range unsecurePacks {
reqChan <- pack
}
}()
timeout := time.After(30 * 60 * time.Second)
concurrency := 10
tasks := util.GenWorkers(concurrency)
for range unsecurePacks {
tasks <- func() {
select {
case pack := <-reqChan:
func(p models.PackageInfo) {
if cveIds, err := o.scanPackageCveIds(p); err != nil {
errChan <- err
} else {
resChan <- struct {
models.PackageInfo
strarray
}{p, cveIds}
}
}(pack)
}
}
}
for i := 0; i < len(unsecurePacks); i++ {
select {
case pair := <-resChan:
pack := pair.PackageInfo
cveIds := pair.strarray
for _, cveID := range cveIds {
cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack)
}
o.log.Infof("(%d/%d) Scanned %s-%s : %s",
i+1, len(unsecurePacks), pair.Name, pair.PackageInfo.Version, cveIds)
case err := <-errChan:
if err != nil {
return nil, err
}
case <-timeout:
return nil, fmt.Errorf("Timeout scanPackageCveIds.")
}
}
var cveIds []string
for k := range cvePackages {
cveIds = append(cveIds, k)
}
o.log.Debugf("%d Cves are found. cves: %v", len(cveIds), cveIds)
o.log.Info("Fetching CVE details...")
cveDetails, err := cveapi.CveClient.FetchCveDetails(cveIds)
if err != nil {
return nil, err
}
o.log.Info("Done")
for _, detail := range cveDetails {
cvePacksList = append(cvePacksList, CvePacksInfo{
CveID: detail.CveID,
CveDetail: detail,
Packs: cvePackages[detail.CveID],
// CvssScore: cinfo.CvssScore(conf.Lang),
})
}
sort.Sort(CvePacksList(cvePacksList))
return
}
func (o *debian) scanPackageCveIds(pack models.PackageInfo) (cveIds []string, err error) {
cmd := ""
switch o.Family {
case "ubuntu":
cmd = fmt.Sprintf(`apt-get changelog %s | grep '\(urgency\|CVE\)'`, pack.Name)
case "debian":
cmd = fmt.Sprintf(`aptitude changelog %s | grep '\(urgency\|CVE\)'`, pack.Name)
}
cmd = util.PrependProxyEnv(cmd)
r := o.ssh(cmd, noSudo)
if !r.isSuccess() {
o.log.Warnf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
// Ignore this Error.
return nil, nil
}
cveIds, err = o.getCveIDParsingChangelog(r.Stdout, pack.Name, pack.Version)
if err != nil {
trimUbuntu := strings.Split(pack.Version, "ubuntu")[0]
return o.getCveIDParsingChangelog(r.Stdout, pack.Name, trimUbuntu)
}
return
}
func (o *debian) getCveIDParsingChangelog(changelog string,
packName string, versionOrLater string) (cveIDs []string, err error) {
cveIDs, err = o.parseChangelog(changelog, packName, versionOrLater)
if err == nil {
return
}
ver := strings.Split(versionOrLater, "ubuntu")[0]
cveIDs, err = o.parseChangelog(changelog, packName, ver)
if err == nil {
return
}
splittedByColon := strings.Split(versionOrLater, ":")
if 1 < len(splittedByColon) {
ver = splittedByColon[1]
}
cveIDs, err = o.parseChangelog(changelog, packName, ver)
if err == nil {
return
}
//TODO report as unable to parse changelog.
o.log.Warn(err)
return []string{}, nil
}
// Collect CVE-IDs included in the changelog.
// The version which specified in argument(versionOrLater) is excluded.
func (o *debian) parseChangelog(changelog string,
packName string, versionOrLater string) (cveIDs []string, err error) {
cveRe, _ := regexp.Compile(`(CVE-\d{4}-\d{4})`)
stopRe, _ := regexp.Compile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(versionOrLater)))
stopLineFound := false
lines := strings.Split(changelog, "\n")
for _, line := range lines {
if matche := stopRe.MatchString(line); matche {
o.log.Debugf("Found the stop line. line: %s", line)
stopLineFound = true
break
} else if matches := cveRe.FindAllString(line, -1); len(matches) > 0 {
for _, m := range matches {
cveIDs = util.AppendIfMissing(cveIDs, m)
}
}
}
if !stopLineFound {
return []string{}, fmt.Errorf(
"Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s",
packName,
versionOrLater,
)
}
return
}
type packCandidateVer struct {
Name string
Installed string
Candidate string
}
// parseAptCachePolicy the stdout of parse pat-get cache policy
func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, error) {
ver := packCandidateVer{Name: name}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) != 2 {
continue
}
switch fields[0] {
case "Installed:":
ver.Installed = fields[1]
case "Candidate:":
ver.Candidate = fields[1]
return ver, nil
default:
// nop
}
}
return ver, fmt.Errorf("Unknown Format: %s", stdout)
}
func appendPackIfMissing(slice []models.PackageInfo, s models.PackageInfo) []models.PackageInfo {
for _, ele := range slice {
if ele.Name == s.Name &&
ele.Version == s.Version &&
ele.Release == s.Release {
return slice
}
}
return append(slice, s)
}

601
scan/debian_test.go Normal file
View File

@@ -0,0 +1,601 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"reflect"
"testing"
"github.com/future-architect/vuls/config"
"github.com/k0kubun/pp"
)
func TestParseScanedPackagesLineDebian(t *testing.T) {
var packagetests = []struct {
in string
name string
version string
}{
{"base-passwd 3.5.33", "base-passwd", "3.5.33"},
{"bzip2 1.0.6-5", "bzip2", "1.0.6-5"},
{"adduser 3.113+nmu3ubuntu3", "adduser", "3.113+nmu3ubuntu3"},
{"bash 4.3-7ubuntu1.5", "bash", "4.3-7ubuntu1.5"},
{"bsdutils 1:2.20.1-5.1ubuntu20.4", "bsdutils", "1:2.20.1-5.1ubuntu20.4"},
{"ca-certificates 20141019ubuntu0.14.04.1", "ca-certificates", "20141019ubuntu0.14.04.1"},
{"apt 1.0.1ubuntu2.8", "apt", "1.0.1ubuntu2.8"},
}
d := newDebian(config.ServerInfo{})
for _, tt := range packagetests {
n, v, _ := d.parseScanedPackagesLine(tt.in)
if n != tt.name {
t.Errorf("name: expected %s, actual %s", tt.name, n)
}
if v != tt.version {
t.Errorf("version: expected %s, actual %s", tt.version, v)
}
}
}
func TestgetCveIDParsingChangelog(t *testing.T) {
var tests = []struct {
in []string
expected []string
}{
{
// verubuntu1
[]string{
"systemd",
"228-4ubuntu1",
`systemd (229-2) unstable; urgency=medium
systemd (229-1) unstable; urgency=medium
systemd (228-6) unstable; urgency=medium
CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
CVE-2015-3210: heap buffer overflow in pcre_compile2() /
systemd (228-5) unstable; urgency=medium
systemd (228-4) unstable; urgency=medium
systemd (228-3) unstable; urgency=medium
systemd (228-2) unstable; urgency=medium
systemd (228-1) unstable; urgency=medium
systemd (227-3) unstable; urgency=medium
systemd (227-2) unstable; urgency=medium
systemd (227-1) unstable; urgency=medium`,
},
[]string{
"CVE-2015-2325",
"CVE-2015-2326",
"CVE-2015-3210",
},
},
{
// ver
[]string{
"libpcre3",
"2:8.38-1ubuntu1",
`pcre3 (2:8.38-2) unstable; urgency=low
pcre3 (2:8.38-1) unstable; urgency=low
pcre3 (2:8.35-8) unstable; urgency=low
pcre3 (2:8.35-7.4) unstable; urgency=medium
pcre3 (2:8.35-7.3) unstable; urgency=medium
pcre3 (2:8.35-7.2) unstable; urgency=low
CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
CVE-2015-3210: heap buffer overflow in pcre_compile2() /
pcre3 (2:8.35-7.1) unstable; urgency=medium
pcre3 (2:8.35-7) unstable; urgency=medium`,
},
[]string{
"CVE-2015-2325",
"CVE-2015-2326",
"CVE-2015-3210",
},
},
{
// ver-ubuntu3
[]string{
"sysvinit",
"2.88dsf-59.2ubuntu3",
`sysvinit (2.88dsf-59.3ubuntu1) xenial; urgency=low
sysvinit (2.88dsf-59.3) unstable; urgency=medium
CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
CVE-2015-3210: heap buffer overflow in pcre_compile2() /
sysvinit (2.88dsf-59.2ubuntu3) xenial; urgency=medium
sysvinit (2.88dsf-59.2ubuntu2) wily; urgency=medium
sysvinit (2.88dsf-59.2ubuntu1) wily; urgency=medium
CVE-2015-2321: heap buffer overflow in pcre_compile2(). (Closes: #783285)
sysvinit (2.88dsf-59.2) unstable; urgency=medium
sysvinit (2.88dsf-59.1ubuntu3) wily; urgency=medium
CVE-2015-2322: heap buffer overflow in pcre_compile2(). (Closes: #783285)
sysvinit (2.88dsf-59.1ubuntu2) wily; urgency=medium
sysvinit (2.88dsf-59.1ubuntu1) wily; urgency=medium
sysvinit (2.88dsf-59.1) unstable; urgency=medium
CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
sysvinit (2.88dsf-59) unstable; urgency=medium
sysvinit (2.88dsf-58) unstable; urgency=low
sysvinit (2.88dsf-57) unstable; urgency=low`,
},
[]string{
"CVE-2015-2325",
"CVE-2015-2326",
"CVE-2015-3210",
},
},
{
// 1:ver-ubuntu3
[]string{
"bsdutils",
"1:2.27.1-1ubuntu3",
` util-linux (2.27.1-3ubuntu1) xenial; urgency=medium
util-linux (2.27.1-3) unstable; urgency=medium
CVE-2015-2325: heap buffer overflow in compile_branch(). (Closes: #781795)
CVE-2015-2326: heap buffer overflow in pcre_compile2(). (Closes: #783285)
CVE-2015-3210: heap buffer overflow in pcre_compile2() /
util-linux (2.27.1-2) unstable; urgency=medium
util-linux (2.27.1-1ubuntu4) xenial; urgency=medium
util-linux (2.27.1-1ubuntu3) xenial; urgency=medium
util-linux (2.27.1-1ubuntu2) xenial; urgency=medium
util-linux (2.27.1-1ubuntu1) xenial; urgency=medium
util-linux (2.27.1-1) unstable; urgency=medium
util-linux (2.27-3ubuntu1) xenial; urgency=medium
util-linux (2.27-3) unstable; urgency=medium
util-linux (2.27-2) unstable; urgency=medium
util-linux (2.27-1) unstable; urgency=medium
util-linux (2.27~rc2-2) experimental; urgency=medium
util-linux (2.27~rc2-1) experimental; urgency=medium
util-linux (2.27~rc1-1) experimental; urgency=medium
util-linux (2.26.2-9) unstable; urgency=medium
util-linux (2.26.2-8) experimental; urgency=medium
util-linux (2.26.2-7) experimental; urgency=medium
util-linux (2.26.2-6ubuntu3) wily; urgency=medium
CVE-2015-2329: heap buffer overflow in compile_branch(). (Closes: #781795)
util-linux (2.26.2-6ubuntu2) wily; urgency=medium
util-linux (2.26.2-6ubuntu1) wily; urgency=medium
util-linux (2.26.2-6) unstable; urgency=medium`,
},
[]string{
"CVE-2015-2325",
"CVE-2015-2326",
"CVE-2015-3210",
},
},
}
d := newDebian(config.ServerInfo{})
for _, tt := range tests {
actual, _ := d.getCveIDParsingChangelog(tt.in[2], tt.in[0], tt.in[1])
if len(actual) != len(tt.expected) {
t.Errorf("Len of return array are'nt same. expected %#v, actual %#v", tt.expected, actual)
continue
}
for i := range tt.expected {
if actual[i] != tt.expected[i] {
t.Errorf("expected %s, actual %s", tt.expected[i], actual[i])
}
}
}
for _, tt := range tests {
_, err := d.getCveIDParsingChangelog(tt.in[2], tt.in[0], "version number do'nt match case")
if err != nil {
t.Errorf("Returning error is unexpected.")
}
}
}
func TestGetUpdatablePackNames(t *testing.T) {
var tests = []struct {
in string
expected []string
}{
{ // Ubuntu 12.04
`Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be upgraded:
apt ca-certificates cpio dpkg e2fslibs e2fsprogs gnupg gpgv libc-bin libc6 libcomerr2 libpcre3
libpng12-0 libss2 libssl1.0.0 libudev0 multiarch-support openssl tzdata udev upstart
21 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst dpkg [1.16.1.2ubuntu7.5] (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64])
Conf dpkg (1.16.1.2ubuntu7.7 Ubuntu:12.04/precise-updates [amd64])
Inst upstart [1.5-0ubuntu7.2] (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64])
Inst libc-bin [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ]
Conf libc-bin (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64]) [libc6:amd64 ]
Inst libc6 [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
Conf libc6 (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
Inst libudev0 [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
Inst tzdata [2015a-0ubuntu0.12.04] (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all])
Conf tzdata (2015g-0ubuntu0.12.04 Ubuntu:12.04/precise-updates [all])
Inst e2fslibs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ]
Conf e2fslibs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64]) [e2fsprogs:amd64 ]
Inst e2fsprogs [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Conf e2fsprogs (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Inst gpgv [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
Conf gpgv (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
Inst gnupg [1.4.11-3ubuntu2.7] (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
Conf gnupg (1.4.11-3ubuntu2.9 Ubuntu:12.04/precise-updates [amd64])
Inst apt [0.8.16~exp12ubuntu10.22] (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64])
Conf apt (0.8.16~exp12ubuntu10.26 Ubuntu:12.04/precise-updates [amd64])
Inst libcomerr2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Conf libcomerr2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Inst libss2 [1.42-1ubuntu2] (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Conf libss2 (1.42-1ubuntu2.3 Ubuntu:12.04/precise-updates [amd64])
Inst libssl1.0.0 [1.0.1-4ubuntu5.21] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
Conf libssl1.0.0 (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
Inst libpcre3 [8.12-4] (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64])
Inst libpng12-0 [1.2.46-3ubuntu4] (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64])
Inst multiarch-support [2.15-0ubuntu10.10] (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
Conf multiarch-support (2.15-0ubuntu10.13 Ubuntu:12.04/precise-updates [amd64])
Inst cpio [2.11-7ubuntu3.1] (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64])
Inst udev [175-0ubuntu9.9] (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
Inst openssl [1.0.1-4ubuntu5.33] (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
Inst ca-certificates [20141019ubuntu0.12.04.1] (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all])
Conf libudev0 (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
Conf upstart (1.5-0ubuntu7.3 Ubuntu:12.04/precise-updates [amd64])
Conf libpcre3 (8.12-4ubuntu0.1 Ubuntu:12.04/precise-updates [amd64])
Conf libpng12-0 (1.2.46-3ubuntu4.2 Ubuntu:12.04/precise-updates [amd64])
Conf cpio (2.11-7ubuntu3.2 Ubuntu:12.04/precise-updates [amd64])
Conf udev (175-0ubuntu9.10 Ubuntu:12.04/precise-updates [amd64])
Conf openssl (1.0.1-4ubuntu5.34 Ubuntu:12.04/precise-updates [amd64])
Conf ca-certificates (20160104ubuntu0.12.04.1 Ubuntu:12.04/precise-updates [all])`,
[]string{
"apt",
"ca-certificates",
"cpio",
"dpkg",
"e2fslibs",
"e2fsprogs",
"gnupg",
"gpgv",
"libc-bin",
"libc6",
"libcomerr2",
"libpcre3",
"libpng12-0",
"libss2",
"libssl1.0.0",
"libudev0",
"multiarch-support",
"openssl",
"tzdata",
"udev",
"upstart",
},
},
{ // Ubuntu 14.04
`Reading package lists... Done
Building dependency tree
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
apt apt-utils base-files bsdutils coreutils cpio dh-python dpkg e2fslibs
e2fsprogs gcc-4.8-base gcc-4.9-base gnupg gpgv ifupdown initscripts iproute2
isc-dhcp-client isc-dhcp-common libapt-inst1.5 libapt-pkg4.12 libblkid1
libc-bin libc6 libcgmanager0 libcomerr2 libdrm2 libexpat1 libffi6 libgcc1
libgcrypt11 libgnutls-openssl27 libgnutls26 libmount1 libpcre3 libpng12-0
libpython3.4-minimal libpython3.4-stdlib libsqlite3-0 libss2 libssl1.0.0
libstdc++6 libtasn1-6 libudev1 libuuid1 login mount multiarch-support
ntpdate passwd python3.4 python3.4-minimal rsyslog sudo sysv-rc
sysvinit-utils tzdata udev util-linux
59 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst base-files [7.2ubuntu5.2] (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64])
Conf base-files (7.2ubuntu5.4 Ubuntu:14.04/trusty-updates [amd64])
Inst coreutils [8.21-1ubuntu5.1] (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64])
Conf coreutils (8.21-1ubuntu5.3 Ubuntu:14.04/trusty-updates [amd64])
Inst dpkg [1.17.5ubuntu5.3] (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64])
Conf dpkg (1.17.5ubuntu5.5 Ubuntu:14.04/trusty-updates [amd64])
Inst libc-bin [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Inst libc6 [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Inst libgcc1 [1:4.9.1-0ubuntu1] (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64]) []
Inst gcc-4.9-base [4.9.1-0ubuntu1] (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
Conf gcc-4.9-base (4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
Conf libgcc1 (1:4.9.3-0ubuntu4 Ubuntu:14.04/trusty-updates [amd64])
Conf libc6 (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Conf libc-bin (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Inst e2fslibs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 on e2fslibs:amd64] [e2fsprogs:amd64 ]
Conf e2fslibs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64]) [e2fsprogs:amd64 ]
Inst e2fsprogs [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Conf e2fsprogs (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Inst login [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
Conf login (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
Inst mount [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf mount (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst tzdata [2015a-0ubuntu0.14.04] (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all])
Conf tzdata (2015g-0ubuntu0.14.04 Ubuntu:14.04/trusty-updates [all])
Inst sysvinit-utils [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
Inst sysv-rc [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all])
Conf sysv-rc (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [all])
Conf sysvinit-utils (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
Inst util-linux [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf util-linux (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst gcc-4.8-base [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ]
Conf gcc-4.8-base (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64]) [libstdc++6:amd64 ]
Inst libstdc++6 [4.8.2-19ubuntu1] (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libstdc++6 (4.8.4-2ubuntu1~14.04.1 Ubuntu:14.04/trusty-updates [amd64])
Inst libapt-pkg4.12 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Conf libapt-pkg4.12 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Inst gpgv [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
Conf gpgv (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
Inst gnupg [1.4.16-1ubuntu2.1] (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
Conf gnupg (1.4.16-1ubuntu2.3 Ubuntu:14.04/trusty-updates [amd64])
Inst apt [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Conf apt (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Inst bsdutils [1:2.20.1-5.1ubuntu20.4] (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf bsdutils (1:2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst passwd [1:4.1.5.1-1ubuntu9] (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
Conf passwd (1:4.1.5.1-1ubuntu9.2 Ubuntu:14.04/trusty-updates [amd64])
Inst libuuid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf libuuid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst libblkid1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf libblkid1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst libcomerr2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libcomerr2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Inst libmount1 [2.20.1-5.1ubuntu20.4] (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Conf libmount1 (2.20.1-5.1ubuntu20.7 Ubuntu:14.04/trusty-updates [amd64])
Inst libpcre3 [1:8.31-2ubuntu2] (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libpcre3 (1:8.31-2ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
Inst libss2 [1.42.9-3ubuntu1] (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libss2 (1.42.9-3ubuntu1.3 Ubuntu:14.04/trusty-updates [amd64])
Inst libapt-inst1.5 [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Inst libexpat1 [2.1.0-4ubuntu1] (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64])
Inst libffi6 [3.1~rc1+r3.0.13-12] (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64])
Inst libgcrypt11 [1.5.3-2ubuntu4.1] (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
Inst libtasn1-6 [3.4-3ubuntu0.1] (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64])
Inst libgnutls-openssl27 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64]) []
Inst libgnutls26 [2.12.23-12ubuntu2.1] (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
Inst libsqlite3-0 [3.8.2-1ubuntu2] (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
Inst python3.4 [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
Inst libpython3.4-stdlib [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
Inst python3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64]) []
Inst libssl1.0.0 [1.0.1f-1ubuntu2.8] (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64]) []
Inst libpython3.4-minimal [3.4.0-2ubuntu1] (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
Inst ntpdate [1:4.2.6.p5+dfsg-3ubuntu2.14.04.2] (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64])
Inst libdrm2 [2.4.56-1~ubuntu2] (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64])
Inst libpng12-0 [1.2.50-1ubuntu2] (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64])
Inst initscripts [2.88dsf-41ubuntu6] (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
Inst libcgmanager0 [0.24-0ubuntu7.3] (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64])
Inst udev [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64]) []
Inst libudev1 [204-5ubuntu20.10] (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
Inst multiarch-support [2.19-0ubuntu6.5] (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Conf multiarch-support (2.19-0ubuntu6.7 Ubuntu:14.04/trusty-updates [amd64])
Inst apt-utils [1.0.1ubuntu2.6] (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Inst dh-python [1.20140128-1ubuntu8] (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all])
Inst iproute2 [3.12.0-2] (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64])
Inst ifupdown [0.7.47.2ubuntu4.1] (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
Inst isc-dhcp-client [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64]) []
Inst isc-dhcp-common [4.2.4-7ubuntu12] (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
Inst rsyslog [7.4.4-1ubuntu2.5] (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64])
Inst sudo [1.8.9p5-1ubuntu1] (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
Inst cpio [2.11+dfsg-1ubuntu1.1] (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
Conf libapt-inst1.5 (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Conf libexpat1 (2.1.0-4ubuntu1.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libffi6 (3.1~rc1+r3.0.13-12ubuntu0.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libgcrypt11 (1.5.3-2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libtasn1-6 (3.4-3ubuntu0.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libgnutls26 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
Conf libgnutls-openssl27 (2.12.23-12ubuntu2.4 Ubuntu:14.04/trusty-updates [amd64])
Conf libsqlite3-0 (3.8.2-1ubuntu2.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libssl1.0.0 (1.0.1f-1ubuntu2.16 Ubuntu:14.04/trusty-updates [amd64])
Conf libpython3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
Conf python3.4-minimal (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libpython3.4-stdlib (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
Conf python3.4 (3.4.3-1ubuntu1~14.04.3 Ubuntu:14.04/trusty-updates [amd64])
Conf ntpdate (1:4.2.6.p5+dfsg-3ubuntu2.14.04.8 Ubuntu:14.04/trusty-updates [amd64])
Conf libdrm2 (2.4.64-1~ubuntu14.04.1 Ubuntu:14.04/trusty-updates [amd64])
Conf libpng12-0 (1.2.50-1ubuntu2.14.04.2 Ubuntu:14.04/trusty-updates [amd64])
Conf initscripts (2.88dsf-41ubuntu6.3 Ubuntu:14.04/trusty-updates [amd64])
Conf libcgmanager0 (0.24-0ubuntu7.5 Ubuntu:14.04/trusty-updates [amd64])
Conf libudev1 (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
Conf udev (204-5ubuntu20.18 Ubuntu:14.04/trusty-updates [amd64])
Conf apt-utils (1.0.1ubuntu2.11 Ubuntu:14.04/trusty-updates [amd64])
Conf dh-python (1.20140128-1ubuntu8.2 Ubuntu:14.04/trusty-updates [all])
Conf iproute2 (3.12.0-2ubuntu1 Ubuntu:14.04/trusty-updates [amd64])
Conf ifupdown (0.7.47.2ubuntu4.3 Ubuntu:14.04/trusty-updates [amd64])
Conf isc-dhcp-common (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
Conf isc-dhcp-client (4.2.4-7ubuntu12.4 Ubuntu:14.04/trusty-updates [amd64])
Conf rsyslog (7.4.4-1ubuntu2.6 Ubuntu:14.04/trusty-updates [amd64])
Conf sudo (1.8.9p5-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
Conf cpio (2.11+dfsg-1ubuntu1.2 Ubuntu:14.04/trusty-updates [amd64])
`,
[]string{
"apt",
"apt-utils",
"base-files",
"bsdutils",
"coreutils",
"cpio",
"dh-python",
"dpkg",
"e2fslibs",
"e2fsprogs",
"gcc-4.8-base",
"gcc-4.9-base",
"gnupg",
"gpgv",
"ifupdown",
"initscripts",
"iproute2",
"isc-dhcp-client",
"isc-dhcp-common",
"libapt-inst1.5",
"libapt-pkg4.12",
"libblkid1",
"libc-bin",
"libc6",
"libcgmanager0",
"libcomerr2",
"libdrm2",
"libexpat1",
"libffi6",
"libgcc1",
"libgcrypt11",
"libgnutls-openssl27",
"libgnutls26",
"libmount1",
"libpcre3",
"libpng12-0",
"libpython3.4-minimal",
"libpython3.4-stdlib",
"libsqlite3-0",
"libss2",
"libssl1.0.0",
"libstdc++6",
"libtasn1-6",
"libudev1",
"libuuid1",
"login",
"mount",
"multiarch-support",
"ntpdate",
"passwd",
"python3.4",
"python3.4-minimal",
"rsyslog",
"sudo",
"sysv-rc",
"sysvinit-utils",
"tzdata",
"udev",
"util-linux",
},
},
{
//Ubuntu12.04
`Reading package lists... Done
Building dependency tree
Reading state information... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.`,
[]string{},
},
{
//Ubuntu14.04
`Reading package lists... Done
Building dependency tree
Reading state information... Done
Calculating upgrade... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.`,
[]string{},
},
}
d := newDebian(config.ServerInfo{})
for _, tt := range tests {
actual, err := d.parseAptGetUpgrade(tt.in)
if err != nil {
t.Errorf("Returning error is unexpected.")
}
if len(tt.expected) != len(actual) {
t.Errorf("Result length is not as same as expected. expected: %d, actual: %d", len(tt.expected), len(actual))
pp.Println(tt.expected)
pp.Println(actual)
return
}
for i := range tt.expected {
if tt.expected[i] != actual[i] {
t.Errorf("[%d] expected %s, actual %s", i, tt.expected[i], actual[i])
}
}
}
}
func TestParseAptCachePolicy(t *testing.T) {
var tests = []struct {
stdout string
name string
expected packCandidateVer
}{
{
// Ubuntu 16.04
`openssl:
Installed: 1.0.2f-2ubuntu1
Candidate: 1.0.2g-1ubuntu2
Version table:
1.0.2g-1ubuntu2 500
500 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages
*** 1.0.2f-2ubuntu1 100
100 /var/lib/dpkg/status`,
"openssl",
packCandidateVer{
Name: "openssl",
Installed: "1.0.2f-2ubuntu1",
Candidate: "1.0.2g-1ubuntu2",
},
},
{
// Ubuntu 14.04
`openssl:
Installed: 1.0.1f-1ubuntu2.16
Candidate: 1.0.1f-1ubuntu2.17
Version table:
1.0.1f-1ubuntu2.17 0
500 http://archive.ubuntu.com/ubuntu/ trusty-updates/main amd64 Packages
500 http://archive.ubuntu.com/ubuntu/ trusty-security/main amd64 Packages
*** 1.0.1f-1ubuntu2.16 0
100 /var/lib/dpkg/status
1.0.1f-1ubuntu2 0
500 http://archive.ubuntu.com/ubuntu/ trusty/main amd64 Packages`,
"openssl",
packCandidateVer{
Name: "openssl",
Installed: "1.0.1f-1ubuntu2.16",
Candidate: "1.0.1f-1ubuntu2.17",
},
},
{
// Ubuntu 12.04
`openssl:
Installed: 1.0.1-4ubuntu5.33
Candidate: 1.0.1-4ubuntu5.34
Version table:
1.0.1-4ubuntu5.34 0
500 http://archive.ubuntu.com/ubuntu/ precise-updates/main amd64 Packages
500 http://archive.ubuntu.com/ubuntu/ precise-security/main amd64 Packages
*** 1.0.1-4ubuntu5.33 0
100 /var/lib/dpkg/status
1.0.1-4ubuntu3 0
500 http://archive.ubuntu.com/ubuntu/ precise/main amd64 Packages`,
"openssl",
packCandidateVer{
Name: "openssl",
Installed: "1.0.1-4ubuntu5.33",
Candidate: "1.0.1-4ubuntu5.34",
},
},
}
d := newDebian(config.ServerInfo{})
for _, tt := range tests {
actual, err := d.parseAptCachePolicy(tt.stdout, tt.name)
if err != nil {
t.Errorf("Error has occurred: %s, actual: %#v", err, actual)
}
if !reflect.DeepEqual(tt.expected, actual) {
e := pp.Sprintf("%v", tt.expected)
a := pp.Sprintf("%v", actual)
t.Errorf("expected %s, actual %s", e, a)
}
}
}

129
scan/linux.go Normal file
View File

@@ -0,0 +1,129 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"sort"
"github.com/Sirupsen/logrus"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/cveapi"
"github.com/future-architect/vuls/models"
)
type linux struct {
ServerInfo config.ServerInfo
Family string
Release string
osPackages
log *logrus.Entry
}
func (l *linux) ssh(cmd string, sudo bool) sshResult {
return sshExec(l.ServerInfo, cmd, sudo, l.log)
}
func (l *linux) setServerInfo(c config.ServerInfo) {
l.ServerInfo = c
}
func (l *linux) getServerInfo() config.ServerInfo {
return l.ServerInfo
}
func (l *linux) setDistributionInfo(fam, rel string) {
l.Family = fam
l.Release = rel
}
func (l *linux) convertToModel() (models.ScanResult, error) {
var cves, unknownScoreCves []models.CveInfo
for _, p := range l.UnsecurePackages {
if p.CveDetail.CvssScore(config.Conf.Lang) < 0 {
unknownScoreCves = append(unknownScoreCves, models.CveInfo{
CveDetail: p.CveDetail,
Packages: p.Packs,
DistroAdvisories: p.DistroAdvisories, // only Amazon Linux
})
continue
}
cpenames := []models.CpeName{}
for _, cpename := range p.CpeNames {
cpenames = append(cpenames,
models.CpeName{Name: cpename})
}
cve := models.CveInfo{
CveDetail: p.CveDetail,
Packages: p.Packs,
DistroAdvisories: p.DistroAdvisories, // only Amazon Linux
CpeNames: cpenames,
}
cves = append(cves, cve)
}
return models.ScanResult{
ServerName: l.ServerInfo.ServerName,
Family: l.Family,
Release: l.Release,
KnownCves: cves,
UnknownCves: unknownScoreCves,
}, nil
}
// scanVulnByCpeName search vulnerabilities that specified in config file.
func (l *linux) scanVulnByCpeName() error {
unsecurePacks := CvePacksList{}
serverInfo := l.getServerInfo()
cpeNames := serverInfo.CpeNames
// remove duplicate
set := map[string]CvePacksInfo{}
for _, name := range cpeNames {
details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name)
if err != nil {
return err
}
for _, detail := range details {
if val, ok := set[detail.CveID]; ok {
names := val.CpeNames
names = append(names, name)
val.CpeNames = names
set[detail.CveID] = val
} else {
set[detail.CveID] = CvePacksInfo{
CveID: detail.CveID,
CveDetail: detail,
CpeNames: []string{name},
}
}
}
}
for key := range set {
unsecurePacks = append(unsecurePacks, set[key])
}
unsecurePacks = append(unsecurePacks, l.UnsecurePackages...)
sort.Sort(CvePacksList(unsecurePacks))
l.setUnsecurePackages(unsecurePacks)
return nil
}

18
scan/linux_test.go Normal file
View File

@@ -0,0 +1,18 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan

861
scan/redhat.go Normal file
View File

@@ -0,0 +1,861 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/cveapi"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/k0kubun/pp"
)
// inherit OsTypeInterface
type redhat struct {
linux
}
// NewRedhat is constructor
func newRedhat(c config.ServerInfo) *redhat {
r := &redhat{}
r.log = util.NewCustomLogger(c)
return r
}
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/redhat.rb
func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
red = newRedhat(c)
// set sudo option flag
c.SudoOpt = config.SudoOption{ExecBySudoSh: true}
red.setServerInfo(c)
if r := sshExec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() {
red.setDistributionInfo("fedora", "unknown")
Log.Warn("Fedora not tested yet. Host: %s:%s", c.Host, c.Port)
return true, red
}
if r := sshExec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() {
// https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/
// e.g.
// $ cat /etc/redhat-release
// CentOS release 6.5 (Final)
if r := sshExec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() {
re, _ := regexp.Compile(`(.*) release (\d[\d.]*)`)
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
if len(result) != 3 {
Log.Warn(
"Failed to parse RedHat/CentOS version. stdout: %s, Host: %s:%s",
r.Stdout, c.Host, c.Port)
return true, red
}
release := result[2]
switch strings.ToLower(result[1]) {
case "centos", "centos linux":
red.setDistributionInfo("centos", release)
default:
red.setDistributionInfo("rhel", release)
}
return true, red
}
return true, red
}
if r := sshExec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
family := "amazon"
release := "unknown"
if r := sshExec(c, "cat /etc/system-release", noSudo); r.isSuccess() {
fields := strings.Fields(r.Stdout)
if len(fields) == 5 {
release = fields[4]
}
}
red.setDistributionInfo(family, release)
return true, red
}
Log.Debugf("Not RedHat like Linux. Host: %s:%s", c.Host, c.Port)
return false, red
}
// CentOS 5 ... yum-plugin-security, yum-changelog
// CentOS 6 ... yum-plugin-security, yum-plugin-changelog
// CentOS 7 ... yum-plugin-security, yum-plugin-changelog
// RHEL, Amazon ... no additinal packages needed
func (o *redhat) install() error {
switch o.Family {
case "rhel", "amazon":
o.log.Infof("Nothing to do")
return nil
}
if err := o.installYumPluginSecurity(); err != nil {
return err
}
return o.installYumChangelog()
}
func (o *redhat) installYumPluginSecurity() error {
if r := o.ssh("rpm -q yum-plugin-security", noSudo); r.isSuccess() {
o.log.Infof("Ignored: yum-plugin-security already installed")
return nil
}
cmd := util.PrependProxyEnv("yum install -y yum-plugin-security")
if r := o.ssh(cmd, sudo); !r.isSuccess() {
return fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
return nil
}
func (o *redhat) installYumChangelog() error {
o.log.Info("Installing yum-plugin-security...")
if o.Family == "centos" {
var majorVersion int
if 0 < len(o.Release) {
majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0])
} else {
return fmt.Errorf(
"Not implemented yet. family: %s, release: %s",
o.Family, o.Release)
}
var packName = ""
if majorVersion < 6 {
packName = "yum-changelog"
} else {
packName = "yum-plugin-changelog"
}
cmd := "rpm -q " + packName
if r := o.ssh(cmd, noSudo); r.isSuccess() {
o.log.Infof("Ignored: %s already installed.", packName)
return nil
}
cmd = util.PrependProxyEnv("yum install -y " + packName)
if r := o.ssh(cmd, sudo); !r.isSuccess() {
return fmt.Errorf(
"Failed to install %s. status: %d, stdout: %s, stderr: %s",
packName, r.ExitStatus, r.Stdout, r.Stderr)
}
o.log.Infof("Installed: %s.", packName)
}
return nil
}
func (o *redhat) checkRequiredPackagesInstalled() error {
if config.Conf.UseYumPluginSecurity {
// check if yum-plugin-security is installed.
// Amazon Linux, REHL can execute 'yum updateinfo --security updates' without yum-plugin-security
cmd := "rpm -q yum-plugin-security"
if o.Family == "centos" {
if r := o.ssh(cmd, noSudo); !r.isSuccess() {
msg := "yum-plugin-security is not installed"
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
}
return nil
}
if o.Family == "centos" {
var majorVersion int
if 0 < len(o.Release) {
majorVersion, _ = strconv.Atoi(strings.Split(o.Release, ".")[0])
} else {
msg := fmt.Sprintf("Not implemented yet. family: %s, release: %s", o.Family, o.Release)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
var packName = ""
if majorVersion < 6 {
packName = "yum-changelog"
} else {
packName = "yum-plugin-changelog"
}
cmd := "rpm -q " + packName
if r := o.ssh(cmd, noSudo); !r.isSuccess() {
msg := fmt.Sprintf("%s is not installed", packName)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
}
return nil
}
func (o *redhat) scanPackages() error {
var err error
var packs []models.PackageInfo
if packs, err = o.scanInstalledPackages(); err != nil {
o.log.Errorf("Failed to scan installed packages")
return err
}
o.setPackages(packs)
var unsecurePacks []CvePacksInfo
if unsecurePacks, err = o.scanUnsecurePackages(); err != nil {
o.log.Errorf("Failed to scan valnerable packages")
return err
}
o.setUnsecurePackages(unsecurePacks)
return nil
}
func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) {
cmd := "rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\n'"
r := o.ssh(cmd, noSudo)
if r.isSuccess() {
// e.g.
// openssl 1.0.1e 30.el6.11
lines := strings.Split(r.Stdout, "\n")
for _, line := range lines {
if trimed := strings.TrimSpace(line); len(trimed) != 0 {
var packinfo models.PackageInfo
if packinfo, err = o.parseScanedPackagesLine(line); err != nil {
return
}
installedPackages = append(installedPackages, packinfo)
}
}
return
}
return installedPackages, fmt.Errorf(
"Scan packages failed. status: %d, stdout: %s, stderr: %s",
r.ExitStatus, r.Stdout, r.Stderr)
}
func (o *redhat) parseScanedPackagesLine(line string) (pack models.PackageInfo, err error) {
re, _ := regexp.Compile(`^([^\t']+)\t([^\t]+)\t(.+)$`)
result := re.FindStringSubmatch(line)
if len(result) == 4 {
pack.Name = result[1]
pack.Version = result[2]
pack.Release = strings.TrimSpace(result[3])
} else {
err = fmt.Errorf("redhat: Failed to parse package line: %s", line)
}
return
}
func (o *redhat) scanUnsecurePackages() ([]CvePacksInfo, error) {
if o.Family != "centos" || config.Conf.UseYumPluginSecurity {
// Amazon, RHEL has yum updateinfo as default
// yum updateinfo can collenct vendor advisory information.
return o.scanUnsecurePackagesUsingYumPluginSecurity()
}
// CentOS does not have security channel...
// So, yum check-update then parse chnagelog.
return o.scanUnsecurePackagesUsingYumCheckUpdate()
}
//TODO return whether already expired.
func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (CvePacksList, error) {
cmd := "yum check-update"
r := o.ssh(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess(0, 100) {
//returns an exit code of 100 if there are available updates.
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
// get Updateble package name, installed, candidate version.
packInfoList, err := o.parseYumCheckUpdateLines(r.Stdout)
if err != nil {
return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
}
o.log.Debugf("%s", pp.Sprintf("%s", packInfoList))
// Collect CVE-IDs in changelog
type PackInfoCveIDs struct {
PackInfo models.PackageInfo
CveIDs []string
}
var results []PackInfoCveIDs
for i, packInfo := range packInfoList {
changelog, err := o.getChangelog(packInfo.Name)
if err != nil {
o.log.Errorf("Failed to collect CVE. err: %s", err)
return nil, err
}
// Collect unique set of CVE-ID in each changelog
uniqueCveIDMap := make(map[string]bool)
lines := strings.Split(changelog, "\n")
for _, line := range lines {
cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
for _, c := range cveIDs {
uniqueCveIDMap[c] = true
}
}
// keys
var cveIDs []string
for k := range uniqueCveIDMap {
cveIDs = append(cveIDs, k)
}
p := PackInfoCveIDs{
PackInfo: packInfo,
CveIDs: cveIDs,
}
results = append(results, p)
o.log.Infof("(%d/%d) Scanned %s-%s-%s -> %s-%s : %s",
i+1,
len(packInfoList),
p.PackInfo.Name,
p.PackInfo.Version,
p.PackInfo.Release,
p.PackInfo.NewVersion,
p.PackInfo.NewRelease,
p.CveIDs)
}
// transform datastructure
// - From
// [
// {
// PackInfo: models.PackageInfo,
// CveIDs: []string,
// },
// ]
// - To
// map {
// CveID: []models.PackageInfo
// }
cveIDPackInfoMap := make(map[string][]models.PackageInfo)
for _, res := range results {
for _, cveID := range res.CveIDs {
// packInfo, found := o.Packages.FindByName(res.Packname)
// if !found {
// return CvePacksList{}, fmt.Errorf(
// "Faild to transform data structure: %v", res.Packname)
// }
cveIDPackInfoMap[cveID] = append(cveIDPackInfoMap[cveID], res.PackInfo)
}
}
var uniqueCveIDs []string
for cveID := range cveIDPackInfoMap {
uniqueCveIDs = append(uniqueCveIDs, cveID)
}
// cveIDs => []cve.CveInfo
o.log.Info("Fetching CVE details...")
cveDetails, err := cveapi.CveClient.FetchCveDetails(uniqueCveIDs)
if err != nil {
return nil, err
}
o.log.Info("Done")
cvePacksList := []CvePacksInfo{}
for _, detail := range cveDetails {
// Amazon, RHEL do not use this method, so VendorAdvisory do not set.
cvePacksList = append(cvePacksList, CvePacksInfo{
CveID: detail.CveID,
CveDetail: detail,
Packs: cveIDPackInfoMap[detail.CveID],
// CvssScore: cinfo.CvssScore(conf.Lang),
})
}
sort.Sort(CvePacksList(cvePacksList))
return cvePacksList, nil
}
// parseYumCheckUpdateLines parse yum check-update to get package name, candidate version
func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.PackageInfoList, err error) {
needToParse := false
lines := strings.Split(stdout, "\n")
for _, line := range lines {
// update information of packages begin after blank line.
if trimed := strings.TrimSpace(line); len(trimed) == 0 {
needToParse = true
continue
}
if needToParse {
candidate, err := o.parseYumCheckUpdateLine(line)
if err != nil {
return models.PackageInfoList{}, err
}
installed, found := o.Packages.FindByName(candidate.Name)
if !found {
return models.PackageInfoList{}, fmt.Errorf(
"Failed to parse yum check update line: %s-%s-%s",
candidate.Name, candidate.Version, candidate.Release)
}
installed.NewVersion = candidate.NewVersion
installed.NewRelease = candidate.NewRelease
results = append(results, installed)
}
}
return
}
func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error) {
fields := strings.Fields(line)
if len(fields) != 3 {
return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line)
}
splitted := strings.Split(fields[0], ".")
packName := ""
if len(splitted) == 1 {
packName = fields[0]
} else {
packName = strings.Join(strings.Split(fields[0], ".")[0:(len(splitted)-1)], ".")
}
fields = strings.Split(fields[1], "-")
if len(fields) != 2 {
return models.PackageInfo{}, fmt.Errorf("Unknown format: %s", line)
}
version := fields[0]
release := fields[1]
return models.PackageInfo{
Name: packName,
NewVersion: version,
NewRelease: release,
}, nil
}
func (o *redhat) getChangelog(packageNames string) (stdout string, err error) {
command := "echo N | "
if 0 < len(config.Conf.HTTPProxy) {
command += util.ProxyEnv()
}
command += fmt.Sprintf(" yum update --changelog %s | grep CVE", packageNames)
r := o.ssh(command, sudo)
if !r.isSuccess(0, 1) {
return "", fmt.Errorf(
"Failed to get changelog. status: %d, stdout: %s, stderr: %s",
r.ExitStatus, r.Stdout, r.Stderr)
}
return r.Stdout, nil
}
type distroAdvisoryCveIDs struct {
DistroAdvisory models.DistroAdvisory
CveIDs []string
}
// Scaning unsecure packages using yum-plugin-security.
//TODO return whether already expired.
func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, error) {
if o.Family == "centos" {
// CentOS has no security channel.
// So use yum check-update && parse changelog
return CvePacksList{}, fmt.Errorf(
"yum updateinfo is not suppported on CentOS")
}
cmd := "yum repolist"
r := o.ssh(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess() {
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
// get advisoryID(RHSA, ALAS) - package name,version
cmd = "yum updateinfo list available --security"
r = o.ssh(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess() {
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout)
// get package name, version, rel to be upgrade.
cmd = "yum check-update --security"
r = o.ssh(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess(0, 100) {
//returns an exit code of 100 if there are available updates.
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
vulnerablePackInfoList, err := o.parseYumCheckUpdateLines(r.Stdout)
if err != nil {
return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
}
o.log.Debugf("%s", pp.Sprintf("%s", vulnerablePackInfoList))
for i, packInfo := range vulnerablePackInfoList {
installedPack, found := o.Packages.FindByName(packInfo.Name)
if !found {
return nil, fmt.Errorf(
"Parsed package not found. packInfo: %#v", packInfo)
}
vulnerablePackInfoList[i].Version = installedPack.Version
vulnerablePackInfoList[i].Release = installedPack.Release
}
dict := map[string][]models.PackageInfo{}
for _, advIDPackNames := range advIDPackNamesList {
packInfoList := models.PackageInfoList{}
for _, packName := range advIDPackNames.PackNames {
packInfo, found := vulnerablePackInfoList.FindByName(packName)
if !found {
return nil, fmt.Errorf(
"PackInfo not found. packInfo: %#v", packName)
}
packInfoList = append(packInfoList, packInfo)
continue
}
dict[advIDPackNames.AdvisoryID] = packInfoList
}
// get advisoryID(RHSA, ALAS) - CVE IDs
cmd = "yum updateinfo --security update"
r = o.ssh(util.PrependProxyEnv(cmd), noSudo)
if !r.isSuccess() {
return nil, fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout)
if err != nil {
return CvePacksList{}, err
}
// pp.Println(advisoryCveIDsList)
// All information collected.
// Convert to CvePacksList.
o.log.Info("Fetching CVE details...")
result := CvePacksList{}
for _, advIDCveIDs := range advisoryCveIDsList {
cveDetails, err :=
cveapi.CveClient.FetchCveDetails(advIDCveIDs.CveIDs)
if err != nil {
return nil, err
}
for _, cveDetail := range cveDetails {
found := false
for i, p := range result {
if cveDetail.CveID == p.CveID {
advAppended := append(p.DistroAdvisories, advIDCveIDs.DistroAdvisory)
result[i].DistroAdvisories = advAppended
packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
result[i].Packs = append(result[i].Packs, packs...)
found = true
break
}
}
if !found {
cpinfo := CvePacksInfo{
CveID: cveDetail.CveID,
CveDetail: cveDetail,
DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
Packs: dict[advIDCveIDs.DistroAdvisory.AdvisoryID],
}
result = append(result, cpinfo)
}
}
}
o.log.Info("Done")
return result, nil
}
func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
sectionState := Outside
lines := strings.Split(stdout, "\n")
lines = append(lines, "=============")
// Amazon Linux AMI Security Information
advisory := models.DistroAdvisory{}
cveIDsSetInThisSection := make(map[string]bool)
// use this flag to Collect CVE IDs in CVEs field.
var inDesctiption = false
for _, line := range lines {
line = strings.TrimSpace(line)
// find the new section pattern
if match, _ := o.isHorizontalRule(line); match {
// set previous section's result to return-variable
if sectionState == Content {
foundCveIDs := []string{}
for cveID := range cveIDsSetInThisSection {
foundCveIDs = append(foundCveIDs, cveID)
}
sort.Strings(foundCveIDs)
result = append(result, distroAdvisoryCveIDs{
DistroAdvisory: advisory,
CveIDs: foundCveIDs,
})
// reset for next section.
cveIDsSetInThisSection = make(map[string]bool)
inDesctiption = false
}
// Go to next section
sectionState = o.changeSectionState(sectionState)
continue
}
switch sectionState {
case Header:
switch o.Family {
case "centos":
// CentOS has no security channel.
// So use yum check-update && parse changelog
return result, fmt.Errorf(
"yum updateinfo is not suppported on CentOS")
case "rhel", "amazon":
// nop
}
case Content:
if found := o.isDescriptionLine(line); found {
inDesctiption = true
}
// severity
severity, found := o.parseYumUpdateinfoToGetSeverity(line)
if found {
advisory.Severity = severity
}
// No need to parse in description except severity
if inDesctiption {
continue
}
cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
for _, cveID := range cveIDs {
cveIDsSetInThisSection[cveID] = true
}
advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line)
if found {
advisory.AdvisoryID = advisoryID
}
issued, found := o.parseYumUpdateinfoLineToGetIssued(line)
if found {
advisory.Issued = issued
}
updated, found := o.parseYumUpdateinfoLineToGetUpdated(line)
if found {
advisory.Updated = updated
}
}
}
return
}
// state
const (
Outside = iota
Header = iota
Content = iota
)
func (o *redhat) changeSectionState(state int) (newState int) {
switch state {
case Outside, Content:
newState = Header
case Header:
newState = Content
}
return newState
}
func (o *redhat) isHorizontalRule(line string) (bool, error) {
return regexp.MatchString("^=+$", line)
}
// see test case
func (o *redhat) parseYumUpdateinfoHeaderCentOS(line string) (packs []models.PackageInfo, err error) {
pkgs := strings.Split(strings.TrimSpace(line), ",")
for _, pkg := range pkgs {
packs = append(packs, models.PackageInfo{})
s := strings.Split(pkg, "-")
if len(s) == 3 {
packs[len(packs)-1].Name = s[0]
packs[len(packs)-1].Version = s[1]
packs[len(packs)-1].Release = s[2]
} else {
return packs, fmt.Errorf("CentOS: Unknown Header format: %s", line)
}
}
return
}
func (o *redhat) parseYumUpdateinfoHeaderAmazon(line string) (a models.DistroAdvisory, names []string, err error) {
re, _ := regexp.Compile(`(ALAS-.+): (.+) priority package update for (.+)$`)
result := re.FindStringSubmatch(line)
if len(result) == 4 {
a.AdvisoryID = result[1]
a.Severity = result[2]
spaceSeparatedPacknames := result[3]
names = strings.Fields(spaceSeparatedPacknames)
return
}
err = fmt.Errorf("Amazon Linux: Unknown Header Format. %s", line)
return
}
func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
re, _ := regexp.Compile(`(CVE-\d{4}-\d{4})`)
return re.FindAllString(line, -1)
}
func (o *redhat) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
re, _ := regexp.Compile(`^ *Update ID : (.*)$`)
result := re.FindStringSubmatch(line)
if len(result) != 2 {
return "", false
}
return strings.TrimSpace(result[1]), true
}
func (o *redhat) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
return o.parseYumUpdateinfoLineToGetDate(line, `^\s*Issued : (\d{4}-\d{2}-\d{2})`)
}
func (o *redhat) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
return o.parseYumUpdateinfoLineToGetDate(line, `^\s*Updated : (\d{4}-\d{2}-\d{2})`)
}
func (o *redhat) parseYumUpdateinfoLineToGetDate(line, regexpFormat string) (date time.Time, found bool) {
re, _ := regexp.Compile(regexpFormat)
result := re.FindStringSubmatch(line)
if len(result) != 2 {
return date, false
}
t, err := time.Parse("2006-01-02", result[1])
if err != nil {
return date, false
}
return t, true
}
func (o *redhat) isDescriptionLine(line string) bool {
re, _ := regexp.Compile(`^\s*Description : `)
return re.MatchString(line)
}
func (o *redhat) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
re, _ := regexp.Compile(`^ *Severity : (.*)$`)
result := re.FindStringSubmatch(line)
if len(result) != 2 {
return "", false
}
return strings.TrimSpace(result[1]), true
}
type advisoryIDPacks struct {
AdvisoryID string
PackNames []string
}
type advisoryIDPacksList []advisoryIDPacks
func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool) {
for _, a := range list {
if a.AdvisoryID == advisoryID {
return a, true
}
}
return advisoryIDPacks{}, false
}
func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
fields := strings.Split(nameVerRel, ".")
archTrimed := strings.Join(fields[0:len(fields)-1], ".")
fields = strings.Split(archTrimed, "-")
rel = fields[len(fields)-1]
ver = fields[len(fields)-2]
name = strings.Join(fields[0:(len(fields)-2)], "-")
return
}
// parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS), packages
func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
result := []advisoryIDPacks{}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if !(strings.HasPrefix(line, "RHSA") ||
strings.HasPrefix(line, "ALAS")) {
continue
}
fields := strings.Fields(line)
if len(fields) != 3 {
return []advisoryIDPacks{}, fmt.Errorf(
"Unknown format. line: %s", line)
}
// extract fields
advisoryID := fields[0]
packVersion := fields[2]
packName, _, _ := o.extractPackNameVerRel(packVersion)
found := false
for i, s := range result {
if s.AdvisoryID == advisoryID {
names := s.PackNames
names = append(names, packName)
result[i].PackNames = names
found = true
break
}
}
if !found {
result = append(result, advisoryIDPacks{
AdvisoryID: advisoryID,
PackNames: []string{packName},
})
}
}
return result, nil
}

843
scan/redhat_test.go Normal file
View File

@@ -0,0 +1,843 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"reflect"
"testing"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/k0kubun/pp"
)
// func unixtimeNoerr(s string) time.Time {
// t, _ := unixtime(s)
// return t
// }
func TestParseScanedPackagesLineRedhat(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var packagetests = []struct {
in string
pack models.PackageInfo
}{
{
"openssl 1.0.1e 30.el6.11",
models.PackageInfo{
Name: "openssl",
Version: "1.0.1e",
Release: "30.el6.11",
},
},
}
for _, tt := range packagetests {
p, _ := r.parseScanedPackagesLine(tt.in)
if p.Name != tt.pack.Name {
t.Errorf("name: expected %s, actual %s", tt.pack.Name, p.Name)
}
if p.Version != tt.pack.Version {
t.Errorf("version: expected %s, actual %s", tt.pack.Version, p.Version)
}
if p.Release != tt.pack.Release {
t.Errorf("release: expected %s, actual %s", tt.pack.Release, p.Release)
}
}
}
func TestChangeSectionState(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
oldState int
newState int
}{
{Outside, Header},
{Header, Content},
{Content, Header},
}
for _, tt := range tests {
if n := r.changeSectionState(tt.oldState); n != tt.newState {
t.Errorf("expected %d, actual %d", tt.newState, n)
}
}
}
func TestParseYumUpdateinfoHeader(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out []models.PackageInfo
}{
{
" nodejs-0.10.36-3.el6,libuv-0.10.34-1.el6,v8-3.14.5.10-17.el6 ",
[]models.PackageInfo{
{
Name: "nodejs",
Version: "0.10.36",
Release: "3.el6",
},
{
Name: "libuv",
Version: "0.10.34",
Release: "1.el6",
},
{
Name: "v8",
Version: "3.14.5.10",
Release: "17.el6",
},
},
},
}
for _, tt := range tests {
if a, err := r.parseYumUpdateinfoHeaderCentOS(tt.in); err != nil {
t.Errorf("err: %s", err)
} else {
if !reflect.DeepEqual(a, tt.out) {
e := pp.Sprintf("%#v", tt.out)
a := pp.Sprintf("%#v", a)
t.Errorf("expected %s, actual %s", e, a)
}
}
}
}
func TestParseYumUpdateinfoLineToGetCveIDs(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out []string
}{
{
"Bugs : 1194651 - CVE-2015-0278 libuv:",
[]string{"CVE-2015-0278"},
},
{
": 1195457 - nodejs-0.10.35 causes undefined symbolsCVE-2015-0278, CVE-2015-0278, CVE-2015-0277",
[]string{
"CVE-2015-0278",
"CVE-2015-0278",
"CVE-2015-0277",
},
},
}
for _, tt := range tests {
act := r.parseYumUpdateinfoLineToGetCveIDs(tt.in)
for i, s := range act {
if s != tt.out[i] {
t.Errorf("expected %s, actual %s", tt.out[i], s)
}
}
}
}
func TestParseYumUpdateinfoToGetAdvisoryID(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out string
found bool
}{
{
"Update ID : RHSA-2015:2315",
"RHSA-2015:2315",
true,
},
{
"Update ID : ALAS-2015-620",
"ALAS-2015-620",
true,
},
{
"Issued : 2015-11-19 00:00:00",
"",
false,
},
}
for _, tt := range tests {
advisoryID, found := r.parseYumUpdateinfoToGetAdvisoryID(tt.in)
if tt.out != advisoryID {
t.Errorf("expected %s, actual %s", tt.out, advisoryID)
}
if tt.found != found {
t.Errorf("expected %t, actual %t", tt.found, found)
}
}
}
func TestParseYumUpdateinfoLineToGetIssued(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2015-12-15")
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out time.Time
found bool
}{
{
"Issued : 2015-12-15 00:00:00",
date,
true,
},
{
" Issued : 2015-12-15 00:00:00 ",
date,
true,
},
{
"Type : security",
time.Time{},
false,
},
}
for i, tt := range tests {
d, found := r.parseYumUpdateinfoLineToGetIssued(tt.in)
if tt.found != found {
t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
}
if tt.out != d {
t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, d)
}
}
}
func TestParseYumUpdateinfoLineToGetUpdated(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2015-12-15")
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out time.Time
found bool
}{
{
"Updated : 2015-12-15 00:00:00 Bugs : 1286966 - CVE-2015-8370 grub2: buffer overflow when checking password entered during bootup",
date,
true,
},
{
"Updated : 2015-12-15 14:16 CVEs : CVE-2015-7981",
date,
true,
},
{
"Type : security",
time.Time{},
false,
},
}
for i, tt := range tests {
d, found := r.parseYumUpdateinfoLineToGetUpdated(tt.in)
if tt.found != found {
t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
}
if tt.out != d {
t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, d)
}
}
}
func TestIsDescriptionLine(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
found bool
}{
{
"Description : Package updates are available for Amazon Linux AMI that fix the",
true,
},
{
" Description : Package updates are available for Amazon Linux AMI that fix the",
true,
},
{
"Status : final",
false,
},
}
for i, tt := range tests {
found := r.isDescriptionLine(tt.in)
if tt.found != found {
t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
}
}
}
func TestParseYumUpdateinfoToGetSeverity(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out string
found bool
}{
{
"Severity : Moderate",
"Moderate",
true,
},
{
" Severity : medium",
"medium",
true,
},
{
"Status : final",
"",
false,
},
}
for i, tt := range tests {
out, found := r.parseYumUpdateinfoToGetSeverity(tt.in)
if tt.found != found {
t.Errorf("[%d] line: %s, expected %t, actual %t", i, tt.in, tt.found, found)
}
if tt.out != out {
t.Errorf("[%d] line: %s, expected %v, actual %v", i, tt.in, tt.out, out)
}
}
}
func TestParseYumUpdateinfoRHEL(t *testing.T) {
stdout := `===============================================================================
Important: bind security update
===============================================================================
Update ID : RHSA-2015:1705
Release :
Type : security
Status : final
Issued : 2015-09-03 00:00:00
Bugs : 1259087 - CVE-2015-5722 bind: malformed DNSSEC key failed assertion denial of service
CVEs : CVE-2015-5722
Description : The Berkeley Internet Name Domain (BIND) is an implementation of
: the Domain Name System (DNS) protocols. BIND
: includes a DNS server (named); a resolver library
: (routines for applications to use when interfacing
: with DNS); and tools for verifying that the DNS
: server is operating correctly.
:
Severity : Important
===============================================================================
Important: bind security update
===============================================================================
Update ID : RHSA-2015:2655
Release :
Type : security
Status : final
Issued : 2015-09-03 01:00:00
Updated : 2015-09-04 00:00:00
Bugs : 1291176 - CVE-2015-8000 bind: responses with a malformed class attribute can trigger an assertion failure in db.c
CVEs : CVE-2015-8000
: CVE-2015-8001
Description : The Berkeley Internet Name Domain (BIND) is an implementation of
: the Domain Name System (DNS) protocols. BIND
: includes a DNS server (named); a resolver library
: (routines for applications to use when interfacing
: with DNS); and tools for verifying that the DNS
: server is operating correctly.
:
Severity : Low
===============================================================================
Moderate: bind security update
===============================================================================
Update ID : RHSA-2016:0073
Release :
Type : security
Status : final
Issued : 2015-09-03 02:00:00
Bugs : 1299364 - CVE-2015-8704 bind: specific APL data could trigger an INSIST in apl_42.c CVEs : CVE-2015-8704
: CVE-2015-8705
Description : The Berkeley Internet Name Domain (BIND) is an implementation of
: the Domain Name System (DNS) protocols. BIND
: includes a DNS server (named); a resolver library
: (routines for applications to use when interfacing
: with DNS); and tools for verifying that the DNS
: server is operating correctly.
:
Severity : Moderate
`
issued, _ := time.Parse("2006-01-02", "2015-09-03")
updated, _ := time.Parse("2006-01-02", "2015-09-04")
r := newRedhat(config.ServerInfo{})
r.Family = "redhat"
var tests = []struct {
in string
out []distroAdvisoryCveIDs
}{
{
stdout,
[]distroAdvisoryCveIDs{
{
DistroAdvisory: models.DistroAdvisory{
AdvisoryID: "RHSA-2015:1705",
Severity: "Important",
Issued: issued,
},
CveIDs: []string{"CVE-2015-5722"},
},
{
DistroAdvisory: models.DistroAdvisory{
AdvisoryID: "RHSA-2015:2655",
Severity: "Low",
Issued: issued,
Updated: updated,
},
CveIDs: []string{
"CVE-2015-8000",
"CVE-2015-8001",
},
},
{
DistroAdvisory: models.DistroAdvisory{
AdvisoryID: "RHSA-2016:0073",
Severity: "Moderate",
Issued: issued,
Updated: updated,
},
CveIDs: []string{
"CVE-2015-8704",
"CVE-2015-8705",
},
},
},
},
}
for _, tt := range tests {
actual, _ := r.parseYumUpdateinfo(tt.in)
for i, advisoryCveIDs := range actual {
if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
e := pp.Sprintf("%v", tt.out[i])
a := pp.Sprintf("%v", advisoryCveIDs)
t.Errorf("[%d] Alas is not same. \nexpected: %s\nactual: %s",
i, e, a)
}
}
}
}
func TestParseYumUpdateinfoAmazon(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r.Family = "amazon"
issued, _ := time.Parse("2006-01-02", "2015-12-15")
updated, _ := time.Parse("2006-01-02", "2015-12-16")
var tests = []struct {
in string
out []distroAdvisoryCveIDs
}{
{
`===============================================================================
Amazon Linux AMI 2014.03 - ALAS-2016-644: medium priority package update for python-rsa
===============================================================================
Update ID : ALAS-2016-644
Release :
Type : security
Status : final
Issued : 2015-12-15 13:30
CVEs : CVE-2016-1494
Description : Package updates are available for Amazon Linux AMI that fix the
: following vulnerabilities: CVE-2016-1494:
: 1295869:
: CVE-2016-1494 python-rsa: Signature forgery using
: Bleichenbacher'06 attack
Severity : medium
===============================================================================
Amazon Linux AMI 2014.03 - ALAS-2015-614: medium priority package update for openssl
===============================================================================
Update ID : ALAS-2015-614
Release :
Type : security
Status : final
Issued : 2015-12-15 10:00
Updated : 2015-12-16 14:15 CVEs : CVE-2015-3194
: CVE-2015-3195
: CVE-2015-3196
Description : Package updates are available for Amazon Linux AMI that fix the
: following vulnerabilities: CVE-2015-3196:
: 1288326:
: CVE-2015-3196 OpenSSL: Race condition handling PSK
: identify hint A race condition flaw, leading to a
: double free, was found in the way OpenSSL handled
: pre-shared keys (PSKs). A remote attacker could
: use this flaw to crash a multi-threaded SSL/TLS
: client.
:
Severity : medium`,
[]distroAdvisoryCveIDs{
{
DistroAdvisory: models.DistroAdvisory{
AdvisoryID: "ALAS-2016-644",
Severity: "medium",
Issued: issued,
},
CveIDs: []string{"CVE-2016-1494"},
},
{
DistroAdvisory: models.DistroAdvisory{
AdvisoryID: "ALAS-2015-614",
Severity: "medium",
Issued: issued,
Updated: updated,
},
CveIDs: []string{
"CVE-2015-3194",
"CVE-2015-3195",
"CVE-2015-3196",
},
},
},
},
}
for _, tt := range tests {
actual, _ := r.parseYumUpdateinfo(tt.in)
for i, advisoryCveIDs := range actual {
if !reflect.DeepEqual(tt.out[i], advisoryCveIDs) {
e := pp.Sprintf("%v", tt.out[i])
a := pp.Sprintf("%v", advisoryCveIDs)
t.Errorf("[%d] Alas is not same. expected %s, actual %s",
i, e, a)
}
}
}
}
func TestParseYumCheckUpdateLines(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r.Family = "centos"
stdout := `Loaded plugins: changelog, fastestmirror, keys, protect-packages, protectbase, security
Loading mirror speeds from cached hostfile
* base: mirror.fairway.ne.jp
* epel: epel.mirror.srv.co.ge
* extras: mirror.fairway.ne.jp
* updates: mirror.fairway.ne.jp
0 packages excluded due to repository protections
audit-libs.x86_64 2.3.7-5.el6 base
bash.x86_64 4.1.2-33.el6_7.1 updates
`
r.Packages = []models.PackageInfo{
{
Name: "audit-libs",
Version: "2.3.6",
Release: "4.el6",
},
{
Name: "bash",
Version: "4.1.1",
Release: "33",
},
}
var tests = []struct {
in string
out models.PackageInfoList
}{
{
stdout,
models.PackageInfoList{
{
Name: "audit-libs",
Version: "2.3.6",
Release: "4.el6",
NewVersion: "2.3.7",
NewRelease: "5.el6",
},
{
Name: "bash",
Version: "4.1.1",
Release: "33",
NewVersion: "4.1.2",
NewRelease: "33.el6_7.1",
},
},
},
}
for _, tt := range tests {
packInfoList, err := r.parseYumCheckUpdateLines(tt.in)
if err != nil {
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
return
}
for i, ePackInfo := range tt.out {
if !reflect.DeepEqual(ePackInfo, packInfoList[i]) {
e := pp.Sprintf("%v", ePackInfo)
a := pp.Sprintf("%v", packInfoList[i])
t.Errorf("[%d] expected %s, actual %s", i, e, a)
}
}
}
}
func TestParseYumCheckUpdateLinesAmazon(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r.Family = "amzon"
stdout := `Loaded plugins: priorities, update-motd, upgrade-helper
34 package(s) needed for security, out of 71 available
bind-libs.x86_64 32:9.8.2-0.37.rc1.45.amzn1 amzn-main
java-1.7.0-openjdk.x86_64 1:1.7.0.95-2.6.4.0.65.amzn1 amzn-main
if-not-architecture 100-200 amzn-main
`
r.Packages = []models.PackageInfo{
{
Name: "bind-libs",
Version: "32:9.8.0",
Release: "0.33.rc1.45.amzn1",
},
{
Name: "java-1.7.0-openjdk",
Version: "1:1.7.0.0",
Release: "2.6.4.0.0.amzn1",
},
{
Name: "if-not-architecture",
Version: "10",
Release: "20",
},
}
var tests = []struct {
in string
out models.PackageInfoList
}{
{
stdout,
models.PackageInfoList{
{
Name: "bind-libs",
Version: "32:9.8.0",
Release: "0.33.rc1.45.amzn1",
NewVersion: "32:9.8.2",
NewRelease: "0.37.rc1.45.amzn1",
},
{
Name: "java-1.7.0-openjdk",
Version: "1:1.7.0.0",
Release: "2.6.4.0.0.amzn1",
NewVersion: "1:1.7.0.95",
NewRelease: "2.6.4.0.65.amzn1",
},
{
Name: "if-not-architecture",
Version: "10",
Release: "20",
NewVersion: "100",
NewRelease: "200",
},
},
},
}
for _, tt := range tests {
packInfoList, err := r.parseYumCheckUpdateLines(tt.in)
if err != nil {
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
return
}
for i, ePackInfo := range tt.out {
if !reflect.DeepEqual(ePackInfo, packInfoList[i]) {
e := pp.Sprintf("%v", ePackInfo)
a := pp.Sprintf("%v", packInfoList[i])
t.Errorf("[%d] expected %s, actual %s", i, e, a)
}
}
}
}
func TestParseYumUpdateinfoAmazonLinuxHeader(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out models.DistroAdvisory
}{
{
"Amazon Linux AMI 2014.03 - ALAS-2015-598: low priority package update for grep",
models.DistroAdvisory{
AdvisoryID: "ALAS-2015-598",
Severity: "low",
},
},
}
for _, tt := range tests {
a, _, _ := r.parseYumUpdateinfoHeaderAmazon(tt.in)
if !reflect.DeepEqual(a, tt.out) {
e := pp.Sprintf("%v", tt.out)
a := pp.Sprintf("%v", a)
t.Errorf("expected %s, actual %s", e, a)
}
}
}
func TestParseYumUpdateinfoListAvailable(t *testing.T) {
r := newRedhat(config.ServerInfo{})
rhelStdout := `RHSA-2015:2315 Moderate/Sec. NetworkManager-1:1.0.6-27.el7.x86_64
RHSA-2015:2315 Moderate/Sec. NetworkManager-config-server-1:1.0.6-27.el7.x86_64
RHSA-2015:1705 Important/Sec. bind-libs-lite-32:9.9.4-18.el7_1.5.x86_64
RHSA-2016:0176 Critical/Sec. glibc-2.17-106.el7_2.4.x86_64
RHSA-2015:2401 Low/Sec. grub2-1:2.02-0.29.el7.x86_64
RHSA-2015:2401 Low/Sec. grub2-tools-1:2.02-0.29.el7.x86_64
updateinfo list done`
var tests = []struct {
in string
out []advisoryIDPacks
}{
{
rhelStdout,
[]advisoryIDPacks{
{
AdvisoryID: "RHSA-2015:2315",
PackNames: []string{
"NetworkManager",
"NetworkManager-config-server",
},
},
{
AdvisoryID: "RHSA-2015:1705",
PackNames: []string{
"bind-libs-lite",
},
},
{
AdvisoryID: "RHSA-2016:0176",
PackNames: []string{
"glibc",
},
},
{
AdvisoryID: "RHSA-2015:2401",
PackNames: []string{
"grub2",
"grub2-tools",
},
},
},
},
}
for _, tt := range tests {
actual, err := r.parseYumUpdateinfoListAvailable(tt.in)
if err != nil {
t.Errorf("Error has occurred: %s", err)
return
}
for i := range actual {
if !reflect.DeepEqual(actual[i], tt.out[i]) {
e := pp.Sprintf("%v", tt.out)
a := pp.Sprintf("%v", actual)
t.Errorf("[%d] expected: %s\nactual: %s", i, e, a)
}
}
}
}
func TestParseYumUpdateinfoToGetUpdateID(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var packagetests = []struct {
in string
pack models.PackageInfo
}{
{
"openssl 1.0.1e 30.el6.11",
models.PackageInfo{
Name: "openssl",
Version: "1.0.1e",
Release: "30.el6.11",
},
},
}
for _, tt := range packagetests {
p, _ := r.parseScanedPackagesLine(tt.in)
if p.Name != tt.pack.Name {
t.Errorf("name: expected %s, actual %s", tt.pack.Name, p.Name)
}
if p.Version != tt.pack.Version {
t.Errorf("version: expected %s, actual %s", tt.pack.Version, p.Version)
}
if p.Release != tt.pack.Release {
t.Errorf("release: expected %s, actual %s", tt.pack.Release, p.Release)
}
}
}
func TestExtractPackNameVerRel(t *testing.T) {
r := newRedhat(config.ServerInfo{})
var tests = []struct {
in string
out []string
}{
{
"openssh-server-6.2p2-8.45.amzn1.x86_64",
[]string{"openssh-server", "6.2p2", "8.45.amzn1"},
},
{
"bind-libs-lite-32:9.9.4-29.el7_2.1.x86_64",
[]string{"bind-libs-lite", "32:9.9.4", "29.el7_2.1"},
},
{
"glibc-2.17-106.el7_2.1.x86_64",
[]string{"glibc", "2.17", "106.el7_2.1"},
},
}
for _, tt := range tests {
name, ver, rel := r.extractPackNameVerRel(tt.in)
if tt.out[0] != name {
t.Errorf("name: expected %s, actual %s", tt.out[0], name)
}
if tt.out[1] != ver {
t.Errorf("ver: expected %s, actual %s", tt.out[1], ver)
}
if tt.out[2] != rel {
t.Errorf("ver: expected %s, actual %s", tt.out[2], rel)
}
}
}

209
scan/serverapi.go Normal file
View File

@@ -0,0 +1,209 @@
package scan
import (
"fmt"
"time"
"github.com/Sirupsen/logrus"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/k0kubun/pp"
cve "github.com/kotakanbe/go-cve-dictionary/models"
)
// Log for localhsot
var Log *logrus.Entry
var servers []osTypeInterface
// Base Interface of redhat, debian
type osTypeInterface interface {
setServerInfo(config.ServerInfo)
getServerInfo() config.ServerInfo
setDistributionInfo(string, string)
checkRequiredPackagesInstalled() error
scanPackages() error
scanVulnByCpeName() error
install() error
convertToModel() (models.ScanResult, error)
}
// osPackages included by linux struct
type osPackages struct {
// installed packages
Packages models.PackageInfoList
// unsecure packages
UnsecurePackages CvePacksList
}
func (p *osPackages) setPackages(pi models.PackageInfoList) {
p.Packages = pi
}
func (p *osPackages) setUnsecurePackages(pi []CvePacksInfo) {
p.UnsecurePackages = pi
}
// CvePacksList have CvePacksInfo list, getter/setter, sortable methods.
type CvePacksList []CvePacksInfo
// CvePacksInfo hold the CVE information.
type CvePacksInfo struct {
CveID string
CveDetail cve.CveDetail
Packs []models.PackageInfo
DistroAdvisories []models.DistroAdvisory // for Aamazon, RHEL
CpeNames []string
// CvssScore float64
}
// FindByCveID find by CVEID
func (s CvePacksList) FindByCveID(cveID string) (pi CvePacksInfo, found bool) {
for _, p := range s {
if cveID == p.CveID {
return p, true
}
}
return CvePacksInfo{CveID: cveID}, false
}
// immutable
func (s CvePacksList) set(cveID string, cvePacksInfo CvePacksInfo) CvePacksList {
for i, p := range s {
if cveID == p.CveID {
s[i] = cvePacksInfo
return s
}
}
return append(s, cvePacksInfo)
}
// Len implement Sort Interface
func (s CvePacksList) Len() int {
return len(s)
}
// Swap implement Sort Interface
func (s CvePacksList) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less implement Sort Interface
func (s CvePacksList) Less(i, j int) bool {
return s[i].CveDetail.CvssScore("en") > s[j].CveDetail.CvssScore("en")
}
func detectOs(c config.ServerInfo) (osType osTypeInterface) {
var itsMe bool
itsMe, osType = detectDebian(c)
if itsMe {
return
}
itsMe, osType = detectRedhat(c)
return
}
// InitServers detect the kind of OS distribution of target servers
func InitServers(localLogger *logrus.Entry) (err error) {
Log = localLogger
if servers, err = detectServersOS(); err != nil {
err = fmt.Errorf("Failed to detect OS")
} else {
Log.Debugf("%s", pp.Sprintf("%s", servers))
}
return
}
func detectServersOS() (osi []osTypeInterface, err error) {
osTypeChan := make(chan osTypeInterface, len(config.Conf.Servers))
defer close(osTypeChan)
for _, s := range config.Conf.Servers {
go func(s config.ServerInfo) {
osTypeChan <- detectOs(s)
}(s)
}
timeout := time.After(60 * time.Second)
for i := 0; i < len(config.Conf.Servers); i++ {
select {
case res := <-osTypeChan:
osi = append(osi, res)
case <-timeout:
Log.Error("Timeout Occured while detecting OS.")
err = fmt.Errorf("Timeout!")
return
}
}
return
}
// Prepare installs requred packages to scan vulnerabilities.
func Prepare() []error {
return parallelSSHExec(func(o osTypeInterface) error {
if err := o.install(); err != nil {
return err
}
return nil
})
}
// Scan scan
func Scan() []error {
if len(servers) == 0 {
return []error{fmt.Errorf("Not initialize yet.")}
}
Log.Info("Check required packages for scanning...")
if errs := checkRequiredPackagesInstalled(); errs != nil {
Log.Error("Please execute with [prepare] subcommand to install required packages before scanning")
return errs
}
Log.Info("Scanning vuluneable OS packages...")
if errs := scanPackages(); errs != nil {
return errs
}
Log.Info("Scanning vulnerable software specified in CPE...")
if errs := scanVulnByCpeName(); errs != nil {
return errs
}
return nil
}
func checkRequiredPackagesInstalled() []error {
timeoutSec := 30 * 60
return parallelSSHExec(func(o osTypeInterface) error {
return o.checkRequiredPackagesInstalled()
}, timeoutSec)
}
func scanPackages() []error {
timeoutSec := 30 * 60
return parallelSSHExec(func(o osTypeInterface) error {
return o.scanPackages()
}, timeoutSec)
}
// scanVulnByCpeName search vulnerabilities that specified in config file.
func scanVulnByCpeName() []error {
timeoutSec := 30 * 60
return parallelSSHExec(func(o osTypeInterface) error {
return o.scanVulnByCpeName()
}, timeoutSec)
}
// GetScanResults returns Scan Resutls
func GetScanResults() (results models.ScanResults, err error) {
for _, s := range servers {
r, err := s.convertToModel()
if err != nil {
return results, fmt.Errorf("Failed converting to model: %s.", err)
}
results = append(results, r)
}
return
}

47
scan/serverapi_test.go Normal file
View File

@@ -0,0 +1,47 @@
package scan
import "testing"
func TestPackageCveInfosSetGet(t *testing.T) {
var test = struct {
in []string
out []string
}{
[]string{
"CVE1",
"CVE2",
"CVE3",
"CVE1",
"CVE1",
"CVE2",
"CVE3",
},
[]string{
"CVE1",
"CVE2",
"CVE3",
},
}
// var ps packageCveInfos
var ps CvePacksList
for _, cid := range test.in {
ps = ps.set(cid, CvePacksInfo{CveID: cid})
}
if len(test.out) != len(ps) {
t.Errorf("length: expected %d, actual %d", len(test.out), len(ps))
}
for i, expectedCid := range test.out {
if expectedCid != ps[i].CveID {
t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID)
}
}
for _, cid := range test.in {
p, _ := ps.FindByCveID(cid)
if p.CveID != cid {
t.Errorf("expected %s, actual %s", cid, p.CveID)
}
}
}

311
scan/sshutil.go Normal file
View File

@@ -0,0 +1,311 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
conf "github.com/future-architect/vuls/config"
"github.com/k0kubun/pp"
)
type sshResult struct {
Host string
Port string
Stdout string
Stderr string
ExitStatus int
}
func (s sshResult) isSuccess(expectedStatusCodes ...int) bool {
if len(expectedStatusCodes) == 0 {
return s.ExitStatus == 0
}
for _, code := range expectedStatusCodes {
if code == s.ExitStatus {
return true
}
}
return false
}
// Sudo is Const value for sudo mode
const sudo = true
// NoSudo is Const value for normal user mode
const noSudo = false
func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs []error) {
errChan := make(chan error, len(servers))
defer close(errChan)
for _, s := range servers {
go func(s osTypeInterface) {
if err := fn(s); err != nil {
errChan <- fmt.Errorf("%s@%s:%s: %s",
s.getServerInfo().User,
s.getServerInfo().Host,
s.getServerInfo().Port,
err,
)
} else {
errChan <- nil
}
}(s)
}
var timeout int
if len(timeoutSec) == 0 {
timeout = 10 * 60
} else {
timeout = timeoutSec[0]
}
for i := 0; i < len(servers); i++ {
select {
case err := <-errChan:
if err != nil {
errs = append(errs, err)
} else {
logrus.Debug("Parallel SSH Success.")
}
case <-time.After(time.Duration(timeout) * time.Second):
logrus.Errorf("Parallel SSH Timeout.")
errs = append(errs, fmt.Errorf("Timed out!"))
}
}
return
}
func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) {
// Setup Logger
var logger *logrus.Entry
if len(log) == 0 {
level := logrus.InfoLevel
if conf.Conf.Debug == true {
level = logrus.DebugLevel
}
l := &logrus.Logger{
Out: os.Stderr,
Formatter: new(logrus.TextFormatter),
Hooks: make(logrus.LevelHooks),
Level: level,
}
logger = logrus.NewEntry(l)
} else {
logger = log[0]
}
var err error
if sudo && c.User != "root" {
switch {
case c.SudoOpt.ExecBySudo:
cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd)
case c.SudoOpt.ExecBySudoSh:
cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd)
default:
logger.Panicf("sudoOpt is invalid. SudoOpt: %v", c.SudoOpt)
}
}
// set pipefail option.
// http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another
cmd = fmt.Sprintf("set -o pipefail; %s", cmd)
logger.Debugf("Command: %s", strings.Replace(cmd, "\n", "", -1))
var client *ssh.Client
client, err = sshConnect(c)
defer client.Close()
var session *ssh.Session
if session, err = client.NewSession(); err != nil {
logger.Errorf("Failed to new session. err: %s, c: %s",
err,
pp.Sprintf("%v", c))
result.ExitStatus = 999
return
}
defer session.Close()
// http://blog.ralch.com/tutorial/golang-ssh-connection/
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 400, 120, modes); err != nil {
logger.Errorf("Failed to request for pseudo terminal. err: %s, c: %s",
err,
pp.Sprintf("%v", c))
result.ExitStatus = 999
return
}
var stdoutBuf, stderrBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = &stderrBuf
if err := session.Run(cmd); err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
result.ExitStatus = exitErr.ExitStatus()
} else {
result.ExitStatus = 999
}
} else {
result.ExitStatus = 0
}
result.Stdout = stdoutBuf.String()
result.Stderr = stderrBuf.String()
result.Host = c.Host
result.Port = c.Port
logger.Debugf(
"SSH executed. cmd: %s, status: %#v\nstdout: \n%s\nstderr: \n%s",
cmd, err, result.Stdout, result.Stderr)
return
}
func getAgentAuth() (auth ssh.AuthMethod, ok bool) {
if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 {
if agconn, err := net.Dial("unix", sock); err == nil {
ag := agent.NewClient(agconn)
auth = ssh.PublicKeysCallback(ag.Signers)
ok = true
}
}
return
}
func tryAgentConnect(c conf.ServerInfo) *ssh.Client {
if auth, ok := getAgentAuth(); ok {
config := &ssh.ClientConfig{
User: c.User,
Auth: []ssh.AuthMethod{auth},
}
client, _ := ssh.Dial("tcp", c.Host+":"+c.Port, config)
return client
}
return nil
}
func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) {
if client = tryAgentConnect(c); client != nil {
return client, nil
}
var auths = []ssh.AuthMethod{}
if auths, err = addKeyAuth(auths, c.KeyPath, c.KeyPassword); err != nil {
logrus.Fatalf("Faild to add keyAuth. err: %s", err)
}
if c.Password != "" {
auths = append(auths, ssh.Password(c.Password))
}
// http://blog.ralch.com/tutorial/golang-ssh-connection/
config := &ssh.ClientConfig{
User: c.User,
Auth: auths,
}
// log.Debugf("config: %s", pp.Sprintf("%v", config))
notifyFunc := func(e error, t time.Duration) {
logrus.Warnf("Faild to ssh %s@%s:%s. err: %s, Retrying in %s...",
c.User, c.Host, c.Port, e, t)
logrus.Debugf("sshConInfo: %s", pp.Sprintf("%v", c))
}
err = backoff.RetryNotify(func() error {
if client, err = ssh.Dial("tcp", c.Host+":"+c.Port, config); err != nil {
return err
}
return nil
}, backoff.NewExponentialBackOff(), notifyFunc)
return
}
// https://github.com/rapidloop/rtop/blob/ba5b35e964135d50e0babedf0bd69b2fcb5dbcb4/src/sshhelper.go#L100
func addKeyAuth(auths []ssh.AuthMethod, keypath string, keypassword string) ([]ssh.AuthMethod, error) {
if len(keypath) == 0 {
return auths, nil
}
// read the file
pemBytes, err := ioutil.ReadFile(keypath)
if err != nil {
return auths, err
}
// get first pem block
block, _ := pem.Decode(pemBytes)
if block == nil {
return auths, fmt.Errorf("no key found in %s", keypath)
}
// handle plain and encrypted keyfiles
if x509.IsEncryptedPEMBlock(block) {
block.Bytes, err = x509.DecryptPEMBlock(block, []byte(keypassword))
if err != nil {
return auths, err
}
key, err := parsePemBlock(block)
if err != nil {
return auths, err
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
return auths, err
}
return append(auths, ssh.PublicKeys(signer)), nil
}
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return auths, err
}
return append(auths, ssh.PublicKeys(signer)), nil
}
// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey.
func parsePemBlock(block *pem.Block) (interface{}, error) {
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "DSA PRIVATE KEY":
return ssh.ParseDSAPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type)
}
}

68
util/logutil.go Normal file
View File

@@ -0,0 +1,68 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package util
import (
"fmt"
"os"
"github.com/Sirupsen/logrus"
"github.com/rifflock/lfshook"
"github.com/future-architect/vuls/config"
formatter "github.com/kotakanbe/logrus-prefixed-formatter"
)
// NewCustomLogger creates logrus
func NewCustomLogger(c config.ServerInfo) *logrus.Entry {
log := logrus.New()
log.Formatter = &formatter.TextFormatter{MsgAnsiColor: c.LogMsgAnsiColor}
log.Out = os.Stderr
log.Level = logrus.InfoLevel
if config.Conf.Debug {
log.Level = logrus.DebugLevel
}
// File output
logDir := "/var/log/vuls"
if _, err := os.Stat(logDir); os.IsNotExist(err) {
if err := os.Mkdir(logDir, 0666); err != nil {
logrus.Errorf("Failed to create log directory: %s", err)
}
}
whereami := "localhost"
if 0 < len(c.ServerName) {
whereami = fmt.Sprintf("%s:%s", c.ServerName, c.Port)
}
if _, err := os.Stat(logDir); err == nil {
path := fmt.Sprintf("%s/%s.log", logDir, whereami)
log.Hooks.Add(lfshook.NewHook(lfshook.PathMap{
logrus.DebugLevel: path,
logrus.InfoLevel: path,
logrus.WarnLevel: path,
logrus.ErrorLevel: path,
logrus.FatalLevel: path,
logrus.PanicLevel: path,
}))
}
fields := logrus.Fields{"prefix": whereami}
return log.WithFields(fields)
}

120
util/util.go Normal file
View File

@@ -0,0 +1,120 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package util
import (
"fmt"
"net/url"
"strings"
"github.com/future-architect/vuls/config"
)
// GenWorkers generates goroutine
// http://qiita.com/na-o-ys/items/65373132b1c5bc973cca
func GenWorkers(num int) chan<- func() {
tasks := make(chan func())
for i := 0; i < num; i++ {
go func() {
for f := range tasks {
f()
}
}()
}
return tasks
}
// AppendIfMissing append to the slice if missing
func AppendIfMissing(slice []string, s string) []string {
for _, ele := range slice {
if ele == s {
return slice
}
}
return append(slice, s)
}
// URLPathJoin make URL
func URLPathJoin(baseURL string, paths ...string) (string, error) {
baseURL = strings.TrimSuffix(baseURL, "/")
trimedPaths := []string{}
for _, path := range paths {
trimed := strings.Trim(path, " /")
if len(trimed) != 0 {
trimedPaths = append(trimedPaths, trimed)
}
}
var url *url.URL
url, err := url.Parse(baseURL)
if err != nil {
return "", err
}
url.Path += strings.Join(trimedPaths, "/")
return url.String(), nil
}
// URLPathParamJoin make URL
func URLPathParamJoin(baseURL string, paths []string, params map[string]string) (string, error) {
urlPath, err := URLPathJoin(baseURL, paths...)
if err != nil {
return "", err
}
u, err := url.Parse(urlPath)
if err != nil {
return "", err
}
parameters := url.Values{}
for key := range params {
parameters.Add(key, params[key])
}
u.RawQuery = parameters.Encode()
return u.String(), nil
}
// ProxyEnv returns shell environment variables to set proxy
func ProxyEnv() string {
httpProxyEnv := "env"
keys := []string{
"http_proxy",
"https_proxy",
"HTTP_PROXY",
"HTTPS_PROXY",
}
for _, key := range keys {
httpProxyEnv += fmt.Sprintf(
` %s="%s"`, key, config.Conf.HTTPProxy)
}
return httpProxyEnv
}
// PrependProxyEnv prepends proxy enviroment variable
func PrependProxyEnv(cmd string) string {
if config.Conf.HTTPProxy == "" {
return cmd
}
return fmt.Sprintf("%s %s", ProxyEnv(), cmd)
}
// func unixtime(s string) (time.Time, error) {
// i, err := strconv.ParseInt(s, 10, 64)
// if err != nil {
// return time.Time{}, err
// }
// return time.Unix(i, 0), nil
// }

133
util/util_test.go Normal file
View File

@@ -0,0 +1,133 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package util
import (
"testing"
"github.com/future-architect/vuls/config"
)
func TestUrlJoin(t *testing.T) {
var tests = []struct {
in []string
out string
}{
{
[]string{
"http://hoge.com:8080",
"status",
},
"http://hoge.com:8080/status",
},
{
[]string{
"http://hoge.com:8080/",
"status",
},
"http://hoge.com:8080/status",
},
{
[]string{
"http://hoge.com:8080",
"/status",
},
"http://hoge.com:8080/status",
},
{
[]string{
"http://hoge.com:8080/",
"/status",
},
"http://hoge.com:8080/status",
},
{
[]string{
"http://hoge.com:8080/",
"/status",
},
"http://hoge.com:8080/status",
},
{
[]string{
"http://hoge.com:8080/",
"",
"hega",
"",
},
"http://hoge.com:8080/hega",
},
{
[]string{
"http://hoge.com:8080/",
"status",
"/fuga",
},
"http://hoge.com:8080/status/fuga",
},
}
for _, tt := range tests {
baseurl := tt.in[0]
paths := tt.in[1:]
actual, err := URLPathJoin(baseurl, paths...)
if err != nil {
t.Errorf("\nunexpected error occurred, err: %s,\ninput:%#v\nexpected: %s\n actual: %s", err, tt.in, tt.out, actual)
}
if actual != tt.out {
t.Errorf("\ninput:%#v\nexpected: %s\n actual: %s", tt.in, tt.out, actual)
}
}
}
func TestPrependHTTPProxyEnv(t *testing.T) {
var tests = []struct {
in []string
out string
}{
{
[]string{
"http://proxy.co.jp:8080",
"yum check-update",
},
`env http_proxy="http://proxy.co.jp:8080" https_proxy="http://proxy.co.jp:8080" HTTP_PROXY="http://proxy.co.jp:8080" HTTPS_PROXY="http://proxy.co.jp:8080" yum check-update`,
},
{
[]string{
"http://proxy.co.jp:8080",
"",
},
`env http_proxy="http://proxy.co.jp:8080" https_proxy="http://proxy.co.jp:8080" HTTP_PROXY="http://proxy.co.jp:8080" HTTPS_PROXY="http://proxy.co.jp:8080" `,
},
{
[]string{
"",
"yum check-update",
},
`yum check-update`,
},
}
for _, tt := range tests {
config.Conf.HTTPProxy = tt.in[0]
actual := PrependProxyEnv(tt.in[1])
if actual != tt.out {
t.Errorf("\nexpected: %s\n actual: %s", tt.out, actual)
}
}
}

24
version.go Normal file
View File

@@ -0,0 +1,24 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
// Name.
const Name string = "vuls"
// Version.
const Version string = "0.1.0"