diff --git a/README.ja.md b/README.ja.md index b443bd756a..c8a43036f1 100644 --- a/README.ja.md +++ b/README.ja.md @@ -835,6 +835,7 @@ report: [-to-azure-blob] [-format-json] [-format-xml] + [-format-one-email] [-format-one-line-text] [-format-short-text] [-format-full-text] @@ -881,6 +882,8 @@ report: Detail report in plain text -format-json JSON format + -format-one-email + Send all the host report via only one EMail (Specify with -to-email) -format-one-line-text One line summary in plain text -format-short-text diff --git a/README.md b/README.md index d3802e6472..f2af1f50b5 100644 --- a/README.md +++ b/README.md @@ -844,6 +844,7 @@ report: [-to-azure-blob] [-format-json] [-format-xml] + [-format-one-email] [-format-one-line-text] [-format-short-text] [-format-full-text] @@ -890,6 +891,8 @@ report: Detail report in plain text -format-json JSON format + -format-one-email + Send all the host report via only one EMail (Specify with -to-email) -format-one-line-text One line summary in plain text -format-short-text diff --git a/commands/report.go b/commands/report.go index ba327a5c24..760ee31de7 100644 --- a/commands/report.go +++ b/commands/report.go @@ -58,6 +58,7 @@ type ReportCmd struct { formatJSON bool formatXML bool + formatOneEMail bool formatOneLineText bool formatShortText bool formatFullText bool @@ -102,6 +103,7 @@ func (*ReportCmd) Usage() string { [-to-azure-blob] [-format-json] [-format-xml] + [-format-one-email] [-format-one-line-text] [-format-short-text] [-format-full-text] @@ -191,6 +193,11 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { false, fmt.Sprintf("XML format")) + f.BoolVar(&p.formatOneEMail, + "format-one-email", + false, + "Send all the host report via only one EMail (Specify with -to-email)") + f.BoolVar(&p.formatOneLineText, "format-one-line-text", false, @@ -274,6 +281,7 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} c.Conf.FormatXML = p.formatXML c.Conf.FormatJSON = p.formatJSON + c.Conf.FormatOneEMail = p.formatOneEMail c.Conf.FormatOneLineText = p.formatOneLineText c.Conf.FormatShortText = p.formatShortText c.Conf.FormatFullText = p.formatFullText diff --git a/config/config.go b/config/config.go index 628502cbd9..dd7ab95858 100644 --- a/config/config.go +++ b/config/config.go @@ -36,7 +36,7 @@ type Config struct { DebugSQL bool Lang string - EMail smtpConf + EMail SMTPConf Slack SlackConf Default ServerInfo Servers map[string]ServerInfo @@ -60,6 +60,7 @@ type Config struct { FormatXML bool FormatJSON bool + FormatOneEMail bool FormatOneLineText bool FormatShortText bool FormatFullText bool @@ -216,8 +217,8 @@ func (c Config) ValidateOnTui() bool { return len(errs) == 0 } -// smtpConf is smtp config -type smtpConf struct { +// SMTPConf is smtp config +type SMTPConf struct { SMTPAddr string SMTPPort string `valid:"port"` @@ -244,7 +245,7 @@ func checkEmails(emails []string) (errs []error) { } // Validate SMTP configuration -func (c *smtpConf) Validate() (errs []error) { +func (c *SMTPConf) Validate() (errs []error) { if !c.UseThisTime { return @@ -398,5 +399,5 @@ type Container struct { ContainerID string Name string Type string - Image string + Image string } diff --git a/report/email.go b/report/email.go index cd17ec4e9f..b06e8ede3d 100644 --- a/report/email.go +++ b/report/email.go @@ -34,54 +34,104 @@ type EMailWriter struct{} func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { conf := config.Conf - to := strings.Join(conf.EMail.To[:], ", ") - cc := strings.Join(conf.EMail.Cc[:], ", ") - mailAddresses := append(conf.EMail.To, conf.EMail.Cc...) - if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil { - return fmt.Errorf("Failed to parse email addresses: %s", err) - } + var message string + var totalResult models.ScanResult + sender := NewEMailSender() for _, r := range rs { - var subject string - if len(r.Errors) != 0 { - subject = fmt.Sprintf("%s%s An error occurred while scanning", - conf.EMail.SubjectPrefix, r.ServerInfo()) + if conf.FormatOneEMail { + message += toFullPlainText(r) + "\r\n\r\n" + totalResult.KnownCves = append(totalResult.KnownCves, r.KnownCves...) + totalResult.UnknownCves = append(totalResult.UnknownCves, r.UnknownCves...) } else { - subject = fmt.Sprintf("%s%s %s", - conf.EMail.SubjectPrefix, r.ServerInfo(), r.CveSummary()) + var subject string + if len(r.Errors) != 0 { + subject = fmt.Sprintf("%s%s An error occurred while scanning", + conf.EMail.SubjectPrefix, r.ServerInfo()) + } else { + subject = fmt.Sprintf("%s%s %s", + conf.EMail.SubjectPrefix, r.ServerInfo(), r.CveSummary()) + } + message = toFullPlainText(r) + if err := sender.Send(subject, message); err != nil { + return err + } } + } - headers := make(map[string]string) - headers["From"] = conf.EMail.From - headers["To"] = to - headers["Cc"] = cc - headers["Subject"] = subject - headers["Date"] = time.Now().Format(time.RFC1123Z) - headers["Content-Type"] = "text/plain; charset=utf-8" - - var message string - for k, v := range headers { - message += fmt.Sprintf("%s: %s\r\n", k, v) - } - message += "\r\n" + toFullPlainText(r) - - smtpServer := net.JoinHostPort(conf.EMail.SMTPAddr, conf.EMail.SMTPPort) - err = smtp.SendMail( - smtpServer, - smtp.PlainAuth( - "", - conf.EMail.User, - conf.EMail.Password, - conf.EMail.SMTPAddr, - ), - conf.EMail.From, - conf.EMail.To, - []byte(message), + if conf.FormatOneEMail { + message = fmt.Sprintf( + ` +One Line Summary +================ +%s + + +%s`, + toOneLineSummary(rs...), message) + + subject := fmt.Sprintf("%s %s", + conf.EMail.SubjectPrefix, + totalResult.CveSummary(), ) + return sender.Send(subject, message) + } + return nil +} - if err != nil { - return fmt.Errorf("Failed to send emails: %s", err) - } +// EMailSender is interface of sending e-mail +type EMailSender interface { + Send(subject, body string) error +} + +type emailSender struct { + conf config.SMTPConf + send func(string, smtp.Auth, string, []string, []byte) error +} + +func (e *emailSender) Send(subject, body string) (err error) { + emailConf := e.conf + to := strings.Join(emailConf.To[:], ", ") + cc := strings.Join(emailConf.Cc[:], ", ") + mailAddresses := append(emailConf.To, emailConf.Cc...) + if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil { + return fmt.Errorf("Failed to parse email addresses: %s", err) + } + + headers := make(map[string]string) + headers["From"] = emailConf.From + headers["To"] = to + headers["Cc"] = cc + headers["Subject"] = subject + headers["Date"] = time.Now().Format(time.RFC1123Z) + headers["Content-Type"] = "text/plain; charset=utf-8" + + var header string + for k, v := range headers { + header += fmt.Sprintf("%s: %s\r\n", k, v) + } + message := fmt.Sprintf("%s\r\n%s", header, body) + + smtpServer := net.JoinHostPort(emailConf.SMTPAddr, emailConf.SMTPPort) + err = e.send( + smtpServer, + smtp.PlainAuth( + "", + emailConf.User, + emailConf.Password, + emailConf.SMTPAddr, + ), + emailConf.From, + emailConf.To, + []byte(message), + ) + if err != nil { + return fmt.Errorf("Failed to send emails: %s", err) } return nil } + +// NewEMailSender creates emailSender +func NewEMailSender() EMailSender { + return &emailSender{config.Conf.EMail, smtp.SendMail} +} diff --git a/report/email_test.go b/report/email_test.go new file mode 100644 index 0000000000..893a46ad33 --- /dev/null +++ b/report/email_test.go @@ -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 . +*/ + +package report + +import ( + "net/smtp" + "reflect" + "strings" + "testing" + + "github.com/future-architect/vuls/config" +) + +type emailRecorder struct { + addr string + auth smtp.Auth + from string + to []string + body string +} + +type mailTest struct { + in config.SMTPConf + out emailRecorder +} + +var mailTests = []mailTest{ + { + config.SMTPConf{ + SMTPAddr: "127.0.0.1", + SMTPPort: "25", + + From: "from@address.com", + To: []string{"to@address.com"}, + }, + emailRecorder{ + addr: "127.0.0.1:25", + auth: smtp.PlainAuth("", "", "", "127.0.0.1"), + from: "from@address.com", + to: []string{"to@address.com"}, + body: "body", + }, + }, + { + config.SMTPConf{ + SMTPAddr: "127.0.0.1", + SMTPPort: "25", + + User: "vuls", + Password: "password", + + From: "from@address.com", + To: []string{"to1@address.com", "to2@address.com"}, + }, + emailRecorder{ + addr: "127.0.0.1:25", + auth: smtp.PlainAuth( + "", + "vuls", + "password", + "127.0.0.1", + ), + from: "from@address.com", + to: []string{"to1@address.com", "to2@address.com"}, + body: "body", + }, + }, +} + +func TestSend(t *testing.T) { + for i, test := range mailTests { + f, r := mockSend(nil) + sender := &emailSender{conf: test.in, send: f} + + subject := "subject" + body := "body" + if err := sender.Send(subject, body); err != nil { + t.Errorf("unexpected error: %s", err) + } + + if r.addr != test.out.addr { + t.Errorf("#%d: wrong 'addr' field.\r\nexpected: %s\n got: %s", i, test.out.addr, r.addr) + } + + if !reflect.DeepEqual(r.auth, test.out.auth) { + t.Errorf("#%d: wrong 'auth' field.\r\nexpected: %v\n got: %v", i, test.out.auth, r.auth) + } + + if r.from != test.out.from { + t.Errorf("#%d: wrong 'from' field.\r\nexpected: %v\n got: %v", i, test.out.from, r.from) + } + + if !reflect.DeepEqual(r.to, test.out.to) { + t.Errorf("#%d: wrong 'to' field.\r\nexpected: %v\n got: %v", i, test.out.to, r.to) + } + + if r.body != test.out.body { + t.Errorf("#%d: wrong 'body' field.\r\nexpected: %v\n got: %v", i, test.out.body, r.body) + } + + } + +} + +func mockSend(errToReturn error) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) { + r := new(emailRecorder) + return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error { + // Split into header and body + messages := strings.Split(string(msg), "\r\n\r\n") + body := messages[1] + *r = emailRecorder{addr, a, from, to, body} + return errToReturn + }, r +}