refactor(config): localize config used like a global variable (#1179)
* refactor(report): LocalFileWriter * refactor -format-json * refacotr: -format-one-email * refactor: -format-csv * refactor: -gzip * refactor: -format-full-text * refactor: -format-one-line-text * refactor: -format-list * refacotr: remove -to-* from config * refactor: IgnoreGitHubDismissed * refactor: GitHub * refactor: IgnoreUnsocred * refactor: diff * refacotr: lang * refacotr: cacheDBPath * refactor: Remove config references * refactor: ScanResults * refacotr: constant pkg * chore: comment * refactor: scanner * refactor: scanner * refactor: serverapi.go * refactor: serverapi * refactor: change pkg structure * refactor: serverapi.go * chore: remove emtpy file * fix(scan): remove -ssh-native-insecure option * fix(scan): remove the deprecated option `keypassword`
This commit is contained in:
		
							
								
								
									
										126
									
								
								reporter/azureblob.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								reporter/azureblob.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	storage "github.com/Azure/azure-sdk-for-go/storage"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
 | 
			
		||||
	c "github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AzureBlobWriter writes results to AzureBlob
 | 
			
		||||
type AzureBlobWriter struct {
 | 
			
		||||
	FormatJSON        bool
 | 
			
		||||
	FormatFullText    bool
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	FormatList        bool
 | 
			
		||||
	Gzip              bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write results to Azure Blob storage
 | 
			
		||||
func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	if len(rs) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cli, err := getBlobClient()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if w.FormatOneLineText {
 | 
			
		||||
		timestr := rs[0].ScannedAt.Format(time.RFC3339)
 | 
			
		||||
		k := fmt.Sprintf(timestr + "/summary.txt")
 | 
			
		||||
		text := formatOneLineSummary(rs...)
 | 
			
		||||
		b := []byte(text)
 | 
			
		||||
		if err := createBlockBlob(cli, k, b, w.Gzip); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		key := r.ReportKeyName()
 | 
			
		||||
		if w.FormatJSON {
 | 
			
		||||
			k := key + ".json"
 | 
			
		||||
			var b []byte
 | 
			
		||||
			if b, err = json.Marshal(r); err != nil {
 | 
			
		||||
				return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := createBlockBlob(cli, k, b, w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatList {
 | 
			
		||||
			k := key + "_short.txt"
 | 
			
		||||
			b := []byte(formatList(r))
 | 
			
		||||
			if err := createBlockBlob(cli, k, b, w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatFullText {
 | 
			
		||||
			k := key + "_full.txt"
 | 
			
		||||
			b := []byte(formatFullPlainText(r))
 | 
			
		||||
			if err := createBlockBlob(cli, k, b, w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckIfAzureContainerExists check the existence of Azure storage container
 | 
			
		||||
func CheckIfAzureContainerExists() error {
 | 
			
		||||
	cli, err := getBlobClient()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	r, err := cli.ListContainers(storage.ListContainersParameters{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found := false
 | 
			
		||||
	for _, con := range r.Containers {
 | 
			
		||||
		if con.Name == c.Conf.Azure.ContainerName {
 | 
			
		||||
			found = true
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !found {
 | 
			
		||||
		return xerrors.Errorf("Container not found. Container: %s", c.Conf.Azure.ContainerName)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getBlobClient() (storage.BlobStorageClient, error) {
 | 
			
		||||
	api, err := storage.NewBasicClient(c.Conf.Azure.AccountName, c.Conf.Azure.AccountKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return storage.BlobStorageClient{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return api.GetBlobService(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte, gzip bool) error {
 | 
			
		||||
	var err error
 | 
			
		||||
	if gzip {
 | 
			
		||||
		if b, err = gz(b); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		k += ".gz"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ref := cli.GetContainerReference(c.Conf.Azure.ContainerName)
 | 
			
		||||
	blob := ref.GetBlobReference(k)
 | 
			
		||||
	if err := blob.CreateBlockBlobFromReader(bytes.NewReader(b), nil); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to upload data to %s/%s, err: %w",
 | 
			
		||||
			c.Conf.Azure.ContainerName, k, err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								reporter/chatwork.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								reporter/chatwork.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ChatWorkWriter send report to ChatWork
 | 
			
		||||
type ChatWorkWriter struct{}
 | 
			
		||||
 | 
			
		||||
func (w ChatWorkWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	conf := config.Conf.ChatWork
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		serverInfo := fmt.Sprintf("%s", r.ServerInfo())
 | 
			
		||||
		if err = chatWorkpostMessage(conf.Room, conf.APIToken, serverInfo); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, vinfo := range r.ScannedCves {
 | 
			
		||||
			maxCvss := vinfo.MaxCvssScore()
 | 
			
		||||
			severity := strings.ToUpper(maxCvss.Value.Severity)
 | 
			
		||||
			if severity == "" {
 | 
			
		||||
				severity = "?"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			message := fmt.Sprintf(`%s[info][title]"https://nvd.nist.gov/vuln/detail/%s" %s %s[/title]%s[/info]`,
 | 
			
		||||
				serverInfo,
 | 
			
		||||
				vinfo.CveID,
 | 
			
		||||
				strconv.FormatFloat(maxCvss.Value.Score, 'f', 1, 64),
 | 
			
		||||
				severity,
 | 
			
		||||
				vinfo.Summaries(r.Lang, r.Family)[0].Value)
 | 
			
		||||
 | 
			
		||||
			if err = chatWorkpostMessage(conf.Room, conf.APIToken, message); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func chatWorkpostMessage(room, token, message string) error {
 | 
			
		||||
	uri := fmt.Sprintf("https://api.chatwork.com/v2/rooms/%s/messages=%s", room, token)
 | 
			
		||||
	payload := url.Values{"body": {message}}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, strings.NewReader(payload.Encode()))
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Add("X-ChatWorkToken", token)
 | 
			
		||||
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 | 
			
		||||
 | 
			
		||||
	client, err := util.GetHTTPClient(config.Conf.HTTPProxy)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										215
									
								
								reporter/email.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								reporter/email.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,215 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/mail"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	sasl "github.com/emersion/go-sasl"
 | 
			
		||||
	smtp "github.com/emersion/go-smtp"
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EMailWriter send mail
 | 
			
		||||
type EMailWriter struct {
 | 
			
		||||
	FormatOneEMail    bool
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	FormatList        bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w EMailWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	conf := config.Conf
 | 
			
		||||
	var message string
 | 
			
		||||
	sender := NewEMailSender()
 | 
			
		||||
	m := map[string]int{}
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		if w.FormatOneEMail {
 | 
			
		||||
			message += formatFullPlainText(r) + "\r\n\r\n"
 | 
			
		||||
			mm := r.ScannedCves.CountGroupBySeverity()
 | 
			
		||||
			keys := []string{"High", "Medium", "Low", "Unknown"}
 | 
			
		||||
			for _, k := range keys {
 | 
			
		||||
				m[k] += mm[k]
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			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.ScannedCves.FormatCveSummary())
 | 
			
		||||
			}
 | 
			
		||||
			if w.FormatList {
 | 
			
		||||
				message = formatList(r)
 | 
			
		||||
			} else {
 | 
			
		||||
				message = formatFullPlainText(r)
 | 
			
		||||
			}
 | 
			
		||||
			if w.FormatOneLineText {
 | 
			
		||||
				message = fmt.Sprintf("One Line Summary\r\n================\r\n%s", formatOneLineSummary(r))
 | 
			
		||||
			}
 | 
			
		||||
			if err := sender.Send(subject, message); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	summary := fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
 | 
			
		||||
		m["High"]+m["Medium"]+m["Low"]+m["Unknown"],
 | 
			
		||||
		m["High"], m["Medium"], m["Low"], m["Unknown"])
 | 
			
		||||
 | 
			
		||||
	origmessage := message
 | 
			
		||||
	if w.FormatOneEMail {
 | 
			
		||||
		message = fmt.Sprintf("One Line Summary\r\n================\r\n%s", formatOneLineSummary(rs...))
 | 
			
		||||
		if !w.FormatOneLineText {
 | 
			
		||||
			message += fmt.Sprintf("\r\n\r\n%s", origmessage)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		subject := fmt.Sprintf("%s %s",
 | 
			
		||||
			conf.EMail.SubjectPrefix, summary)
 | 
			
		||||
		return sender.Send(subject, message)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EMailSender is interface of sending e-mail
 | 
			
		||||
type EMailSender interface {
 | 
			
		||||
	Send(subject, body string) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type emailSender struct {
 | 
			
		||||
	conf config.SMTPConf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *emailSender) sendMail(smtpServerAddr, message string) (err error) {
 | 
			
		||||
	var c *smtp.Client
 | 
			
		||||
	var auth sasl.Client
 | 
			
		||||
	emailConf := e.conf
 | 
			
		||||
	//TLS Config
 | 
			
		||||
	tlsConfig := &tls.Config{
 | 
			
		||||
		ServerName: emailConf.SMTPAddr,
 | 
			
		||||
	}
 | 
			
		||||
	switch emailConf.SMTPPort {
 | 
			
		||||
	case "465":
 | 
			
		||||
		//New TLS connection
 | 
			
		||||
		c, err = smtp.DialTLS(smtpServerAddr, tlsConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to create TLS connection to SMTP server: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		c, err = smtp.Dial(smtpServerAddr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to create connection to SMTP server: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	defer c.Close()
 | 
			
		||||
 | 
			
		||||
	if err = c.Hello("localhost"); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to send Hello command: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ok, _ := c.Extension("STARTTLS"); ok {
 | 
			
		||||
		if err := c.StartTLS(tlsConfig); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to STARTTLS: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ok, param := c.Extension("AUTH"); ok {
 | 
			
		||||
		authList := strings.Split(param, " ")
 | 
			
		||||
		auth = e.newSaslClient(authList)
 | 
			
		||||
		if err = c.Auth(auth); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to authenticate: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = c.Mail(emailConf.From, nil); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to send Mail command: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	for _, to := range emailConf.To {
 | 
			
		||||
		if err = c.Rcpt(to); err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to send Rcpt command: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w, err := c.Data()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to send Data command: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	_, err = w.Write([]byte(message))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to write EMail message: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = w.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to close Writer: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = c.Quit()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to close connection: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 xerrors.Errorf("Failed to parse email addresses: %w", 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)
 | 
			
		||||
 | 
			
		||||
	if emailConf.User != "" && emailConf.Password != "" {
 | 
			
		||||
		err = e.sendMail(smtpServer, message)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return xerrors.Errorf("Failed to send emails: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	err = e.sendMail(smtpServer, message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to send emails: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewEMailSender creates emailSender
 | 
			
		||||
func NewEMailSender() EMailSender {
 | 
			
		||||
	return &emailSender{config.Conf.EMail}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *emailSender) newSaslClient(authList []string) sasl.Client {
 | 
			
		||||
	for _, v := range authList {
 | 
			
		||||
		switch v {
 | 
			
		||||
		case "PLAIN":
 | 
			
		||||
			auth := sasl.NewPlainClient("", e.conf.User, e.conf.Password)
 | 
			
		||||
			return auth
 | 
			
		||||
		case "LOGIN":
 | 
			
		||||
			auth := sasl.NewLoginClient(e.conf.User, e.conf.Password)
 | 
			
		||||
			return auth
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								reporter/http.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								reporter/http.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	c "github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HTTPRequestWriter writes results to HTTP request
 | 
			
		||||
type HTTPRequestWriter struct{}
 | 
			
		||||
 | 
			
		||||
// Write sends results as HTTP response
 | 
			
		||||
func (w HTTPRequestWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		b := new(bytes.Buffer)
 | 
			
		||||
		if err := json.NewEncoder(b).Encode(r); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		_, err = http.Post(c.Conf.HTTP.URL, "application/json; charset=utf-8", b)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HTTPResponseWriter writes results to HTTP response
 | 
			
		||||
type HTTPResponseWriter struct {
 | 
			
		||||
	Writer http.ResponseWriter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write sends results as HTTP response
 | 
			
		||||
func (w HTTPResponseWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	res, err := json.Marshal(rs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to marshal scan results: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	w.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	_, err = w.Writer.Write(res)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								reporter/localfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								reporter/localfile.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// LocalFileWriter writes results to a local file.
 | 
			
		||||
type LocalFileWriter struct {
 | 
			
		||||
	CurrentDir        string
 | 
			
		||||
	DiffPlus          bool
 | 
			
		||||
	DiffMinus         bool
 | 
			
		||||
	FormatJSON        bool
 | 
			
		||||
	FormatCsv         bool
 | 
			
		||||
	FormatFullText    bool
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	FormatList        bool
 | 
			
		||||
	Gzip              bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	if w.FormatOneLineText {
 | 
			
		||||
		path := filepath.Join(w.CurrentDir, "summary.txt")
 | 
			
		||||
		text := formatOneLineSummary(rs...)
 | 
			
		||||
		if err := w.writeFile(path, []byte(text), 0600); err != nil {
 | 
			
		||||
			return xerrors.Errorf(
 | 
			
		||||
				"Failed to write to file. path: %s, err: %w",
 | 
			
		||||
				path, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		path := filepath.Join(w.CurrentDir, r.ReportFileName())
 | 
			
		||||
 | 
			
		||||
		if w.FormatJSON {
 | 
			
		||||
			p := path + ".json"
 | 
			
		||||
			if w.DiffPlus || w.DiffMinus {
 | 
			
		||||
				p = path + "_diff.json"
 | 
			
		||||
			}
 | 
			
		||||
			var b []byte
 | 
			
		||||
			if b, err = json.MarshalIndent(r, "", "    "); err != nil {
 | 
			
		||||
				return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := w.writeFile(p, b, 0600); err != nil {
 | 
			
		||||
				return xerrors.Errorf("Failed to write JSON. path: %s, err: %w", p, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatList {
 | 
			
		||||
			p := path + "_short.txt"
 | 
			
		||||
			if w.DiffPlus || w.DiffMinus {
 | 
			
		||||
				p = path + "_short_diff.txt"
 | 
			
		||||
			}
 | 
			
		||||
			if err := w.writeFile(
 | 
			
		||||
				p, []byte(formatList(r)), 0600); err != nil {
 | 
			
		||||
				return xerrors.Errorf(
 | 
			
		||||
					"Failed to write text files. path: %s, err: %w", p, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatFullText {
 | 
			
		||||
			p := path + "_full.txt"
 | 
			
		||||
			if w.DiffPlus || w.DiffMinus {
 | 
			
		||||
				p = path + "_full_diff.txt"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err := w.writeFile(
 | 
			
		||||
				p, []byte(formatFullPlainText(r)), 0600); err != nil {
 | 
			
		||||
				return xerrors.Errorf(
 | 
			
		||||
					"Failed to write text files. path: %s, err: %w", p, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatCsv {
 | 
			
		||||
			p := path + ".csv"
 | 
			
		||||
			if w.DiffPlus || w.DiffMinus {
 | 
			
		||||
				p = path + "_diff.csv"
 | 
			
		||||
			}
 | 
			
		||||
			if err := formatCsvList(r, p); err != nil {
 | 
			
		||||
				return xerrors.Errorf("Failed to write CSV: %s, %w", p, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w LocalFileWriter) writeFile(path string, data []byte, perm os.FileMode) (err error) {
 | 
			
		||||
	if w.Gzip {
 | 
			
		||||
		data, err = gz(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		path += ".gz"
 | 
			
		||||
	}
 | 
			
		||||
	return ioutil.WriteFile(path, []byte(data), perm)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								reporter/s3.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								reporter/s3.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws/credentials"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws/ec2metadata"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/aws/session"
 | 
			
		||||
	"github.com/aws/aws-sdk-go/service/s3"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
 | 
			
		||||
	c "github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// S3Writer writes results to S3
 | 
			
		||||
type S3Writer struct {
 | 
			
		||||
	FormatJSON        bool
 | 
			
		||||
	FormatFullText    bool
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	FormatList        bool
 | 
			
		||||
	Gzip              bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getS3() (*s3.S3, error) {
 | 
			
		||||
	ses, err := session.NewSession()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	config := &aws.Config{
 | 
			
		||||
		Region: aws.String(c.Conf.AWS.Region),
 | 
			
		||||
		Credentials: credentials.NewChainCredentials([]credentials.Provider{
 | 
			
		||||
			&credentials.EnvProvider{},
 | 
			
		||||
			&credentials.SharedCredentialsProvider{Filename: "", Profile: c.Conf.AWS.Profile},
 | 
			
		||||
			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(ses)},
 | 
			
		||||
		}),
 | 
			
		||||
	}
 | 
			
		||||
	s, err := session.NewSession(config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return s3.New(s), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write results to S3
 | 
			
		||||
// http://docs.aws.amazon.com/sdk-for-go/latest/v1/developerguide/common-examples.title.html
 | 
			
		||||
func (w S3Writer) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	if len(rs) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	svc, err := getS3()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if w.FormatOneLineText {
 | 
			
		||||
		timestr := rs[0].ScannedAt.Format(time.RFC3339)
 | 
			
		||||
		k := fmt.Sprintf(timestr + "/summary.txt")
 | 
			
		||||
		text := formatOneLineSummary(rs...)
 | 
			
		||||
		if err := putObject(svc, k, []byte(text), w.Gzip); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		key := r.ReportKeyName()
 | 
			
		||||
		if w.FormatJSON {
 | 
			
		||||
			k := key + ".json"
 | 
			
		||||
			var b []byte
 | 
			
		||||
			if b, err = json.Marshal(r); err != nil {
 | 
			
		||||
				return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := putObject(svc, k, b, w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatList {
 | 
			
		||||
			k := key + "_short.txt"
 | 
			
		||||
			text := formatList(r)
 | 
			
		||||
			if err := putObject(svc, k, []byte(text), w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if w.FormatFullText {
 | 
			
		||||
			k := key + "_full.txt"
 | 
			
		||||
			text := formatFullPlainText(r)
 | 
			
		||||
			if err := putObject(svc, k, []byte(text), w.Gzip); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckIfBucketExists check the existence of S3 bucket
 | 
			
		||||
func CheckIfBucketExists() error {
 | 
			
		||||
	svc, err := getS3()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := svc.ListBuckets(&s3.ListBucketsInput{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf(
 | 
			
		||||
			"Failed to list buckets. err: %w, profile: %s, region: %s",
 | 
			
		||||
			err, c.Conf.AWS.Profile, c.Conf.AWS.Region)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found := false
 | 
			
		||||
	for _, bucket := range result.Buckets {
 | 
			
		||||
		if *bucket.Name == c.Conf.AWS.S3Bucket {
 | 
			
		||||
			found = true
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !found {
 | 
			
		||||
		return xerrors.Errorf(
 | 
			
		||||
			"Failed to find the buckets. profile: %s, region: %s, bucket: %s",
 | 
			
		||||
			c.Conf.AWS.Profile, c.Conf.AWS.Region, c.Conf.AWS.S3Bucket)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func putObject(svc *s3.S3, k string, b []byte, gzip bool) error {
 | 
			
		||||
	var err error
 | 
			
		||||
	if gzip {
 | 
			
		||||
		if b, err = gz(b); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		k += ".gz"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	putObjectInput := &s3.PutObjectInput{
 | 
			
		||||
		Bucket: aws.String(c.Conf.AWS.S3Bucket),
 | 
			
		||||
		Key:    aws.String(path.Join(c.Conf.AWS.S3ResultsDir, k)),
 | 
			
		||||
		Body:   bytes.NewReader(b),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Conf.AWS.S3ServerSideEncryption != "" {
 | 
			
		||||
		putObjectInput.ServerSideEncryption = aws.String(c.Conf.AWS.S3ServerSideEncryption)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := svc.PutObject(putObjectInput); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to upload data to %s/%s, err: %w",
 | 
			
		||||
			c.Conf.AWS.S3Bucket, k, err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										358
									
								
								reporter/slack.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								reporter/slack.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,358 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cenkalti/backoff"
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
	"github.com/parnurzeal/gorequest"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SlackWriter send report to slack
 | 
			
		||||
type SlackWriter struct {
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	lang              string
 | 
			
		||||
	osFamily          string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type message struct {
 | 
			
		||||
	Text        string             `json:"text"`
 | 
			
		||||
	Username    string             `json:"username"`
 | 
			
		||||
	IconEmoji   string             `json:"icon_emoji"`
 | 
			
		||||
	Channel     string             `json:"channel"`
 | 
			
		||||
	Attachments []slack.Attachment `json:"attachments"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	conf := config.Conf.Slack
 | 
			
		||||
	channel := conf.Channel
 | 
			
		||||
	token := conf.LegacyToken
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		w.lang, w.osFamily = r.Lang, r.Family
 | 
			
		||||
		if channel == "${servername}" {
 | 
			
		||||
			channel = fmt.Sprintf("#%s", r.ServerName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// A maximum of 100 attachments are allowed on a message.
 | 
			
		||||
		// Split into chunks with 100 elements
 | 
			
		||||
		// https://api.slack.com/methods/chat.postMessage
 | 
			
		||||
		maxAttachments := 100
 | 
			
		||||
		m := map[int][]slack.Attachment{}
 | 
			
		||||
		for i, a := range w.toSlackAttachments(r) {
 | 
			
		||||
			m[i/maxAttachments] = append(m[i/maxAttachments], a)
 | 
			
		||||
		}
 | 
			
		||||
		chunkKeys := []int{}
 | 
			
		||||
		for k := range m {
 | 
			
		||||
			chunkKeys = append(chunkKeys, k)
 | 
			
		||||
		}
 | 
			
		||||
		sort.Ints(chunkKeys)
 | 
			
		||||
 | 
			
		||||
		summary := fmt.Sprintf("%s\n%s",
 | 
			
		||||
			w.getNotifyUsers(config.Conf.Slack.NotifyUsers),
 | 
			
		||||
			formatOneLineSummary(r))
 | 
			
		||||
 | 
			
		||||
		// Send slack by API
 | 
			
		||||
		if 0 < len(token) {
 | 
			
		||||
			api := slack.New(token)
 | 
			
		||||
			msgPrms := slack.PostMessageParameters{
 | 
			
		||||
				Username:  conf.AuthUser,
 | 
			
		||||
				IconEmoji: conf.IconEmoji,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var ts string
 | 
			
		||||
			if _, ts, err = api.PostMessage(
 | 
			
		||||
				channel,
 | 
			
		||||
				slack.MsgOptionText(summary, true),
 | 
			
		||||
				slack.MsgOptionPostMessageParameters(msgPrms),
 | 
			
		||||
			); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if w.FormatOneLineText || 0 < len(r.Errors) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, k := range chunkKeys {
 | 
			
		||||
				params := slack.PostMessageParameters{
 | 
			
		||||
					Username:        conf.AuthUser,
 | 
			
		||||
					IconEmoji:       conf.IconEmoji,
 | 
			
		||||
					ThreadTimestamp: ts,
 | 
			
		||||
				}
 | 
			
		||||
				if _, _, err = api.PostMessage(
 | 
			
		||||
					channel,
 | 
			
		||||
					slack.MsgOptionText("", false),
 | 
			
		||||
					slack.MsgOptionPostMessageParameters(params),
 | 
			
		||||
					slack.MsgOptionAttachments(m[k]...),
 | 
			
		||||
				); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			msg := message{
 | 
			
		||||
				Text:      summary,
 | 
			
		||||
				Username:  conf.AuthUser,
 | 
			
		||||
				IconEmoji: conf.IconEmoji,
 | 
			
		||||
				Channel:   channel,
 | 
			
		||||
			}
 | 
			
		||||
			if err := w.send(msg); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if w.FormatOneLineText || 0 < len(r.Errors) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, k := range chunkKeys {
 | 
			
		||||
				txt := fmt.Sprintf("%d/%d for %s",
 | 
			
		||||
					k+1,
 | 
			
		||||
					len(chunkKeys),
 | 
			
		||||
					r.FormatServerName())
 | 
			
		||||
 | 
			
		||||
				msg := message{
 | 
			
		||||
					Text:        txt,
 | 
			
		||||
					Username:    conf.AuthUser,
 | 
			
		||||
					IconEmoji:   conf.IconEmoji,
 | 
			
		||||
					Channel:     channel,
 | 
			
		||||
					Attachments: m[k],
 | 
			
		||||
				}
 | 
			
		||||
				if err = w.send(msg); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w SlackWriter) send(msg message) error {
 | 
			
		||||
	conf := config.Conf.Slack
 | 
			
		||||
	count, retryMax := 0, 10
 | 
			
		||||
 | 
			
		||||
	bytes, _ := json.Marshal(msg)
 | 
			
		||||
	jsonBody := string(bytes)
 | 
			
		||||
 | 
			
		||||
	f := func() (err error) {
 | 
			
		||||
		resp, body, errs := gorequest.New().Timeout(10 * time.Second).Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End()
 | 
			
		||||
		if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
 | 
			
		||||
			count++
 | 
			
		||||
			if count == retryMax {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
			return xerrors.Errorf(
 | 
			
		||||
				"HTTP POST error. url: %s, resp: %v, body: %s, err: %s",
 | 
			
		||||
				conf.HookURL, resp, body, errs)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	notify := func(err error, t time.Duration) {
 | 
			
		||||
		log.Warnf("Error %s", err)
 | 
			
		||||
		log.Warn("Retrying in ", t)
 | 
			
		||||
	}
 | 
			
		||||
	boff := backoff.NewExponentialBackOff()
 | 
			
		||||
	if err := backoff.RetryNotify(f, boff, notify); err != nil {
 | 
			
		||||
		return xerrors.Errorf("HTTP error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if count == retryMax {
 | 
			
		||||
		return xerrors.New("Retry count exceeded")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w SlackWriter) toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
 | 
			
		||||
	vinfos := r.ScannedCves.ToSortedSlice()
 | 
			
		||||
	for _, vinfo := range vinfos {
 | 
			
		||||
 | 
			
		||||
		installed, candidate := []string{}, []string{}
 | 
			
		||||
		for _, affected := range vinfo.AffectedPackages {
 | 
			
		||||
			if p, ok := r.Packages[affected.Name]; ok {
 | 
			
		||||
				installed = append(installed,
 | 
			
		||||
					fmt.Sprintf("%s-%s", p.Name, p.FormatVer()))
 | 
			
		||||
			} else {
 | 
			
		||||
				installed = append(installed, affected.Name)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if p, ok := r.Packages[affected.Name]; ok {
 | 
			
		||||
				if affected.NotFixedYet {
 | 
			
		||||
					candidate = append(candidate, "Not Fixed Yet")
 | 
			
		||||
				} else {
 | 
			
		||||
					candidate = append(candidate, p.FormatNewVer())
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				candidate = append(candidate, "?")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, n := range vinfo.CpeURIs {
 | 
			
		||||
			installed = append(installed, n)
 | 
			
		||||
			candidate = append(candidate, "?")
 | 
			
		||||
		}
 | 
			
		||||
		for _, n := range vinfo.GitHubSecurityAlerts {
 | 
			
		||||
			installed = append(installed, n.PackageName)
 | 
			
		||||
			candidate = append(candidate, "?")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, wp := range vinfo.WpPackageFixStats {
 | 
			
		||||
			if p, ok := r.WordPressPackages.Find(wp.Name); ok {
 | 
			
		||||
				installed = append(installed, fmt.Sprintf("%s-%s", wp.Name, p.Version))
 | 
			
		||||
				candidate = append(candidate, wp.FixedIn)
 | 
			
		||||
			} else {
 | 
			
		||||
				installed = append(installed, wp.Name)
 | 
			
		||||
				candidate = append(candidate, "?")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		a := slack.Attachment{
 | 
			
		||||
			Title:      vinfo.CveIDDiffFormat(),
 | 
			
		||||
			TitleLink:  "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID,
 | 
			
		||||
			Text:       w.attachmentText(vinfo, r.CweDict, r.Packages),
 | 
			
		||||
			MarkdownIn: []string{"text", "pretext"},
 | 
			
		||||
			Fields: []slack.AttachmentField{
 | 
			
		||||
				{
 | 
			
		||||
					// Title: "Current Package/CPE",
 | 
			
		||||
					Title: "Installed",
 | 
			
		||||
					Value: strings.Join(installed, "\n"),
 | 
			
		||||
					Short: true,
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					Title: "Candidate",
 | 
			
		||||
					Value: strings.Join(candidate, "\n"),
 | 
			
		||||
					Short: true,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Color: cvssColor(vinfo.MaxCvssScore().Value.Score),
 | 
			
		||||
		}
 | 
			
		||||
		attaches = append(attaches, a)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://api.slack.com/docs/attachments
 | 
			
		||||
func cvssColor(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 (w SlackWriter) attachmentText(vinfo models.VulnInfo, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
 | 
			
		||||
	maxCvss := vinfo.MaxCvssScore()
 | 
			
		||||
	vectors := []string{}
 | 
			
		||||
 | 
			
		||||
	scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)
 | 
			
		||||
	for _, cvss := range scores {
 | 
			
		||||
		if cvss.Value.Severity == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		calcURL := ""
 | 
			
		||||
		switch cvss.Value.Type {
 | 
			
		||||
		case models.CVSS2:
 | 
			
		||||
			calcURL = fmt.Sprintf(
 | 
			
		||||
				"https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?name=%s",
 | 
			
		||||
				vinfo.CveID)
 | 
			
		||||
		case models.CVSS3:
 | 
			
		||||
			calcURL = fmt.Sprintf(
 | 
			
		||||
				"https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s",
 | 
			
		||||
				vinfo.CveID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if cont, ok := vinfo.CveContents[cvss.Type]; ok {
 | 
			
		||||
			v := fmt.Sprintf("<%s|%s> %s (<%s|%s>)",
 | 
			
		||||
				calcURL,
 | 
			
		||||
				fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
 | 
			
		||||
				cvss.Value.Severity,
 | 
			
		||||
				cont.SourceLink,
 | 
			
		||||
				cvss.Type)
 | 
			
		||||
			vectors = append(vectors, v)
 | 
			
		||||
 | 
			
		||||
		} else {
 | 
			
		||||
			if 0 < len(vinfo.DistroAdvisories) {
 | 
			
		||||
				links := []string{}
 | 
			
		||||
				for _, v := range vinfo.CveContents.PrimarySrcURLs(w.lang, w.osFamily, vinfo.CveID) {
 | 
			
		||||
					links = append(links, fmt.Sprintf("<%s|%s>", v.Value, v.Type))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				v := fmt.Sprintf("<%s|%s> %s (%s)",
 | 
			
		||||
					calcURL,
 | 
			
		||||
					fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
 | 
			
		||||
					cvss.Value.Severity,
 | 
			
		||||
					strings.Join(links, ", "))
 | 
			
		||||
				vectors = append(vectors, v)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	severity := strings.ToUpper(maxCvss.Value.Severity)
 | 
			
		||||
	if severity == "" {
 | 
			
		||||
		severity = "?"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nwvec := vinfo.AttackVector()
 | 
			
		||||
	if nwvec == "Network" || nwvec == "remote" {
 | 
			
		||||
		nwvec = fmt.Sprintf("*%s*", nwvec)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mitigation := ""
 | 
			
		||||
	for _, m := range vinfo.Mitigations {
 | 
			
		||||
		mitigation = fmt.Sprintf("\nMitigation:\n<%s|%s>", m.URL, m.CveContentType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("*%4.1f (%s)* %s %s\n%s\n```\n%s\n```%s\n%s\n",
 | 
			
		||||
		maxCvss.Value.Score,
 | 
			
		||||
		severity,
 | 
			
		||||
		nwvec,
 | 
			
		||||
		vinfo.PatchStatus(packs),
 | 
			
		||||
		strings.Join(vectors, "\n"),
 | 
			
		||||
		vinfo.Summaries(w.lang, w.osFamily)[0].Value,
 | 
			
		||||
		mitigation,
 | 
			
		||||
		w.cweIDs(vinfo, w.osFamily, cweDict),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w SlackWriter) cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
 | 
			
		||||
	links := []string{}
 | 
			
		||||
	for _, c := range vinfo.CveContents.UniqCweIDs(osFamily) {
 | 
			
		||||
		name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL := cweDict.Get(c.Value, w.lang)
 | 
			
		||||
		line := ""
 | 
			
		||||
		if top10Rank != "" {
 | 
			
		||||
			line = fmt.Sprintf("<%s|[OWASP Top %s]>",
 | 
			
		||||
				top10URL, top10Rank)
 | 
			
		||||
		}
 | 
			
		||||
		if cweTop25Rank != "" {
 | 
			
		||||
			line = fmt.Sprintf("<%s|[CWE Top %s]>",
 | 
			
		||||
				cweTop25URL, cweTop25Rank)
 | 
			
		||||
		}
 | 
			
		||||
		if sansTop25Rank != "" {
 | 
			
		||||
			line = fmt.Sprintf("<%s|[CWE/SANS Top %s]>",
 | 
			
		||||
				sansTop25URL, sansTop25Rank)
 | 
			
		||||
		}
 | 
			
		||||
		if top10Rank == "" && cweTop25Rank == "" && sansTop25Rank == "" {
 | 
			
		||||
			links = append(links, fmt.Sprintf("%s <%s|%s>: %s",
 | 
			
		||||
				line, url, c.Value, name))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(links, "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See testcase
 | 
			
		||||
func (w SlackWriter) getNotifyUsers(notifyUsers []string) string {
 | 
			
		||||
	slackStyleTexts := []string{}
 | 
			
		||||
	for _, username := range notifyUsers {
 | 
			
		||||
		slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(slackStyleTexts, " ")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								reporter/slack_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								reporter/slack_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
 | 
			
		||||
func TestGetNotifyUsers(t *testing.T) {
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		in       []string
 | 
			
		||||
		expected string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			[]string{"@user1", "@user2"},
 | 
			
		||||
			"<@user1> <@user2>",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		actual := SlackWriter{}.getNotifyUsers(tt.in)
 | 
			
		||||
		if tt.expected != actual {
 | 
			
		||||
			t.Errorf("expected %s, actual %s", tt.expected, actual)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								reporter/stdout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								reporter/stdout.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StdoutWriter write to stdout
 | 
			
		||||
type StdoutWriter struct {
 | 
			
		||||
	FormatCsv         bool
 | 
			
		||||
	FormatFullText    bool
 | 
			
		||||
	FormatOneLineText bool
 | 
			
		||||
	FormatList        bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//TODO support -format-jSON
 | 
			
		||||
 | 
			
		||||
// WriteScanSummary prints Scan summary at the end of scan
 | 
			
		||||
func (w StdoutWriter) WriteScanSummary(rs ...models.ScanResult) {
 | 
			
		||||
	fmt.Printf("\n\n")
 | 
			
		||||
	fmt.Println("Scan Summary")
 | 
			
		||||
	fmt.Println("================")
 | 
			
		||||
	fmt.Printf("%s\n", formatScanSummary(rs...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w StdoutWriter) Write(rs ...models.ScanResult) error {
 | 
			
		||||
	if w.FormatOneLineText {
 | 
			
		||||
		fmt.Print("\n\n")
 | 
			
		||||
		fmt.Println("One Line Summary")
 | 
			
		||||
		fmt.Println("================")
 | 
			
		||||
		fmt.Println(formatOneLineSummary(rs...))
 | 
			
		||||
		fmt.Print("\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if w.FormatList || w.FormatCsv {
 | 
			
		||||
		for _, r := range rs {
 | 
			
		||||
			fmt.Println(formatList(r))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if w.FormatFullText {
 | 
			
		||||
		for _, r := range rs {
 | 
			
		||||
			fmt.Println(formatFullPlainText(r))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								reporter/syslog.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								reporter/syslog.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/syslog"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
 | 
			
		||||
	"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 xerrors.Errorf("Failed to initialize syslog client: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		messages := w.encodeSyslog(r)
 | 
			
		||||
		for _, m := range messages {
 | 
			
		||||
			if _, err = fmt.Fprint(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, ",")
 | 
			
		||||
 | 
			
		||||
	var commonKvPairs []string
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`scanned_at="%s"`, result.ScannedAt))
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`server_name="%s"`, result.ServerName))
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`os_family="%s"`, result.Family))
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`os_release="%s"`, result.Release))
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`ipv4_addr="%s"`, ipv4Addrs))
 | 
			
		||||
	commonKvPairs = append(commonKvPairs, fmt.Sprintf(`ipv6_addr="%s"`, ipv6Addrs))
 | 
			
		||||
 | 
			
		||||
	for cveID, vinfo := range result.ScannedCves {
 | 
			
		||||
		kvPairs := commonKvPairs
 | 
			
		||||
 | 
			
		||||
		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() {
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`cvss_score_%s_v2="%.2f"`, cvss.Type, cvss.Value.Score))
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v2="%s"`, cvss.Type, cvss.Value.Vector))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, cvss := range vinfo.Cvss3Scores() {
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`cvss_score_%s_v3="%.2f"`, cvss.Type, cvss.Value.Score))
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v3="%s"`, cvss.Type, cvss.Value.Vector))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if content, ok := vinfo.CveContents[models.Nvd]; ok {
 | 
			
		||||
			cwes := strings.Join(content.CweIDs, ",")
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`cwe_ids="%s"`, cwes))
 | 
			
		||||
			if config.Conf.Syslog.Verbose {
 | 
			
		||||
				kvPairs = append(kvPairs, fmt.Sprintf(`source_link="%s"`, content.SourceLink))
 | 
			
		||||
				kvPairs = append(kvPairs, fmt.Sprintf(`summary="%s"`, content.Summary))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if content, ok := vinfo.CveContents[models.RedHat]; ok {
 | 
			
		||||
			kvPairs = append(kvPairs, fmt.Sprintf(`title="%s"`, content.Title))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// message: key1="value1" key2="value2"...
 | 
			
		||||
		messages = append(messages, strings.Join(kvPairs, " "))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(messages) == 0 {
 | 
			
		||||
		commonKvPairs = append(commonKvPairs, `message="No CVE-IDs are found"`)
 | 
			
		||||
		messages = append(messages, strings.Join(commonKvPairs, " "))
 | 
			
		||||
	}
 | 
			
		||||
	return messages
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								reporter/syslog_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								reporter/syslog_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"sort"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSyslogWriterEncodeSyslog(t *testing.T) {
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		result           models.ScanResult
 | 
			
		||||
		expectedMessages []string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			result: models.ScanResult{
 | 
			
		||||
				ScannedAt:  time.Date(2018, 6, 13, 16, 10, 0, 0, time.UTC),
 | 
			
		||||
				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.PackageFixStatuses{
 | 
			
		||||
							models.PackageFixStatus{Name: "pkg1"},
 | 
			
		||||
							models.PackageFixStatus{Name: "pkg2"},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					"CVE-2017-0002": models.VulnInfo{
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{
 | 
			
		||||
							models.PackageFixStatus{Name: "pkg3"},
 | 
			
		||||
							models.PackageFixStatus{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",
 | 
			
		||||
								Cvss2Severity: "MEDIUM",
 | 
			
		||||
								CweIDs:        []string{"CWE-20"},
 | 
			
		||||
								Cvss3Score:    9.8,
 | 
			
		||||
								Cvss3Vector:   "AV:L/AC:L/Au:N/C:N/I:N/A:C",
 | 
			
		||||
								Cvss3Severity: "HIGH",
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedMessages: []string{
 | 
			
		||||
				`scanned_at="2018-06-13 16:10:00 +0000 UTC" 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"`,
 | 
			
		||||
				`scanned_at="2018-06-13 16:10:00 +0000 UTC" 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" cvss_score_nvd_v2="5.00" cvss_vector_nvd_v2="AV:L/AC:L/Au:N/C:N/I:N/A:C" cvss_score_nvd_v3="9.80" cvss_vector_nvd_v3="AV:L/AC:L/Au:N/C:N/I:N/A:C" cwe_ids="CWE-20"`,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		// 1
 | 
			
		||||
		{
 | 
			
		||||
			result: models.ScanResult{
 | 
			
		||||
				ScannedAt:  time.Date(2018, 6, 13, 17, 10, 0, 0, time.UTC),
 | 
			
		||||
				ServerName: "teste02",
 | 
			
		||||
				Family:     "centos",
 | 
			
		||||
				Release:    "6",
 | 
			
		||||
				IPv6Addrs:  []string{"2001:0DB8::1"},
 | 
			
		||||
				ScannedCves: models.VulnInfos{
 | 
			
		||||
					"CVE-2017-0003": models.VulnInfo{
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{
 | 
			
		||||
							models.PackageFixStatus{Name: "pkg5"},
 | 
			
		||||
						},
 | 
			
		||||
						CveContents: models.CveContents{
 | 
			
		||||
							models.RedHat: models.CveContent{
 | 
			
		||||
								Cvss3Score:    5.0,
 | 
			
		||||
								Cvss3Severity: "Medium",
 | 
			
		||||
								Cvss3Vector:   "AV:L/AC:L/Au:N/C:N/I:N/A:C",
 | 
			
		||||
								CweIDs:        []string{"CWE-284"},
 | 
			
		||||
								Title:         "RHSA-2017:0001: pkg5 security update (Important)",
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedMessages: []string{
 | 
			
		||||
				`scanned_at="2018-06-13 17:10:00 +0000 UTC" server_name="teste02" os_family="centos" os_release="6" ipv4_addr="" ipv6_addr="2001:0DB8::1" packages="pkg5" cve_id="CVE-2017-0003" cvss_score_redhat_v3="5.00" cvss_vector_redhat_v3="AV:L/AC:L/Au:N/C:N/I:N/A:C" title="RHSA-2017:0001: pkg5 security update (Important)"`,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			result: models.ScanResult{
 | 
			
		||||
				ScannedAt:   time.Date(2018, 6, 13, 12, 10, 0, 0, time.UTC),
 | 
			
		||||
				ServerName:  "teste03",
 | 
			
		||||
				Family:      "centos",
 | 
			
		||||
				Release:     "7",
 | 
			
		||||
				IPv6Addrs:   []string{"2001:0DB8::1"},
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
			expectedMessages: []string{
 | 
			
		||||
				`scanned_at="2018-06-13 12:10:00 +0000 UTC" server_name="teste03" os_family="centos" os_release="7" ipv4_addr="" ipv6_addr="2001:0DB8::1" message="No CVE-IDs are found"`,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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: \nexpected %s \nactual   %s", i, j, e, m)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								reporter/telegram.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								reporter/telegram.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TelegramWriter sends report to Telegram
 | 
			
		||||
type TelegramWriter struct{}
 | 
			
		||||
 | 
			
		||||
func (w TelegramWriter) Write(rs ...models.ScanResult) (err error) {
 | 
			
		||||
	conf := config.Conf.Telegram
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		msgs := []string{fmt.Sprintf("*%s*\n%s\n%s\n%s",
 | 
			
		||||
			r.ServerInfo(),
 | 
			
		||||
			r.ScannedCves.FormatCveSummary(),
 | 
			
		||||
			r.ScannedCves.FormatFixedStatus(r.Packages),
 | 
			
		||||
			r.FormatUpdatablePkgsSummary())}
 | 
			
		||||
		for _, vinfo := range r.ScannedCves {
 | 
			
		||||
			maxCvss := vinfo.MaxCvssScore()
 | 
			
		||||
			severity := strings.ToUpper(maxCvss.Value.Severity)
 | 
			
		||||
			if severity == "" {
 | 
			
		||||
				severity = "?"
 | 
			
		||||
			}
 | 
			
		||||
			msgs = append(msgs, fmt.Sprintf(`[%s](https://nvd.nist.gov/vuln/detail/%s) _%s %s %s_\n%s`,
 | 
			
		||||
				vinfo.CveID,
 | 
			
		||||
				vinfo.CveID,
 | 
			
		||||
				strconv.FormatFloat(maxCvss.Value.Score, 'f', 1, 64),
 | 
			
		||||
				severity,
 | 
			
		||||
				maxCvss.Value.Vector,
 | 
			
		||||
				vinfo.Summaries(r.Lang, r.Family)[0].Value))
 | 
			
		||||
			if len(msgs) == 5 {
 | 
			
		||||
				if err = sendMessage(conf.ChatID, conf.Token, strings.Join(msgs, "\n\n")); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				msgs = []string{}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if len(msgs) != 0 {
 | 
			
		||||
			if err = sendMessage(conf.ChatID, conf.Token, strings.Join(msgs, "\n\n")); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sendMessage(chatID, token, message string) error {
 | 
			
		||||
	uri := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
 | 
			
		||||
	payload := `{"text": "` + strings.Replace(message, `"`, `\"`, -1) + `", "chat_id": "` + chatID + `", "parse_mode": "Markdown" }`
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBuffer([]byte(payload)))
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Add("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
	client, err := util.GetHTTPClient(config.Conf.HTTPProxy)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if checkResponse(resp) != nil && err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkResponse(r *http.Response) error {
 | 
			
		||||
	if c := r.StatusCode; 200 <= c && c <= 299 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return xerrors.Errorf("API call to %s failed: %s", r.Request.URL.String(), r.Status)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										709
									
								
								reporter/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										709
									
								
								reporter/util.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,709 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/csv"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	"github.com/gosuri/uitable"
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	maxColWidth = 100
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func formatScanSummary(rs ...models.ScanResult) string {
 | 
			
		||||
	table := uitable.New()
 | 
			
		||||
	table.MaxColWidth = maxColWidth
 | 
			
		||||
	table.Wrap = true
 | 
			
		||||
 | 
			
		||||
	warnMsgs := []string{}
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		var cols []interface{}
 | 
			
		||||
		if len(r.Errors) == 0 {
 | 
			
		||||
			cols = []interface{}{
 | 
			
		||||
				r.FormatServerName(),
 | 
			
		||||
				fmt.Sprintf("%s%s", r.Family, r.Release),
 | 
			
		||||
				r.FormatUpdatablePkgsSummary(),
 | 
			
		||||
			}
 | 
			
		||||
			if 0 < len(r.WordPressPackages) {
 | 
			
		||||
				cols = append(cols, fmt.Sprintf("%d WordPress pkgs", len(r.WordPressPackages)))
 | 
			
		||||
			}
 | 
			
		||||
			if 0 < len(r.LibraryScanners) {
 | 
			
		||||
				cols = append(cols, fmt.Sprintf("%d libs", r.LibraryScanners.Total()))
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			cols = []interface{}{
 | 
			
		||||
				r.FormatServerName(),
 | 
			
		||||
				"Error",
 | 
			
		||||
				"",
 | 
			
		||||
				"Use configtest subcommand or scan with --debug to view the details",
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		table.AddRow(cols...)
 | 
			
		||||
 | 
			
		||||
		if len(r.Warnings) != 0 {
 | 
			
		||||
			warnMsgs = append(warnMsgs, fmt.Sprintf("Warning: %s", r.Warnings))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s\n\n%s", table, strings.Join(
 | 
			
		||||
		warnMsgs, "\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatOneLineSummary(rs ...models.ScanResult) string {
 | 
			
		||||
	table := uitable.New()
 | 
			
		||||
	table.MaxColWidth = maxColWidth
 | 
			
		||||
	table.Wrap = true
 | 
			
		||||
 | 
			
		||||
	warnMsgs := []string{}
 | 
			
		||||
	for _, r := range rs {
 | 
			
		||||
		var cols []interface{}
 | 
			
		||||
		if len(r.Errors) == 0 {
 | 
			
		||||
			cols = []interface{}{
 | 
			
		||||
				r.FormatServerName(),
 | 
			
		||||
				r.ScannedCves.FormatCveSummary(),
 | 
			
		||||
				r.ScannedCves.FormatFixedStatus(r.Packages),
 | 
			
		||||
				r.FormatUpdatablePkgsSummary(),
 | 
			
		||||
				r.FormatExploitCveSummary(),
 | 
			
		||||
				r.FormatMetasploitCveSummary(),
 | 
			
		||||
				r.FormatAlertSummary(),
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			cols = []interface{}{
 | 
			
		||||
				r.FormatServerName(),
 | 
			
		||||
				"Use configtest subcommand or scan with --debug to view the details",
 | 
			
		||||
				"",
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		table.AddRow(cols...)
 | 
			
		||||
 | 
			
		||||
		if len(r.Warnings) != 0 {
 | 
			
		||||
			warnMsgs = append(warnMsgs, fmt.Sprintf("Warning for %s: %s",
 | 
			
		||||
				r.FormatServerName(), r.Warnings))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// We don't want warning message to the summary file
 | 
			
		||||
	if config.Conf.Quiet {
 | 
			
		||||
		return fmt.Sprintf("%s\n", table)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s\n\n%s", table, strings.Join(
 | 
			
		||||
		warnMsgs, "\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatList(r models.ScanResult) string {
 | 
			
		||||
	header := r.FormatTextReportHeader()
 | 
			
		||||
	if len(r.Errors) != 0 {
 | 
			
		||||
		return fmt.Sprintf(
 | 
			
		||||
			"%s\nError: Use configtest subcommand or scan with --debug to view the details\n%s\n\n",
 | 
			
		||||
			header, r.Errors)
 | 
			
		||||
	}
 | 
			
		||||
	if len(r.Warnings) != 0 {
 | 
			
		||||
		header += fmt.Sprintf(
 | 
			
		||||
			"\nWarning: Some warnings occurred.\n%s\n\n",
 | 
			
		||||
			r.Warnings)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(r.ScannedCves) == 0 {
 | 
			
		||||
		return fmt.Sprintf(`
 | 
			
		||||
%s
 | 
			
		||||
No CVE-IDs are found in updatable packages.
 | 
			
		||||
%s
 | 
			
		||||
`, header, r.FormatUpdatablePkgsSummary())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := [][]string{}
 | 
			
		||||
	for _, vinfo := range r.ScannedCves.ToSortedSlice() {
 | 
			
		||||
		max := vinfo.MaxCvssScore().Value.Score
 | 
			
		||||
		// v2max := vinfo.MaxCvss2Score().Value.Score
 | 
			
		||||
		// v3max := vinfo.MaxCvss3Score().Value.Score
 | 
			
		||||
 | 
			
		||||
		// packname := vinfo.AffectedPackages.FormatTuiSummary()
 | 
			
		||||
		// packname += strings.Join(vinfo.CpeURIs, ", ")
 | 
			
		||||
 | 
			
		||||
		exploits := ""
 | 
			
		||||
		if 0 < len(vinfo.Exploits) || 0 < len(vinfo.Metasploits) {
 | 
			
		||||
			exploits = "POC"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		link := ""
 | 
			
		||||
		if strings.HasPrefix(vinfo.CveID, "CVE-") {
 | 
			
		||||
			link = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vinfo.CveID)
 | 
			
		||||
		} else if strings.HasPrefix(vinfo.CveID, "WPVDBID-") {
 | 
			
		||||
			link = fmt.Sprintf("https://wpscan.com/vulnerabilities/%s", strings.TrimPrefix(vinfo.CveID, "WPVDBID-"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data = append(data, []string{
 | 
			
		||||
			vinfo.CveIDDiffFormat(),
 | 
			
		||||
			fmt.Sprintf("%4.1f", max),
 | 
			
		||||
			fmt.Sprintf("%5s", vinfo.AttackVector()),
 | 
			
		||||
			// fmt.Sprintf("%4.1f", v2max),
 | 
			
		||||
			// fmt.Sprintf("%4.1f", v3max),
 | 
			
		||||
			exploits,
 | 
			
		||||
			vinfo.AlertDict.FormatSource(),
 | 
			
		||||
			fmt.Sprintf("%7s", vinfo.PatchStatus(r.Packages)),
 | 
			
		||||
			link,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b := bytes.Buffer{}
 | 
			
		||||
	table := tablewriter.NewWriter(&b)
 | 
			
		||||
	table.SetHeader([]string{
 | 
			
		||||
		"CVE-ID",
 | 
			
		||||
		"CVSS",
 | 
			
		||||
		"Attack",
 | 
			
		||||
		// "v3",
 | 
			
		||||
		// "v2",
 | 
			
		||||
		"PoC",
 | 
			
		||||
		"CERT",
 | 
			
		||||
		"Fixed",
 | 
			
		||||
		"NVD",
 | 
			
		||||
	})
 | 
			
		||||
	table.SetBorder(true)
 | 
			
		||||
	table.AppendBulk(data)
 | 
			
		||||
	table.Render()
 | 
			
		||||
	return fmt.Sprintf("%s\n%s", header, b.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatFullPlainText(r models.ScanResult) (lines string) {
 | 
			
		||||
	header := r.FormatTextReportHeader()
 | 
			
		||||
	if len(r.Errors) != 0 {
 | 
			
		||||
		return fmt.Sprintf(
 | 
			
		||||
			"%s\nError: Use configtest subcommand or scan with --debug to view the details\n%s\n\n",
 | 
			
		||||
			header, r.Errors)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(r.Warnings) != 0 {
 | 
			
		||||
		header += fmt.Sprintf(
 | 
			
		||||
			"\nWarning: Some warnings occurred.\n%s\n\n",
 | 
			
		||||
			r.Warnings)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(r.ScannedCves) == 0 {
 | 
			
		||||
		return fmt.Sprintf(`
 | 
			
		||||
%s
 | 
			
		||||
No CVE-IDs are found in updatable packages.
 | 
			
		||||
%s
 | 
			
		||||
`, header, r.FormatUpdatablePkgsSummary())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lines = header + "\n"
 | 
			
		||||
 | 
			
		||||
	for _, vuln := range r.ScannedCves.ToSortedSlice() {
 | 
			
		||||
		data := [][]string{}
 | 
			
		||||
		data = append(data, []string{"Max Score", vuln.FormatMaxCvssScore()})
 | 
			
		||||
		for _, cvss := range vuln.Cvss3Scores() {
 | 
			
		||||
			if cvssstr := cvss.Value.Format(); cvssstr != "" {
 | 
			
		||||
				data = append(data, []string{string(cvss.Type), cvssstr})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, cvss := range vuln.Cvss2Scores() {
 | 
			
		||||
			if cvssstr := cvss.Value.Format(); cvssstr != "" {
 | 
			
		||||
				data = append(data, []string{string(cvss.Type), cvssstr})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data = append(data, []string{"Summary", vuln.Summaries(
 | 
			
		||||
			r.Lang, r.Family)[0].Value})
 | 
			
		||||
 | 
			
		||||
		for _, m := range vuln.Mitigations {
 | 
			
		||||
			data = append(data, []string{"Mitigation", m.URL})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		links := vuln.CveContents.PrimarySrcURLs(r.Lang, r.Family, vuln.CveID)
 | 
			
		||||
		for _, link := range links {
 | 
			
		||||
			data = append(data, []string{"Primary Src", link.Value})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, url := range vuln.CveContents.PatchURLs() {
 | 
			
		||||
			data = append(data, []string{"Patch", url})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		vuln.AffectedPackages.Sort()
 | 
			
		||||
		for _, affected := range vuln.AffectedPackages {
 | 
			
		||||
			if pack, ok := r.Packages[affected.Name]; ok {
 | 
			
		||||
				var line string
 | 
			
		||||
				if pack.Repository != "" {
 | 
			
		||||
					line = fmt.Sprintf("%s (%s)",
 | 
			
		||||
						pack.FormatVersionFromTo(affected),
 | 
			
		||||
						pack.Repository)
 | 
			
		||||
				} else {
 | 
			
		||||
					line = pack.FormatVersionFromTo(affected)
 | 
			
		||||
				}
 | 
			
		||||
				data = append(data, []string{"Affected Pkg", line})
 | 
			
		||||
 | 
			
		||||
				if len(pack.AffectedProcs) != 0 {
 | 
			
		||||
					for _, p := range pack.AffectedProcs {
 | 
			
		||||
						if len(p.ListenPortStats) == 0 {
 | 
			
		||||
							data = append(data, []string{"",
 | 
			
		||||
								fmt.Sprintf("  - PID: %s %s, Port: []", p.PID, p.Name)})
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						var ports []string
 | 
			
		||||
						for _, pp := range p.ListenPortStats {
 | 
			
		||||
							if len(pp.PortReachableTo) == 0 {
 | 
			
		||||
								ports = append(ports, fmt.Sprintf("%s:%s", pp.BindAddress, pp.Port))
 | 
			
		||||
							} else {
 | 
			
		||||
								ports = append(ports, fmt.Sprintf("%s:%s(◉ Scannable: %s)", pp.BindAddress, pp.Port, pp.PortReachableTo))
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						data = append(data, []string{"",
 | 
			
		||||
							fmt.Sprintf("  - PID: %s %s, Port: %s", p.PID, p.Name, ports)})
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		sort.Strings(vuln.CpeURIs)
 | 
			
		||||
		for _, name := range vuln.CpeURIs {
 | 
			
		||||
			data = append(data, []string{"CPE", name})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, alert := range vuln.GitHubSecurityAlerts {
 | 
			
		||||
			data = append(data, []string{"GitHub", alert.PackageName})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, wp := range vuln.WpPackageFixStats {
 | 
			
		||||
			if p, ok := r.WordPressPackages.Find(wp.Name); ok {
 | 
			
		||||
				if p.Type == models.WPCore {
 | 
			
		||||
					data = append(data, []string{"WordPress",
 | 
			
		||||
						fmt.Sprintf("%s-%s, FixedIn: %s", wp.Name, p.Version, wp.FixedIn)})
 | 
			
		||||
				} else {
 | 
			
		||||
					data = append(data, []string{"WordPress",
 | 
			
		||||
						fmt.Sprintf("%s-%s, Update: %s, FixedIn: %s, %s",
 | 
			
		||||
							wp.Name, p.Version, p.Update, wp.FixedIn, p.Status)})
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				data = append(data, []string{"WordPress",
 | 
			
		||||
					fmt.Sprintf("%s", wp.Name)})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, l := range vuln.LibraryFixedIns {
 | 
			
		||||
			libs := r.LibraryScanners.Find(l.Path, l.Name)
 | 
			
		||||
			for path, lib := range libs {
 | 
			
		||||
				data = append(data, []string{l.Key,
 | 
			
		||||
					fmt.Sprintf("%s-%s, FixedIn: %s (%s)",
 | 
			
		||||
						lib.Name, lib.Version, l.FixedIn, path)})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, confidence := range vuln.Confidences {
 | 
			
		||||
			data = append(data, []string{"Confidence", confidence.String()})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cweURLs, top10URLs := []string{}, []string{}
 | 
			
		||||
		cweTop25URLs, sansTop25URLs := []string{}, []string{}
 | 
			
		||||
		for _, v := range vuln.CveContents.UniqCweIDs(r.Family) {
 | 
			
		||||
			name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL := r.CweDict.Get(v.Value, r.Lang)
 | 
			
		||||
			if top10Rank != "" {
 | 
			
		||||
				data = append(data, []string{"CWE",
 | 
			
		||||
					fmt.Sprintf("[OWASP Top%s] %s: %s (%s)",
 | 
			
		||||
						top10Rank, v.Value, name, v.Type)})
 | 
			
		||||
				top10URLs = append(top10URLs, top10URL)
 | 
			
		||||
			}
 | 
			
		||||
			if cweTop25Rank != "" {
 | 
			
		||||
				data = append(data, []string{"CWE",
 | 
			
		||||
					fmt.Sprintf("[CWE Top%s] %s: %s (%s)",
 | 
			
		||||
						cweTop25Rank, v.Value, name, v.Type)})
 | 
			
		||||
				cweTop25URLs = append(cweTop25URLs, cweTop25URL)
 | 
			
		||||
			}
 | 
			
		||||
			if sansTop25Rank != "" {
 | 
			
		||||
				data = append(data, []string{"CWE",
 | 
			
		||||
					fmt.Sprintf("[CWE/SANS Top%s]  %s: %s (%s)",
 | 
			
		||||
						sansTop25Rank, v.Value, name, v.Type)})
 | 
			
		||||
				sansTop25URLs = append(sansTop25URLs, sansTop25URL)
 | 
			
		||||
			}
 | 
			
		||||
			if top10Rank == "" && cweTop25Rank == "" && sansTop25Rank == "" {
 | 
			
		||||
				data = append(data, []string{"CWE", fmt.Sprintf("%s: %s (%s)",
 | 
			
		||||
					v.Value, name, v.Type)})
 | 
			
		||||
			}
 | 
			
		||||
			cweURLs = append(cweURLs, url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, url := range cweURLs {
 | 
			
		||||
			data = append(data, []string{"CWE", url})
 | 
			
		||||
		}
 | 
			
		||||
		for _, exploit := range vuln.Exploits {
 | 
			
		||||
			data = append(data, []string{string(exploit.ExploitType), exploit.URL})
 | 
			
		||||
		}
 | 
			
		||||
		for _, url := range top10URLs {
 | 
			
		||||
			data = append(data, []string{"OWASP Top10", url})
 | 
			
		||||
		}
 | 
			
		||||
		if len(cweTop25URLs) != 0 {
 | 
			
		||||
			data = append(data, []string{"CWE Top25", cweTop25URLs[0]})
 | 
			
		||||
		}
 | 
			
		||||
		if len(sansTop25URLs) != 0 {
 | 
			
		||||
			data = append(data, []string{"SANS/CWE Top25", sansTop25URLs[0]})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, alert := range vuln.AlertDict.Ja {
 | 
			
		||||
			data = append(data, []string{"JPCERT Alert", alert.URL})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, alert := range vuln.AlertDict.En {
 | 
			
		||||
			data = append(data, []string{"USCERT Alert", alert.URL})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for _, rr := range vuln.CveContents.References(r.Family) {
 | 
			
		||||
		// for _, ref := range rr.Value {
 | 
			
		||||
		// data = append(data, []string{ref.Source, ref.Link})
 | 
			
		||||
		// }
 | 
			
		||||
		// }
 | 
			
		||||
 | 
			
		||||
		b := bytes.Buffer{}
 | 
			
		||||
		table := tablewriter.NewWriter(&b)
 | 
			
		||||
		table.SetColWidth(80)
 | 
			
		||||
		table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
 | 
			
		||||
		table.SetHeader([]string{
 | 
			
		||||
			vuln.CveIDDiffFormat(),
 | 
			
		||||
			vuln.PatchStatus(r.Packages),
 | 
			
		||||
		})
 | 
			
		||||
		table.SetBorder(true)
 | 
			
		||||
		table.AppendBulk(data)
 | 
			
		||||
		table.Render()
 | 
			
		||||
		lines += b.String() + "\n"
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatCsvList(r models.ScanResult, path string) error {
 | 
			
		||||
	data := [][]string{{"CVE-ID", "CVSS", "Attack", "PoC", "CERT", "Fixed", "NVD"}}
 | 
			
		||||
	for _, vinfo := range r.ScannedCves.ToSortedSlice() {
 | 
			
		||||
		max := vinfo.MaxCvssScore().Value.Score
 | 
			
		||||
 | 
			
		||||
		exploits := ""
 | 
			
		||||
		if 0 < len(vinfo.Exploits) || 0 < len(vinfo.Metasploits) {
 | 
			
		||||
			exploits = "POC"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		link := ""
 | 
			
		||||
		if strings.HasPrefix(vinfo.CveID, "CVE-") {
 | 
			
		||||
			link = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vinfo.CveID)
 | 
			
		||||
		} else if strings.HasPrefix(vinfo.CveID, "WPVDBID-") {
 | 
			
		||||
			link = fmt.Sprintf("https://wpscan.com/vulnerabilities/%s", strings.TrimPrefix(vinfo.CveID, "WPVDBID-"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data = append(data, []string{
 | 
			
		||||
			vinfo.CveID,
 | 
			
		||||
			fmt.Sprintf("%4.1f", max),
 | 
			
		||||
			vinfo.AttackVector(),
 | 
			
		||||
			exploits,
 | 
			
		||||
			vinfo.AlertDict.FormatSource(),
 | 
			
		||||
			vinfo.PatchStatus(r.Packages),
 | 
			
		||||
			link,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	file, err := os.Create(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to create a file: %s, err: %w", path, err)
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
	if err := csv.NewWriter(file).WriteAll(data); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to write to file: %s, err: %w", path, err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cweURL(cweID string) string {
 | 
			
		||||
	return fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html",
 | 
			
		||||
		strings.TrimPrefix(cweID, "CWE-"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cweJvnURL(cweID string) string {
 | 
			
		||||
	return fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OverwriteJSONFile(dir string, r models.ScanResult) error {
 | 
			
		||||
	w := LocalFileWriter{
 | 
			
		||||
		CurrentDir: dir,
 | 
			
		||||
		FormatJSON: true,
 | 
			
		||||
	}
 | 
			
		||||
	if err := w.Write(r); err != nil {
 | 
			
		||||
		return xerrors.Errorf("Failed to write summary report: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func diff(curResults, preResults models.ScanResults, isPlus, isMinus bool) (diffed models.ScanResults) {
 | 
			
		||||
	for _, current := range curResults {
 | 
			
		||||
		found := false
 | 
			
		||||
		var previous models.ScanResult
 | 
			
		||||
		for _, r := range preResults {
 | 
			
		||||
			if current.ServerName == r.ServerName && current.Container.Name == r.Container.Name {
 | 
			
		||||
				found = true
 | 
			
		||||
				previous = r
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !found {
 | 
			
		||||
			diffed = append(diffed, current)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cves := models.VulnInfos{}
 | 
			
		||||
		if isPlus {
 | 
			
		||||
			cves = getPlusDiffCves(previous, current)
 | 
			
		||||
		}
 | 
			
		||||
		if isMinus {
 | 
			
		||||
			minus := getMinusDiffCves(previous, current)
 | 
			
		||||
			if len(cves) == 0 {
 | 
			
		||||
				cves = minus
 | 
			
		||||
			} else {
 | 
			
		||||
				for k, v := range minus {
 | 
			
		||||
					cves[k] = v
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		packages := models.Packages{}
 | 
			
		||||
		for _, s := range cves {
 | 
			
		||||
			for _, affected := range s.AffectedPackages {
 | 
			
		||||
				var p models.Package
 | 
			
		||||
				if s.DiffStatus == models.DiffPlus {
 | 
			
		||||
					p = current.Packages[affected.Name]
 | 
			
		||||
				} else {
 | 
			
		||||
					p = previous.Packages[affected.Name]
 | 
			
		||||
				}
 | 
			
		||||
				packages[affected.Name] = p
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		current.ScannedCves = cves
 | 
			
		||||
		current.Packages = packages
 | 
			
		||||
		diffed = append(diffed, current)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPlusDiffCves(previous, current models.ScanResult) models.VulnInfos {
 | 
			
		||||
	previousCveIDsSet := map[string]bool{}
 | 
			
		||||
	for _, previousVulnInfo := range previous.ScannedCves {
 | 
			
		||||
		previousCveIDsSet[previousVulnInfo.CveID] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	new := models.VulnInfos{}
 | 
			
		||||
	updated := models.VulnInfos{}
 | 
			
		||||
	for _, v := range current.ScannedCves {
 | 
			
		||||
		if previousCveIDsSet[v.CveID] {
 | 
			
		||||
			if isCveInfoUpdated(v.CveID, previous, current) {
 | 
			
		||||
				v.DiffStatus = models.DiffPlus
 | 
			
		||||
				updated[v.CveID] = v
 | 
			
		||||
				util.Log.Debugf("updated: %s", v.CveID)
 | 
			
		||||
 | 
			
		||||
				// TODO commented out because  a bug of diff logic when multiple oval defs found for a certain CVE-ID and same updated_at
 | 
			
		||||
				// if these OVAL defs have different affected packages, this logic detects as updated.
 | 
			
		||||
				// This logic will be uncomented after integration with gost https://github.com/knqyf263/gost
 | 
			
		||||
				// } else if isCveFixed(v, previous) {
 | 
			
		||||
				// updated[v.CveID] = v
 | 
			
		||||
				// util.Log.Debugf("fixed: %s", v.CveID)
 | 
			
		||||
 | 
			
		||||
			} else {
 | 
			
		||||
				util.Log.Debugf("same: %s", v.CveID)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			util.Log.Debugf("new: %s", v.CveID)
 | 
			
		||||
			v.DiffStatus = models.DiffPlus
 | 
			
		||||
			new[v.CveID] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(updated) == 0 && len(new) == 0 {
 | 
			
		||||
		util.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for cveID, vuln := range new {
 | 
			
		||||
		updated[cveID] = vuln
 | 
			
		||||
	}
 | 
			
		||||
	return updated
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMinusDiffCves(previous, current models.ScanResult) models.VulnInfos {
 | 
			
		||||
	currentCveIDsSet := map[string]bool{}
 | 
			
		||||
	for _, currentVulnInfo := range current.ScannedCves {
 | 
			
		||||
		currentCveIDsSet[currentVulnInfo.CveID] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clear := models.VulnInfos{}
 | 
			
		||||
	for _, v := range previous.ScannedCves {
 | 
			
		||||
		if !currentCveIDsSet[v.CveID] {
 | 
			
		||||
			v.DiffStatus = models.DiffMinus
 | 
			
		||||
			clear[v.CveID] = v
 | 
			
		||||
			util.Log.Debugf("clear: %s", v.CveID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(clear) == 0 {
 | 
			
		||||
		util.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return clear
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool {
 | 
			
		||||
	cTypes := []models.CveContentType{
 | 
			
		||||
		models.Nvd,
 | 
			
		||||
		models.Jvn,
 | 
			
		||||
		models.NewCveContentType(current.Family),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prevLastModified := map[models.CveContentType]time.Time{}
 | 
			
		||||
	preVinfo, ok := previous.ScannedCves[cveID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	for _, cType := range cTypes {
 | 
			
		||||
		if content, ok := preVinfo.CveContents[cType]; ok {
 | 
			
		||||
			prevLastModified[cType] = content.LastModified
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	curLastModified := map[models.CveContentType]time.Time{}
 | 
			
		||||
	curVinfo, ok := current.ScannedCves[cveID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	for _, cType := range cTypes {
 | 
			
		||||
		if content, ok := curVinfo.CveContents[cType]; ok {
 | 
			
		||||
			curLastModified[cType] = content.LastModified
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, t := range cTypes {
 | 
			
		||||
		if !curLastModified[t].Equal(prevLastModified[t]) {
 | 
			
		||||
			util.Log.Debugf("%s LastModified not equal: \n%s\n%s",
 | 
			
		||||
				cveID, curLastModified[t], prevLastModified[t])
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// jsonDirPattern is file name pattern of JSON directory
 | 
			
		||||
// 2016-11-16T10:43:28+09:00
 | 
			
		||||
// 2016-11-16T10:43:28Z
 | 
			
		||||
var jsonDirPattern = regexp.MustCompile(
 | 
			
		||||
	`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`)
 | 
			
		||||
 | 
			
		||||
// ListValidJSONDirs returns valid json directory as array
 | 
			
		||||
// Returned array is sorted so that recent directories are at the head
 | 
			
		||||
func ListValidJSONDirs() (dirs []string, err error) {
 | 
			
		||||
	var dirInfo []os.FileInfo
 | 
			
		||||
	if dirInfo, err = ioutil.ReadDir(config.Conf.ResultsDir); err != nil {
 | 
			
		||||
		err = xerrors.Errorf("Failed to read %s: %w",
 | 
			
		||||
			config.Conf.ResultsDir, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, d := range dirInfo {
 | 
			
		||||
		if d.IsDir() && jsonDirPattern.MatchString(d.Name()) {
 | 
			
		||||
			jsonDir := filepath.Join(config.Conf.ResultsDir, d.Name())
 | 
			
		||||
			dirs = append(dirs, jsonDir)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	sort.Slice(dirs, func(i, j int) bool {
 | 
			
		||||
		return dirs[j] < dirs[i]
 | 
			
		||||
	})
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JSONDir returns
 | 
			
		||||
// If there is an arg, check if it is a valid format and return the corresponding path under results.
 | 
			
		||||
// If arg passed via PIPE (such as history subcommand), return that path.
 | 
			
		||||
// Otherwise, returns the path of the latest directory
 | 
			
		||||
func JSONDir(args []string) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	var dirs []string
 | 
			
		||||
 | 
			
		||||
	if 0 < len(args) {
 | 
			
		||||
		if dirs, err = ListValidJSONDirs(); err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		path := filepath.Join(config.Conf.ResultsDir, args[0])
 | 
			
		||||
		for _, d := range dirs {
 | 
			
		||||
			ss := strings.Split(d, string(os.PathSeparator))
 | 
			
		||||
			timedir := ss[len(ss)-1]
 | 
			
		||||
			if timedir == args[0] {
 | 
			
		||||
				return path, nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return "", xerrors.Errorf("Invalid path: %s", path)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// PIPE
 | 
			
		||||
	if config.Conf.Pipe {
 | 
			
		||||
		bytes, err := ioutil.ReadAll(os.Stdin)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", xerrors.Errorf("Failed to read stdin: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		fields := strings.Fields(string(bytes))
 | 
			
		||||
		if 0 < len(fields) {
 | 
			
		||||
			return filepath.Join(config.Conf.ResultsDir, fields[0]), nil
 | 
			
		||||
		}
 | 
			
		||||
		return "", xerrors.Errorf("Stdin is invalid: %s", string(bytes))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// returns latest dir when no args or no PIPE
 | 
			
		||||
	if dirs, err = ListValidJSONDirs(); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if len(dirs) == 0 {
 | 
			
		||||
		return "", xerrors.Errorf("No results under %s",
 | 
			
		||||
			config.Conf.ResultsDir)
 | 
			
		||||
	}
 | 
			
		||||
	return dirs[0], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadScanResults read JSON data
 | 
			
		||||
func LoadScanResults(jsonDir string) (results models.ScanResults, err error) {
 | 
			
		||||
	var files []os.FileInfo
 | 
			
		||||
	if files, err = ioutil.ReadDir(jsonDir); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to read %s: %w", jsonDir, err)
 | 
			
		||||
	}
 | 
			
		||||
	for _, f := range files {
 | 
			
		||||
		if filepath.Ext(f.Name()) != ".json" || strings.HasSuffix(f.Name(), "_diff.json") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var r *models.ScanResult
 | 
			
		||||
		path := filepath.Join(jsonDir, f.Name())
 | 
			
		||||
		if r, err = loadOneServerScanResult(path); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		results = append(results, *r)
 | 
			
		||||
	}
 | 
			
		||||
	if len(results) == 0 {
 | 
			
		||||
		return nil, xerrors.Errorf("There is no json file under %s", jsonDir)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadOneServerScanResult read JSON data of one server
 | 
			
		||||
func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		data []byte
 | 
			
		||||
		err  error
 | 
			
		||||
	)
 | 
			
		||||
	if data, err = ioutil.ReadFile(jsonFile); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to read %s: %w", jsonFile, err)
 | 
			
		||||
	}
 | 
			
		||||
	result := &models.ScanResult{}
 | 
			
		||||
	if err := json.Unmarshal(data, result); err != nil {
 | 
			
		||||
		return nil, xerrors.Errorf("Failed to parse %s: %w", jsonFile, err)
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										632
									
								
								reporter/util_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										632
									
								
								reporter/util_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,632 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"github.com/future-architect/vuls/util"
 | 
			
		||||
	"github.com/k0kubun/pp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	util.Log = util.NewCustomLogger(config.ServerInfo{})
 | 
			
		||||
	pp.ColoringEnabled = false
 | 
			
		||||
	code := m.Run()
 | 
			
		||||
	os.Exit(code)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIsCveInfoUpdated(t *testing.T) {
 | 
			
		||||
	f := "2006-01-02"
 | 
			
		||||
	old, _ := time.Parse(f, "2015-12-15")
 | 
			
		||||
	new, _ := time.Parse(f, "2015-12-16")
 | 
			
		||||
 | 
			
		||||
	type In struct {
 | 
			
		||||
		cveID string
 | 
			
		||||
		cur   models.ScanResult
 | 
			
		||||
		prev  models.ScanResult
 | 
			
		||||
	}
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		in       In
 | 
			
		||||
		expected bool
 | 
			
		||||
	}{
 | 
			
		||||
		// NVD compare non-initialized times
 | 
			
		||||
		{
 | 
			
		||||
			in: In{
 | 
			
		||||
				cveID: "CVE-2017-0001",
 | 
			
		||||
				cur: models.ScanResult{
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0001": {
 | 
			
		||||
							CveID: "CVE-2017-0001",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Nvd,
 | 
			
		||||
									CveID:        "CVE-2017-0001",
 | 
			
		||||
									LastModified: time.Time{},
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				prev: models.ScanResult{
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0001": {
 | 
			
		||||
							CveID: "CVE-2017-0001",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Nvd,
 | 
			
		||||
									CveID:        "CVE-2017-0001",
 | 
			
		||||
									LastModified: time.Time{},
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		// JVN not updated
 | 
			
		||||
		{
 | 
			
		||||
			in: In{
 | 
			
		||||
				cveID: "CVE-2017-0002",
 | 
			
		||||
				cur: models.ScanResult{
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0002": {
 | 
			
		||||
							CveID: "CVE-2017-0002",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Jvn,
 | 
			
		||||
									CveID:        "CVE-2017-0002",
 | 
			
		||||
									LastModified: old,
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				prev: models.ScanResult{
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0002": {
 | 
			
		||||
							CveID: "CVE-2017-0002",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Jvn,
 | 
			
		||||
									CveID:        "CVE-2017-0002",
 | 
			
		||||
									LastModified: old,
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		// OVAL updated
 | 
			
		||||
		{
 | 
			
		||||
			in: In{
 | 
			
		||||
				cveID: "CVE-2017-0003",
 | 
			
		||||
				cur: models.ScanResult{
 | 
			
		||||
					Family: "ubuntu",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0003": {
 | 
			
		||||
							CveID: "CVE-2017-0003",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Nvd,
 | 
			
		||||
									CveID:        "CVE-2017-0002",
 | 
			
		||||
									LastModified: new,
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				prev: models.ScanResult{
 | 
			
		||||
					Family: "ubuntu",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0003": {
 | 
			
		||||
							CveID: "CVE-2017-0003",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Nvd,
 | 
			
		||||
									CveID:        "CVE-2017-0002",
 | 
			
		||||
									LastModified: old,
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		// OVAL newly detected
 | 
			
		||||
		{
 | 
			
		||||
			in: In{
 | 
			
		||||
				cveID: "CVE-2017-0004",
 | 
			
		||||
				cur: models.ScanResult{
 | 
			
		||||
					Family: "redhat",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2017-0004": {
 | 
			
		||||
							CveID: "CVE-2017-0004",
 | 
			
		||||
							CveContents: models.NewCveContents(
 | 
			
		||||
								models.CveContent{
 | 
			
		||||
									Type:         models.Nvd,
 | 
			
		||||
									CveID:        "CVE-2017-0002",
 | 
			
		||||
									LastModified: old,
 | 
			
		||||
								},
 | 
			
		||||
							),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				prev: models.ScanResult{
 | 
			
		||||
					Family:      "redhat",
 | 
			
		||||
					ScannedCves: models.VulnInfos{},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for i, tt := range tests {
 | 
			
		||||
		actual := isCveInfoUpdated(tt.in.cveID, tt.in.prev, tt.in.cur)
 | 
			
		||||
		if actual != tt.expected {
 | 
			
		||||
			t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPlusMinusDiff(t *testing.T) {
 | 
			
		||||
	atCurrent, _ := time.Parse("2006-01-02", "2014-12-31")
 | 
			
		||||
	atPrevious, _ := time.Parse("2006-01-02", "2014-11-31")
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		inCurrent  models.ScanResults
 | 
			
		||||
		inPrevious models.ScanResults
 | 
			
		||||
		out        models.ScanResult
 | 
			
		||||
	}{
 | 
			
		||||
		//same
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:   atCurrent,
 | 
			
		||||
				ServerName:  "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		//plus, minus
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2016-6662": {
 | 
			
		||||
							CveID:            "CVE-2016-6662",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					Packages: models.Packages{
 | 
			
		||||
						"mysql-libs": {
 | 
			
		||||
							Name:       "mysql-libs",
 | 
			
		||||
							Version:    "5.1.73",
 | 
			
		||||
							Release:    "7.el6",
 | 
			
		||||
							NewVersion: "5.1.73",
 | 
			
		||||
							NewRelease: "8.el6_8",
 | 
			
		||||
							Repository: "",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2020-6662": {
 | 
			
		||||
							CveID:            "CVE-2020-6662",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "bind"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					Packages: models.Packages{
 | 
			
		||||
						"bind": {
 | 
			
		||||
							Name:       "bind",
 | 
			
		||||
							Version:    "5.1.73",
 | 
			
		||||
							Release:    "7.el6",
 | 
			
		||||
							NewVersion: "5.1.73",
 | 
			
		||||
							NewRelease: "8.el6_8",
 | 
			
		||||
							Repository: "",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:  atCurrent,
 | 
			
		||||
				ServerName: "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{
 | 
			
		||||
					"CVE-2016-6662": {
 | 
			
		||||
						CveID:            "CVE-2016-6662",
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						DiffStatus:       "+",
 | 
			
		||||
					},
 | 
			
		||||
					"CVE-2020-6662": {
 | 
			
		||||
						CveID:            "CVE-2020-6662",
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{{Name: "bind"}},
 | 
			
		||||
						DiffStatus:       "-",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Packages: models.Packages{
 | 
			
		||||
					"mysql-libs": {
 | 
			
		||||
						Name:       "mysql-libs",
 | 
			
		||||
						Version:    "5.1.73",
 | 
			
		||||
						Release:    "7.el6",
 | 
			
		||||
						NewVersion: "5.1.73",
 | 
			
		||||
						NewRelease: "8.el6_8",
 | 
			
		||||
						Repository: "",
 | 
			
		||||
					},
 | 
			
		||||
					"bind": {
 | 
			
		||||
						Name:       "bind",
 | 
			
		||||
						Version:    "5.1.73",
 | 
			
		||||
						Release:    "7.el6",
 | 
			
		||||
						NewVersion: "5.1.73",
 | 
			
		||||
						NewRelease: "8.el6_8",
 | 
			
		||||
						Repository: "",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, tt := range tests {
 | 
			
		||||
		diff := diff(tt.inCurrent, tt.inPrevious, true, true)
 | 
			
		||||
		for _, actual := range diff {
 | 
			
		||||
			if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) {
 | 
			
		||||
				h := pp.Sprint(actual.ScannedCves)
 | 
			
		||||
				x := pp.Sprint(tt.out.ScannedCves)
 | 
			
		||||
				t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for j := range tt.out.Packages {
 | 
			
		||||
				if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) {
 | 
			
		||||
					h := pp.Sprint(tt.out.Packages[j])
 | 
			
		||||
					x := pp.Sprint(actual.Packages[j])
 | 
			
		||||
					t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPlusDiff(t *testing.T) {
 | 
			
		||||
	atCurrent, _ := time.Parse("2006-01-02", "2014-12-31")
 | 
			
		||||
	atPrevious, _ := time.Parse("2006-01-02", "2014-11-31")
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		inCurrent  models.ScanResults
 | 
			
		||||
		inPrevious models.ScanResults
 | 
			
		||||
		out        models.ScanResult
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			// same
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:   atCurrent,
 | 
			
		||||
				ServerName:  "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		// plus
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2016-6662": {
 | 
			
		||||
							CveID:            "CVE-2016-6662",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					Packages: models.Packages{
 | 
			
		||||
						"mysql-libs": {
 | 
			
		||||
							Name:       "mysql-libs",
 | 
			
		||||
							Version:    "5.1.73",
 | 
			
		||||
							Release:    "7.el6",
 | 
			
		||||
							NewVersion: "5.1.73",
 | 
			
		||||
							NewRelease: "8.el6_8",
 | 
			
		||||
							Repository: "",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:  atCurrent,
 | 
			
		||||
				ServerName: "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{
 | 
			
		||||
					"CVE-2016-6662": {
 | 
			
		||||
						CveID:            "CVE-2016-6662",
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						DiffStatus:       "+",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Packages: models.Packages{
 | 
			
		||||
					"mysql-libs": {
 | 
			
		||||
						Name:       "mysql-libs",
 | 
			
		||||
						Version:    "5.1.73",
 | 
			
		||||
						Release:    "7.el6",
 | 
			
		||||
						NewVersion: "5.1.73",
 | 
			
		||||
						NewRelease: "8.el6_8",
 | 
			
		||||
						Repository: "",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		// minus
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID: "CVE-2012-6702",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID: "CVE-2012-6702",
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID: "CVE-2014-9761",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:   atCurrent,
 | 
			
		||||
				ServerName:  "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, tt := range tests {
 | 
			
		||||
		diff := diff(tt.inCurrent, tt.inPrevious, true, false)
 | 
			
		||||
		for _, actual := range diff {
 | 
			
		||||
			if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) {
 | 
			
		||||
				h := pp.Sprint(actual.ScannedCves)
 | 
			
		||||
				x := pp.Sprint(tt.out.ScannedCves)
 | 
			
		||||
				t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for j := range tt.out.Packages {
 | 
			
		||||
				if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) {
 | 
			
		||||
					h := pp.Sprint(tt.out.Packages[j])
 | 
			
		||||
					x := pp.Sprint(actual.Packages[j])
 | 
			
		||||
					t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMinusDiff(t *testing.T) {
 | 
			
		||||
	atCurrent, _ := time.Parse("2006-01-02", "2014-12-31")
 | 
			
		||||
	atPrevious, _ := time.Parse("2006-01-02", "2014-11-31")
 | 
			
		||||
	var tests = []struct {
 | 
			
		||||
		inCurrent  models.ScanResults
 | 
			
		||||
		inPrevious models.ScanResults
 | 
			
		||||
		out        models.ScanResult
 | 
			
		||||
	}{
 | 
			
		||||
		// same
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2012-6702": {
 | 
			
		||||
							CveID:            "CVE-2012-6702",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
 | 
			
		||||
						},
 | 
			
		||||
						"CVE-2014-9761": {
 | 
			
		||||
							CveID:            "CVE-2014-9761",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:   atCurrent,
 | 
			
		||||
				ServerName:  "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		// minus
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					Packages:   models.Packages{},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atCurrent,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2016-6662": {
 | 
			
		||||
							CveID:            "CVE-2016-6662",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					Packages: models.Packages{
 | 
			
		||||
						"mysql-libs": {
 | 
			
		||||
							Name:       "mysql-libs",
 | 
			
		||||
							Version:    "5.1.73",
 | 
			
		||||
							Release:    "7.el6",
 | 
			
		||||
							NewVersion: "5.1.73",
 | 
			
		||||
							NewRelease: "8.el6_8",
 | 
			
		||||
							Repository: "",
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:  atCurrent,
 | 
			
		||||
				ServerName: "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{
 | 
			
		||||
					"CVE-2016-6662": {
 | 
			
		||||
						CveID:            "CVE-2016-6662",
 | 
			
		||||
						AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						DiffStatus:       "-",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Packages: models.Packages{
 | 
			
		||||
					"mysql-libs": {
 | 
			
		||||
						Name:       "mysql-libs",
 | 
			
		||||
						Version:    "5.1.73",
 | 
			
		||||
						Release:    "7.el6",
 | 
			
		||||
						NewVersion: "5.1.73",
 | 
			
		||||
						NewRelease: "8.el6_8",
 | 
			
		||||
						Repository: "",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		// plus
 | 
			
		||||
		{
 | 
			
		||||
			inCurrent: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:  atPrevious,
 | 
			
		||||
					ServerName: "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{
 | 
			
		||||
						"CVE-2016-6662": {
 | 
			
		||||
							CveID:            "CVE-2016-6662",
 | 
			
		||||
							AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			inPrevious: models.ScanResults{
 | 
			
		||||
				{
 | 
			
		||||
					ScannedAt:   atCurrent,
 | 
			
		||||
					ServerName:  "u16",
 | 
			
		||||
					ScannedCves: models.VulnInfos{},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			out: models.ScanResult{
 | 
			
		||||
				ScannedAt:   atCurrent,
 | 
			
		||||
				ServerName:  "u16",
 | 
			
		||||
				ScannedCves: models.VulnInfos{},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, tt := range tests {
 | 
			
		||||
		diff := diff(tt.inCurrent, tt.inPrevious, false, true)
 | 
			
		||||
		for _, actual := range diff {
 | 
			
		||||
			if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) {
 | 
			
		||||
				h := pp.Sprint(actual.ScannedCves)
 | 
			
		||||
				x := pp.Sprint(tt.out.ScannedCves)
 | 
			
		||||
				t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for j := range tt.out.Packages {
 | 
			
		||||
				if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) {
 | 
			
		||||
					h := pp.Sprint(tt.out.Packages[j])
 | 
			
		||||
					x := pp.Sprint(actual.Packages[j])
 | 
			
		||||
					t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								reporter/writer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								reporter/writer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
package reporter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ResultWriter Interface
 | 
			
		||||
type ResultWriter interface {
 | 
			
		||||
	Write(...models.ScanResult) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func gz(data []byte) ([]byte, error) {
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
	gz := gzip.NewWriter(&b)
 | 
			
		||||
	if _, err := gz.Write(data); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := gz.Flush(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := gz.Close(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return b.Bytes(), nil
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user