From b08969ad890cec982dcb53932e764ec29ced28ca Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Tue, 27 Feb 2018 20:38:34 +0900 Subject: [PATCH] Support a reporting via Syslog (#604) * Support a reporting via syslog * Update dependencies --- Gopkg.lock | 43 ++++++++------- Gopkg.toml | 2 +- commands/report.go | 6 ++ config/config.go | 125 ++++++++++++++++++++++++++++++++++++++++++ config/config_test.go | 63 +++++++++++++++++++++ config/tomlloader.go | 1 + report/syslog.go | 97 ++++++++++++++++++++++++++++++++ report/syslog_test.go | 93 +++++++++++++++++++++++++++++++ scan/freebsd.go | 2 +- 9 files changed, 410 insertions(+), 22 deletions(-) create mode 100644 config/config_test.go create mode 100644 report/syslog.go create mode 100644 report/syslog_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 7fd151c8..a3e0571b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -15,8 +15,8 @@ "autorest/azure", "autorest/date" ] - revision = "c2a68353555b68de3ee8455a4fd3e890a0ac6d99" - version = "v9.8.1" + revision = "fc3b03a2d2d1f43fad3007038bd16f044f870722" + version = "v9.10.0" [[projects]] name = "github.com/BurntSushi/toml" @@ -49,6 +49,7 @@ "aws/request", "aws/session", "aws/signer/v4", + "internal/sdkrand", "internal/shareddefaults", "private/protocol", "private/protocol/query", @@ -59,8 +60,8 @@ "service/s3", "service/sts" ] - revision = "decd990ddc5dcdf2f73309cbcab90d06b996ca28" - version = "v1.12.67" + revision = "adeb60566bc8c9202b0f1be7e3a675beedbc11f0" + version = "v1.13.2" [[projects]] name = "github.com/boltdb/bolt" @@ -75,10 +76,10 @@ version = "v1.1.0" [[projects]] + branch = "master" name = "github.com/cheggaaa/pb" packages = ["."] - revision = "5119518d88fbf604cab04dafd667dc7022e3b5ac" - version = "v1.0.21" + revision = "521e54ab5f0d0e5260964d094a414759c65fcbb3" [[projects]] name = "github.com/dgrijalva/jwt-go" @@ -100,10 +101,11 @@ "internal/consistenthash", "internal/hashtag", "internal/pool", - "internal/proto" + "internal/proto", + "internal/singleflight" ] - revision = "4021ace05686f632ff17fd824bbed229fc474cf8" - version = "v6.8.2" + revision = "fa7f64f7f27348658ecfa71dd71dfb2d112e2f86" + version = "v6.9.0" [[projects]] name = "github.com/go-sql-driver/mysql" @@ -191,7 +193,7 @@ "nvd", "util" ] - revision = "c640d74072007e0b3b86968388c12c54acd8f97e" + revision = "fde71467f9c6a941490af81dd1d34e8bff0aba01" [[projects]] name = "github.com/kotakanbe/go-pingscanner" @@ -209,7 +211,7 @@ "log", "models" ] - revision = "eda9803e0e770516046db375c44b558de883856c" + revision = "85c10368a38d0c020d76b8bd93155f00324dcb08" [[projects]] branch = "master" @@ -225,7 +227,7 @@ "hstore", "oid" ] - revision = "61fe37aa2ee24fabcdbe5c4ac1d4ac566f88f345" + revision = "88edab0803230a3898347e77b474f8c1820a1f20" [[projects]] name = "github.com/marstr/guid" @@ -278,7 +280,7 @@ branch = "master" name = "github.com/nsf/termbox-go" packages = ["."] - revision = "157ff97de075fe3e9e54bda782b8ca6b552dfff7" + revision = "88b7b944be8bc8d8ec6195fca97c5869ba20f99d" [[projects]] name = "github.com/parnurzeal/gorequest" @@ -305,9 +307,10 @@ version = "v1.2.0" [[projects]] + branch = "master" name = "github.com/sirupsen/logrus" packages = ["."] - revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" + revision = "8c0189d9f6bbf301e5d055d34268156b317016af" [[projects]] branch = "master" @@ -328,7 +331,7 @@ "ssh/agent", "ssh/terminal" ] - revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b" + revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" [[projects]] branch = "master" @@ -339,7 +342,7 @@ "publicsuffix", "websocket" ] - revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec" + revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" [[projects]] branch = "master" @@ -348,10 +351,9 @@ "unix", "windows" ] - revision = "af50095a40f9041b3b38960738837185c26e9419" + revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" [[projects]] - branch = "master" name = "golang.org/x/text" packages = [ "collate", @@ -369,11 +371,12 @@ "unicode/norm", "unicode/rangetable" ] - revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "bfe830562c6df05e01e16b1337e1e4d97844ae0235cfda39c55eee38ba838f68" + inputs-digest = "1a43293a9ffc91270316199bec5474167594f96a3612f899842532c0f224d694" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 2eb487ef..afc118fc 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -101,8 +101,8 @@ version = "2.2.0" [[constraint]] + branch = "master" name = "github.com/sirupsen/logrus" - version = "1.0.4" [[constraint]] branch = "master" diff --git a/commands/report.go b/commands/report.go index d18698c5..3a4b4a7e 100644 --- a/commands/report.go +++ b/commands/report.go @@ -58,6 +58,7 @@ type ReportCmd struct { toSlack bool toEMail bool + toSyslog bool toLocalFile bool toS3 bool toAzureBlob bool @@ -264,6 +265,7 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.toSlack, "to-slack", false, "Send report via Slack") f.BoolVar(&p.toEMail, "to-email", false, "Send report via Email") + f.BoolVar(&p.toSyslog, "to-syslog", false, "Send report via Syslog") f.BoolVar(&p.toLocalFile, "to-localfile", false, @@ -363,6 +365,10 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} reports = append(reports, report.EMailWriter{}) } + if p.toSyslog { + reports = append(reports, report.SyslogWriter{}) + } + if p.toLocalFile { reports = append(reports, report.LocalFileWriter{ CurrentDir: dir, diff --git a/config/config.go b/config/config.go index 30255f2b..76c0e38e 100644 --- a/config/config.go +++ b/config/config.go @@ -18,7 +18,9 @@ along with this program. If not, see . package config import ( + "errors" "fmt" + "log/syslog" "os" "runtime" "strconv" @@ -94,6 +96,7 @@ type Config struct { EMail SMTPConf Slack SlackConf + Syslog SyslogConf Default ServerInfo Servers map[string]ServerInfo @@ -260,6 +263,10 @@ func (c Config) ValidateOnReport() bool { errs = append(errs, slackerrs...) } + if syslogerrs := c.Syslog.Validate(); 0 < len(syslogerrs) { + errs = append(errs, syslogerrs...) + } + for _, err := range errs { log.Error(err) } @@ -444,6 +451,124 @@ func (c *SlackConf) Validate() (errs []error) { return } +// SyslogConf is syslog config +type SyslogConf struct { + Protocol string + Host string `valid:"host"` + Port string `valid:"port"` + Severity string + Facility string + Tag string + + Verbose bool +} + +// Validate validates configuration +func (c *SyslogConf) Validate() (errs []error) { + // If protocol is empty, it will connect to the local syslog server. + if len(c.Protocol) > 0 && c.Protocol != "tcp" && c.Protocol != "udp" { + errs = append(errs, errors.New(`protocol must be "tcp" or "udp"`)) + } + + // Default port: 514 + if c.Port == "" { + c.Port = "514" + } + + if _, err := c.GetSeverity(); err != nil { + errs = append(errs, err) + } + + if _, err := c.GetFacility(); err != nil { + errs = append(errs, err) + } + + if _, err := valid.ValidateStruct(c); err != nil { + errs = append(errs, err) + } + return errs +} + +// GetSeverity gets severity +func (c *SyslogConf) GetSeverity() (syslog.Priority, error) { + if c.Severity == "" { + return syslog.LOG_INFO, nil + } + + switch c.Severity { + case "emerg": + return syslog.LOG_EMERG, nil + case "alert": + return syslog.LOG_ALERT, nil + case "crit": + return syslog.LOG_CRIT, nil + case "err": + return syslog.LOG_ERR, nil + case "warning": + return syslog.LOG_WARNING, nil + case "notice": + return syslog.LOG_NOTICE, nil + case "info": + return syslog.LOG_INFO, nil + case "debug": + return syslog.LOG_DEBUG, nil + default: + return -1, fmt.Errorf("Invalid severity: %s", c.Severity) + } +} + +// GetFacility gets facility +func (c *SyslogConf) GetFacility() (syslog.Priority, error) { + if c.Facility == "" { + return syslog.LOG_AUTH, nil + } + + switch c.Facility { + case "kern": + return syslog.LOG_KERN, nil + case "user": + return syslog.LOG_USER, nil + case "mail": + return syslog.LOG_MAIL, nil + case "daemon": + return syslog.LOG_DAEMON, nil + case "auth": + return syslog.LOG_AUTH, nil + case "syslog": + return syslog.LOG_SYSLOG, nil + case "lpr": + return syslog.LOG_LPR, nil + case "news": + return syslog.LOG_NEWS, nil + case "uucp": + return syslog.LOG_UUCP, nil + case "cron": + return syslog.LOG_CRON, nil + case "authpriv": + return syslog.LOG_AUTHPRIV, nil + case "ftp": + return syslog.LOG_FTP, nil + case "local0": + return syslog.LOG_LOCAL0, nil + case "local1": + return syslog.LOG_LOCAL1, nil + case "local2": + return syslog.LOG_LOCAL2, nil + case "local3": + return syslog.LOG_LOCAL3, nil + case "local4": + return syslog.LOG_LOCAL4, nil + case "local5": + return syslog.LOG_LOCAL5, nil + case "local6": + return syslog.LOG_LOCAL6, nil + case "local7": + return syslog.LOG_LOCAL7, nil + default: + return -1, fmt.Errorf("Invalid facility: %s", c.Facility) + } +} + // ServerInfo has SSH Info, additional CPE packages to scan. type ServerInfo struct { ServerName string diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..6c2c517b --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,63 @@ +package config + +import ( + "testing" +) + +func TestSyslogConfValidate(t *testing.T) { + var tests = []struct { + conf SyslogConf + expectedErrLength int + }{ + { + conf: SyslogConf{}, + expectedErrLength: 0, + }, + { + conf: SyslogConf{ + Protocol: "tcp", + Port: "5140", + }, + expectedErrLength: 0, + }, + { + conf: SyslogConf{ + Protocol: "udp", + Port: "12345", + Severity: "emerg", + Facility: "user", + }, + expectedErrLength: 0, + }, + { + conf: SyslogConf{ + Protocol: "foo", + Port: "514", + }, + expectedErrLength: 1, + }, + { + conf: SyslogConf{ + Protocol: "invalid", + Port: "-1", + }, + expectedErrLength: 2, + }, + { + conf: SyslogConf{ + Protocol: "invalid", + Port: "invalid", + Severity: "invalid", + Facility: "invalid", + }, + expectedErrLength: 4, + }, + } + + for i, tt := range tests { + errs := tt.conf.Validate() + if len(errs) != tt.expectedErrLength { + t.Errorf("test: %d, expected %d, actual %d", i, tt.expectedErrLength, len(errs)) + } + } +} diff --git a/config/tomlloader.go b/config/tomlloader.go index 071c0f94..3ed52ce1 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -44,6 +44,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { Conf.EMail = conf.EMail Conf.Slack = conf.Slack + Conf.Syslog = conf.Syslog d := conf.Default Conf.Default = d diff --git a/report/syslog.go b/report/syslog.go new file mode 100644 index 00000000..ee5bd30f --- /dev/null +++ b/report/syslog.go @@ -0,0 +1,97 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2018 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 . +*/ + +package report + +import ( + "fmt" + "log/syslog" + "strings" + + "github.com/pkg/errors" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" +) + +// SyslogWriter send report to syslog +type SyslogWriter struct{} + +func (w SyslogWriter) Write(rs ...models.ScanResult) (err error) { + conf := config.Conf.Syslog + facility, _ := conf.GetFacility() + severity, _ := conf.GetSeverity() + raddr := fmt.Sprintf("%s:%s", conf.Host, conf.Port) + + sysLog, err := syslog.Dial(conf.Protocol, raddr, severity|facility, conf.Tag) + if err != nil { + return errors.Wrap(err, "Failed to initialize syslog client") + } + + for _, r := range rs { + messages := w.encodeSyslog(r) + for _, m := range messages { + if _, err = fmt.Fprintf(sysLog, m); err != nil { + return err + } + } + } + return nil +} + +func (w SyslogWriter) encodeSyslog(result models.ScanResult) (messages []string) { + ipv4Addrs := strings.Join(result.IPv4Addrs, ",") + ipv6Addrs := strings.Join(result.IPv6Addrs, ",") + + for cveID, vinfo := range result.ScannedCves { + var kvPairs []string + kvPairs = append(kvPairs, fmt.Sprintf(`server_name="%s"`, result.ServerName)) + kvPairs = append(kvPairs, fmt.Sprintf(`os_family="%s"`, result.Family)) + kvPairs = append(kvPairs, fmt.Sprintf(`os_release="%s"`, result.Release)) + kvPairs = append(kvPairs, fmt.Sprintf(`ipv4_addr="%s"`, ipv4Addrs)) + kvPairs = append(kvPairs, fmt.Sprintf(`ipv6_addr="%s"`, ipv6Addrs)) + + var pkgNames []string + for _, pkg := range vinfo.AffectedPackages { + pkgNames = append(pkgNames, pkg.Name) + } + pkgs := strings.Join(pkgNames, ",") + kvPairs = append(kvPairs, fmt.Sprintf(`packages="%s"`, pkgs)) + + kvPairs = append(kvPairs, fmt.Sprintf(`cve_id="%s"`, cveID)) + for _, cvss := range vinfo.Cvss2Scores() { + if cvss.Type != models.NVD { + continue + } + kvPairs = append(kvPairs, fmt.Sprintf(`severity="%s"`, cvss.Value.Severity)) + kvPairs = append(kvPairs, fmt.Sprintf(`cvss_score_v2="%.2f"`, cvss.Value.Score)) + kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_v2="%s"`, cvss.Value.Vector)) + } + + if content, ok := vinfo.CveContents[models.NVD]; ok { + kvPairs = append(kvPairs, fmt.Sprintf(`cwe_id="%s"`, content.CweID)) + if config.Conf.Syslog.Verbose { + kvPairs = append(kvPairs, fmt.Sprintf(`source_link="%s"`, content.SourceLink)) + kvPairs = append(kvPairs, fmt.Sprintf(`summary="%s"`, content.Summary)) + } + } + + // message: key1="value1" key2="value2"... + messages = append(messages, strings.Join(kvPairs, " ")) + } + return messages +} diff --git a/report/syslog_test.go b/report/syslog_test.go new file mode 100644 index 00000000..6e18417b --- /dev/null +++ b/report/syslog_test.go @@ -0,0 +1,93 @@ +package report + +import ( + "sort" + "testing" + + "github.com/future-architect/vuls/models" +) + +func TestSyslogWriterEncodeSyslog(t *testing.T) { + var tests = []struct { + result models.ScanResult + expectedMessages []string + }{ + { + result: models.ScanResult{ + ServerName: "teste01", + Family: "ubuntu", + Release: "16.04", + IPv4Addrs: []string{"192.168.0.1", "10.0.2.15"}, + ScannedCves: models.VulnInfos{ + "CVE-2017-0001": models.VulnInfo{ + AffectedPackages: models.PackageStatuses{ + models.PackageStatus{Name: "pkg1"}, + models.PackageStatus{Name: "pkg2"}, + }, + }, + "CVE-2017-0002": models.VulnInfo{ + AffectedPackages: models.PackageStatuses{ + models.PackageStatus{Name: "pkg3"}, + models.PackageStatus{Name: "pkg4"}, + }, + CveContents: models.CveContents{ + models.NVD: models.CveContent{ + Cvss2Score: 5.0, + Cvss2Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C", + CweID: "CWE-20", + }, + }, + }, + }, + }, + expectedMessages: []string{ + `server_name="teste01" os_family="ubuntu" os_release="16.04" ipv4_addr="192.168.0.1,10.0.2.15" ipv6_addr="" packages="pkg1,pkg2" cve_id="CVE-2017-0001"`, + `server_name="teste01" os_family="ubuntu" os_release="16.04" ipv4_addr="192.168.0.1,10.0.2.15" ipv6_addr="" packages="pkg3,pkg4" cve_id="CVE-2017-0002" severity="MEDIUM" cvss_score_v2="5.00" cvss_vector_v2="AV:L/AC:L/Au:N/C:N/I:N/A:C" cwe_id="CWE-20"`, + }, + }, + { + result: models.ScanResult{ + ServerName: "teste02", + Family: "centos", + Release: "6", + IPv6Addrs: []string{"2001:0DB8::1"}, + ScannedCves: models.VulnInfos{ + "CVE-2017-0003": models.VulnInfo{ + AffectedPackages: models.PackageStatuses{ + models.PackageStatus{Name: "pkg5"}, + }, + CveContents: models.CveContents{ + models.RedHat: models.CveContent{ + Cvss3Score: 5.0, + Cvss3Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C", + CweID: "CWE-284", + }, + }, + }, + }, + }, + expectedMessages: []string{ + `server_name="teste02" os_family="centos" os_release="6" ipv4_addr="" ipv6_addr="2001:0DB8::1" packages="pkg5" cve_id="CVE-2017-0003"`, + }, + }, + } + + for i, tt := range tests { + messages := SyslogWriter{}.encodeSyslog(tt.result) + if len(messages) != len(tt.expectedMessages) { + t.Fatalf("test: %d, Message Length: expected %d, actual: %d", + i, len(tt.expectedMessages), len(messages)) + } + + sort.Slice(messages, func(i, j int) bool { + return messages[i] < messages[j] + }) + + for j, m := range messages { + e := tt.expectedMessages[j] + if e != m { + t.Errorf("test: %d, Messsage %d: expected %s, actual %s", i, j, e, m) + } + } + } +} diff --git a/scan/freebsd.go b/scan/freebsd.go index 5446b801..cfb988de 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -79,7 +79,7 @@ func (o *bsd) checkDependencies() error { } func (o *bsd) preCure() error { - if err := o.detectIPAddr(); err != nil{ + if err := o.detectIPAddr(); err != nil { o.log.Debugf("Failed to detect IP addresses: %s", err) } // Ignore this error as it just failed to detect the IP addresses