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