Support a reporting via Syslog (#604)

* Support a reporting via syslog

* Update dependencies
This commit is contained in:
Teppei Fukuda
2018-02-27 20:38:34 +09:00
committed by Kota Kanbe
parent 0653656526
commit b08969ad89
9 changed files with 410 additions and 22 deletions

43
Gopkg.lock generated
View File

@@ -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

View File

@@ -101,8 +101,8 @@
version = "2.2.0"
[[constraint]]
branch = "master"
name = "github.com/sirupsen/logrus"
version = "1.0.4"
[[constraint]]
branch = "master"

View File

@@ -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,

View File

@@ -18,7 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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

63
config/config_test.go Normal file
View File

@@ -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))
}
}
}

View File

@@ -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

97
report/syslog.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

93
report/syslog_test.go Normal file
View File

@@ -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)
}
}
}
}

View File

@@ -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