Add report subcommand, change scan options. Bump up ver #239
This commit is contained in:
@@ -20,6 +20,7 @@ package report
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -27,12 +28,76 @@ import (
|
||||
|
||||
c "github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/future-architect/vuls/util"
|
||||
)
|
||||
|
||||
// AzureBlobWriter writes results to AzureBlob
|
||||
type AzureBlobWriter struct{}
|
||||
|
||||
// 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 c.Conf.FormatOneLineText {
|
||||
timestr := rs[0].ScannedAt.Format(time.RFC3339)
|
||||
k := fmt.Sprintf(timestr + "/summary.txt")
|
||||
text := toOneLineSummary(rs...)
|
||||
b := []byte(text)
|
||||
if err := createBlockBlob(cli, k, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rs {
|
||||
key := r.ReportKeyName()
|
||||
if c.Conf.FormatJSON {
|
||||
k := key + ".json"
|
||||
var b []byte
|
||||
if b, err = json.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
if err := createBlockBlob(cli, k, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatShortText {
|
||||
k := key + "_short.txt"
|
||||
b := []byte(toShortPlainText(r))
|
||||
if err := createBlockBlob(cli, k, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatFullText {
|
||||
k := key + "_full.txt"
|
||||
b := []byte(toFullPlainText(r))
|
||||
if err := createBlockBlob(cli, k, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatXML {
|
||||
k := key + ".xml"
|
||||
var b []byte
|
||||
if b, err = xml.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to XML: %s", err)
|
||||
}
|
||||
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
|
||||
if err := createBlockBlob(cli, k, allBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIfAzureContainerExists check the existence of Azure storage container
|
||||
func CheckIfAzureContainerExists() error {
|
||||
cli, err := getBlobClient()
|
||||
@@ -57,84 +122,24 @@ func getBlobClient() (storage.BlobStorageClient, error) {
|
||||
return api.GetBlobService(), nil
|
||||
}
|
||||
|
||||
// Write results to Azure Blob storage
|
||||
func (w AzureBlobWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
reqChan := make(chan models.ScanResult, len(scanResults))
|
||||
resChan := make(chan bool)
|
||||
errChan := make(chan error, len(scanResults))
|
||||
defer close(resChan)
|
||||
defer close(errChan)
|
||||
defer close(reqChan)
|
||||
|
||||
timeout := time.After(10 * 60 * time.Second)
|
||||
concurrency := 10
|
||||
tasks := util.GenWorkers(concurrency)
|
||||
|
||||
go func() {
|
||||
for _, r := range scanResults {
|
||||
reqChan <- r
|
||||
}
|
||||
}()
|
||||
|
||||
for range scanResults {
|
||||
tasks <- func() {
|
||||
select {
|
||||
case sresult := <-reqChan:
|
||||
func(r models.ScanResult) {
|
||||
err := w.upload(r)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
resChan <- true
|
||||
}(sresult)
|
||||
}
|
||||
func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte) error {
|
||||
var err error
|
||||
if c.Conf.GZIP {
|
||||
if b, err = gz(b); err != nil {
|
||||
return err
|
||||
}
|
||||
k = k + ".gz"
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
for i := 0; i < len(scanResults); i++ {
|
||||
select {
|
||||
case <-resChan:
|
||||
case err := <-errChan:
|
||||
errs = append(errs, err)
|
||||
case <-timeout:
|
||||
errs = append(errs, fmt.Errorf("Timeout while uploading to azure Blob"))
|
||||
}
|
||||
}
|
||||
|
||||
if 0 < len(errs) {
|
||||
return fmt.Errorf("Failed to upload json to Azure Blob: %v", errs)
|
||||
if err := cli.CreateBlockBlobFromReader(
|
||||
c.Conf.AzureContainer,
|
||||
k,
|
||||
uint64(len(b)),
|
||||
bytes.NewReader(b),
|
||||
map[string]string{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("Failed to upload data to %s/%s, %s",
|
||||
c.Conf.AzureContainer, k, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w AzureBlobWriter) upload(res models.ScanResult) (err error) {
|
||||
cli, err := getBlobClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timestr := time.Now().Format(time.RFC3339)
|
||||
name := ""
|
||||
if len(res.Container.ContainerID) == 0 {
|
||||
name = fmt.Sprintf("%s/%s.json", timestr, res.ServerName)
|
||||
} else {
|
||||
name = fmt.Sprintf("%s/%s_%s.json", timestr, res.ServerName, res.Container.Name)
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
|
||||
if err = cli.CreateBlockBlobFromReader(
|
||||
c.Conf.AzureContainer,
|
||||
name,
|
||||
uint64(len(jsonBytes)),
|
||||
bytes.NewReader(jsonBytes),
|
||||
map[string]string{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("%s/%s, %s",
|
||||
c.Conf.AzureContainer, name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,27 +29,27 @@ import (
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// MailWriter send mail
|
||||
type MailWriter struct{}
|
||||
// EMailWriter send mail
|
||||
type EMailWriter struct{}
|
||||
|
||||
func (w MailWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
func (w EMailWriter) Write(rs ...models.ScanResult) (err error) {
|
||||
conf := config.Conf
|
||||
for _, s := range scanResults {
|
||||
to := strings.Join(conf.Mail.To[:], ", ")
|
||||
cc := strings.Join(conf.Mail.Cc[:], ", ")
|
||||
mailAddresses := append(conf.Mail.To, conf.Mail.Cc...)
|
||||
if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil {
|
||||
return fmt.Errorf("Failed to parse email addresses: %s", err)
|
||||
}
|
||||
to := strings.Join(conf.EMail.To[:], ", ")
|
||||
cc := strings.Join(conf.EMail.Cc[:], ", ")
|
||||
mailAddresses := append(conf.EMail.To, conf.EMail.Cc...)
|
||||
if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil {
|
||||
return fmt.Errorf("Failed to parse email addresses: %s", err)
|
||||
}
|
||||
|
||||
for _, r := range rs {
|
||||
subject := fmt.Sprintf("%s%s %s",
|
||||
conf.Mail.SubjectPrefix,
|
||||
s.ServerInfo(),
|
||||
s.CveSummary(),
|
||||
conf.EMail.SubjectPrefix,
|
||||
r.ServerInfo(),
|
||||
r.CveSummary(),
|
||||
)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = conf.Mail.From
|
||||
headers["From"] = conf.EMail.From
|
||||
headers["To"] = to
|
||||
headers["Cc"] = cc
|
||||
headers["Subject"] = subject
|
||||
@@ -60,25 +60,19 @@ func (w MailWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
for k, v := range headers {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
message += "\r\n" + toFullPlainText(r)
|
||||
|
||||
var body string
|
||||
if body, err = toPlainText(s); err != nil {
|
||||
return err
|
||||
}
|
||||
message += "\r\n" + body
|
||||
|
||||
smtpServer := net.JoinHostPort(conf.Mail.SMTPAddr, conf.Mail.SMTPPort)
|
||||
|
||||
err := smtp.SendMail(
|
||||
smtpServer := net.JoinHostPort(conf.EMail.SMTPAddr, conf.EMail.SMTPPort)
|
||||
err = smtp.SendMail(
|
||||
smtpServer,
|
||||
smtp.PlainAuth(
|
||||
"",
|
||||
conf.Mail.User,
|
||||
conf.Mail.Password,
|
||||
conf.Mail.SMTPAddr,
|
||||
conf.EMail.User,
|
||||
conf.EMail.Password,
|
||||
conf.EMail.SMTPAddr,
|
||||
),
|
||||
conf.Mail.From,
|
||||
conf.Mail.To,
|
||||
conf.EMail.From,
|
||||
conf.EMail.To,
|
||||
[]byte(message),
|
||||
)
|
||||
|
||||
150
report/json.go
150
report/json.go
@@ -1,150 +0,0 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
c "github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// JSONDirs array of json files path.
|
||||
type JSONDirs []string
|
||||
|
||||
func (d JSONDirs) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
func (d JSONDirs) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
||||
func (d JSONDirs) Less(i, j int) bool {
|
||||
return d[j] < d[i]
|
||||
}
|
||||
|
||||
// JSONWriter writes results to file.
|
||||
type JSONWriter struct {
|
||||
ScannedAt time.Time
|
||||
}
|
||||
|
||||
func (w JSONWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
var path string
|
||||
if path, err = ensureResultDir(w.ScannedAt); err != nil {
|
||||
return fmt.Errorf("Failed to make direcotory/symlink : %s", err)
|
||||
}
|
||||
|
||||
for _, scanResult := range scanResults {
|
||||
scanResult.ScannedAt = w.ScannedAt
|
||||
}
|
||||
|
||||
var jsonBytes []byte
|
||||
for _, r := range scanResults {
|
||||
jsonPath := ""
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
jsonPath = filepath.Join(path, fmt.Sprintf("%s.json", r.ServerName))
|
||||
} else {
|
||||
jsonPath = filepath.Join(path,
|
||||
fmt.Sprintf("%s_%s.json", r.ServerName, r.Container.Name))
|
||||
}
|
||||
|
||||
if jsonBytes, err = json.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(jsonPath, jsonBytes, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write JSON. path: %s, err: %s", jsonPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONDirPattern is file name pattern of JSON directory
|
||||
var JSONDirPattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$`)
|
||||
|
||||
// GetValidJSONDirs return valid json directory as array
|
||||
func GetValidJSONDirs() (jsonDirs JSONDirs, err error) {
|
||||
var dirInfo []os.FileInfo
|
||||
if dirInfo, err = ioutil.ReadDir(c.Conf.ResultsDir); err != nil {
|
||||
err = fmt.Errorf("Failed to read %s: %s", c.Conf.ResultsDir, err)
|
||||
return
|
||||
}
|
||||
for _, d := range dirInfo {
|
||||
if d.IsDir() && JSONDirPattern.MatchString(d.Name()) {
|
||||
jsonDir := filepath.Join(c.Conf.ResultsDir, d.Name())
|
||||
jsonDirs = append(jsonDirs, jsonDir)
|
||||
}
|
||||
}
|
||||
sort.Sort(jsonDirs)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadOneScanHistory read JSON data
|
||||
func LoadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err error) {
|
||||
var scanResults []models.ScanResult
|
||||
var files []os.FileInfo
|
||||
if files, err = ioutil.ReadDir(jsonDir); err != nil {
|
||||
err = fmt.Errorf("Failed to read %s: %s", jsonDir, err)
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
var scanResult models.ScanResult
|
||||
var data []byte
|
||||
jsonPath := filepath.Join(jsonDir, file.Name())
|
||||
if data, err = ioutil.ReadFile(jsonPath); err != nil {
|
||||
err = fmt.Errorf("Failed to read %s: %s", jsonPath, err)
|
||||
return
|
||||
}
|
||||
if json.Unmarshal(data, &scanResult) != nil {
|
||||
err = fmt.Errorf("Failed to parse %s: %s", jsonPath, err)
|
||||
return
|
||||
}
|
||||
scanResults = append(scanResults, scanResult)
|
||||
}
|
||||
if len(scanResults) == 0 {
|
||||
err = fmt.Errorf("There is no json file under %s", jsonDir)
|
||||
return
|
||||
}
|
||||
|
||||
var scannedAt time.Time
|
||||
if scanResults[0].ScannedAt.IsZero() {
|
||||
splitPath := strings.Split(jsonDir, string(os.PathSeparator))
|
||||
timeStr := splitPath[len(splitPath)-1]
|
||||
if scannedAt, err = time.Parse(time.RFC3339, timeStr); err != nil {
|
||||
err = fmt.Errorf("Failed to parse %s: %s", timeStr, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
scannedAt = scanResults[0].ScannedAt
|
||||
}
|
||||
|
||||
scanHistory = models.ScanHistory{
|
||||
ScanResults: scanResults,
|
||||
ScannedAt: scannedAt,
|
||||
}
|
||||
return
|
||||
}
|
||||
111
report/localfile.go
Normal file
111
report/localfile.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
c "github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// LocalFileWriter writes results to a local file.
|
||||
type LocalFileWriter struct {
|
||||
CurrentDir string
|
||||
}
|
||||
|
||||
func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
|
||||
if c.Conf.FormatOneLineText {
|
||||
path := filepath.Join(w.CurrentDir, "summary.txt")
|
||||
text := toOneLineSummary(rs...)
|
||||
if err := writeFile(path, []byte(text), 0600); err != nil {
|
||||
return fmt.Errorf(
|
||||
"Failed to write to file. path: %s, err: %s",
|
||||
path, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rs {
|
||||
path := filepath.Join(w.CurrentDir, r.ReportFileName())
|
||||
|
||||
if c.Conf.FormatJSON {
|
||||
p := path + ".json"
|
||||
var b []byte
|
||||
if b, err = json.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
if err := writeFile(p, b, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write JSON. path: %s, err: %s", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatShortText {
|
||||
p := path + "_short.txt"
|
||||
if err := writeFile(
|
||||
p, []byte(toShortPlainText(r)), 0600); err != nil {
|
||||
return fmt.Errorf(
|
||||
"Failed to write text files. path: %s, err: %s", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatFullText {
|
||||
p := path + "_full.txt"
|
||||
if err := writeFile(
|
||||
p, []byte(toFullPlainText(r)), 0600); err != nil {
|
||||
return fmt.Errorf(
|
||||
"Failed to write text files. path: %s, err: %s", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatXML {
|
||||
p := path + ".xml"
|
||||
var b []byte
|
||||
if b, err = xml.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to XML: %s", err)
|
||||
}
|
||||
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
|
||||
if err := writeFile(p, allBytes, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write XML. path: %s, err: %s", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFile(path string, data []byte, perm os.FileMode) error {
|
||||
var err error
|
||||
if c.Conf.GZIP {
|
||||
if data, err = gz(data); err != nil {
|
||||
return err
|
||||
}
|
||||
path = path + ".gz"
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(
|
||||
path, []byte(data), perm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/future-architect/vuls/models"
|
||||
formatter "github.com/kotakanbe/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
// LogrusWriter write to logfile
|
||||
type LogrusWriter struct {
|
||||
}
|
||||
|
||||
func (w LogrusWriter) Write(scanResults []models.ScanResult) error {
|
||||
path := "/var/log/vuls/report.log"
|
||||
if runtime.GOOS == "windows" {
|
||||
path = filepath.Join(os.Getenv("APPDATA"), "vuls", "report.log")
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log := logrus.New()
|
||||
log.Formatter = &formatter.TextFormatter{}
|
||||
log.Out = f
|
||||
log.Level = logrus.InfoLevel
|
||||
|
||||
for _, s := range scanResults {
|
||||
text, err := toPlainText(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof(text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
125
report/s3.go
125
report/s3.go
@@ -20,6 +20,7 @@ package report
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -32,6 +33,78 @@ import (
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// S3Writer writes results to S3
|
||||
type S3Writer struct{}
|
||||
|
||||
func getS3() *s3.S3 {
|
||||
return s3.New(session.New(&aws.Config{
|
||||
Region: aws.String(c.Conf.AwsRegion),
|
||||
Credentials: credentials.NewSharedCredentials("", c.Conf.AwsProfile),
|
||||
}))
|
||||
}
|
||||
|
||||
// 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 := getS3()
|
||||
|
||||
if c.Conf.FormatOneLineText {
|
||||
timestr := rs[0].ScannedAt.Format(time.RFC3339)
|
||||
k := fmt.Sprintf(timestr + "/summary.txt")
|
||||
text := toOneLineSummary(rs...)
|
||||
if err := putObject(svc, k, []byte(text)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rs {
|
||||
key := r.ReportKeyName()
|
||||
if c.Conf.FormatJSON {
|
||||
k := key + ".json"
|
||||
var b []byte
|
||||
if b, err = json.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
if err := putObject(svc, k, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatShortText {
|
||||
k := key + "_short.txt"
|
||||
text := toShortPlainText(r)
|
||||
if err := putObject(svc, k, []byte(text)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatFullText {
|
||||
k := key + "_full.txt"
|
||||
text := toFullPlainText(r)
|
||||
if err := putObject(svc, k, []byte(text)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatXML {
|
||||
k := key + ".xml"
|
||||
var b []byte
|
||||
if b, err = xml.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to XML: %s", err)
|
||||
}
|
||||
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
|
||||
if err := putObject(svc, k, allBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckIfBucketExists check the existence of S3 bucket
|
||||
func CheckIfBucketExists() error {
|
||||
svc := getS3()
|
||||
@@ -57,46 +130,22 @@ func CheckIfBucketExists() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// S3Writer writes results to S3
|
||||
type S3Writer struct{}
|
||||
|
||||
func getS3() *s3.S3 {
|
||||
return s3.New(session.New(&aws.Config{
|
||||
Region: aws.String(c.Conf.AwsRegion),
|
||||
Credentials: credentials.NewSharedCredentials("", c.Conf.AwsProfile),
|
||||
}))
|
||||
}
|
||||
|
||||
// Write results to S3
|
||||
func (w S3Writer) Write(scanResults []models.ScanResult) (err error) {
|
||||
|
||||
var jsonBytes []byte
|
||||
if jsonBytes, err = json.Marshal(scanResults); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
func putObject(svc *s3.S3, k string, b []byte) error {
|
||||
var err error
|
||||
if c.Conf.GZIP {
|
||||
if b, err = gz(b); err != nil {
|
||||
return err
|
||||
}
|
||||
k = k + ".gz"
|
||||
}
|
||||
|
||||
// http://docs.aws.amazon.com/sdk-for-go/latest/v1/developerguide/common-examples.title.html
|
||||
svc := getS3()
|
||||
timestr := time.Now().Format(time.RFC3339)
|
||||
for _, r := range scanResults {
|
||||
key := ""
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
key = fmt.Sprintf("%s/%s.json", timestr, r.ServerName)
|
||||
} else {
|
||||
key = fmt.Sprintf("%s/%s_%s.json", timestr, r.ServerName, r.Container.Name)
|
||||
}
|
||||
|
||||
if jsonBytes, err = json.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
|
||||
}
|
||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: &c.Conf.S3Bucket,
|
||||
Key: &key,
|
||||
Body: bytes.NewReader(jsonBytes),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to upload data to %s/%s, %s", c.Conf.S3Bucket, key, err)
|
||||
}
|
||||
if _, err := svc.PutObject(&s3.PutObjectInput{
|
||||
Bucket: &c.Conf.S3Bucket,
|
||||
Key: &k,
|
||||
Body: bytes.NewReader(b),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to upload data to %s/%s, %s",
|
||||
c.Conf.S3Bucket, k, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,36 +56,36 @@ type message struct {
|
||||
// SlackWriter send report to slack
|
||||
type SlackWriter struct{}
|
||||
|
||||
func (w SlackWriter) Write(scanResults []models.ScanResult) error {
|
||||
func (w SlackWriter) Write(rs ...models.ScanResult) error {
|
||||
conf := config.Conf.Slack
|
||||
for _, s := range scanResults {
|
||||
channel := conf.Channel
|
||||
channel := conf.Channel
|
||||
|
||||
for _, r := range rs {
|
||||
if channel == "${servername}" {
|
||||
channel = fmt.Sprintf("#%s", s.ServerName)
|
||||
channel = fmt.Sprintf("#%s", r.ServerName)
|
||||
}
|
||||
|
||||
msg := message{
|
||||
Text: msgText(s),
|
||||
Text: msgText(r),
|
||||
Username: conf.AuthUser,
|
||||
IconEmoji: conf.IconEmoji,
|
||||
Channel: channel,
|
||||
Attachments: toSlackAttachments(s),
|
||||
Attachments: toSlackAttachments(r),
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(msg)
|
||||
jsonBody := string(bytes)
|
||||
f := func() (err error) {
|
||||
resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).
|
||||
Send(string(jsonBody)).End()
|
||||
if resp.StatusCode != 200 {
|
||||
log.Errorf("Resonse body: %s", body)
|
||||
if 0 < len(errs) {
|
||||
return errs[0]
|
||||
}
|
||||
resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End()
|
||||
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
|
||||
return fmt.Errorf(
|
||||
"HTTP POST error: %v, url: %s, resp: %v, body: %s",
|
||||
errs, conf.HookURL, resp, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
notify := func(err error, t time.Duration) {
|
||||
log.Warn("Error %s", err)
|
||||
log.Warn("Retrying in ", t)
|
||||
}
|
||||
if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil {
|
||||
@@ -118,8 +118,8 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) {
|
||||
for _, p := range cveInfo.Packages {
|
||||
curentPackages = append(curentPackages, p.ToStringCurrentVersion())
|
||||
}
|
||||
for _, cpename := range cveInfo.CpeNames {
|
||||
curentPackages = append(curentPackages, cpename.Name)
|
||||
for _, n := range cveInfo.CpeNames {
|
||||
curentPackages = append(curentPackages, n)
|
||||
}
|
||||
|
||||
newPackages := []string{}
|
||||
|
||||
@@ -20,19 +20,40 @@ package report
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
c "github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// StdoutWriter write to stdout
|
||||
type StdoutWriter struct{}
|
||||
|
||||
func (w StdoutWriter) Write(scanResults []models.ScanResult) error {
|
||||
for _, s := range scanResults {
|
||||
text, err := toPlainText(s)
|
||||
if err != nil {
|
||||
return err
|
||||
// WriteScanSummary prints Scan summary at the end of scan
|
||||
func (w StdoutWriter) WriteScanSummary(rs ...models.ScanResult) {
|
||||
fmt.Printf("\n\n")
|
||||
fmt.Printf("Scan Summary\n")
|
||||
fmt.Printf("============\n")
|
||||
fmt.Printf("%s\n", toScanSummary(rs...))
|
||||
}
|
||||
|
||||
func (w StdoutWriter) Write(rs ...models.ScanResult) error {
|
||||
if c.Conf.FormatOneLineText {
|
||||
fmt.Print("\n\n")
|
||||
fmt.Println("One Line Summary")
|
||||
fmt.Println("================")
|
||||
fmt.Println(toOneLineSummary(rs...))
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if c.Conf.FormatShortText {
|
||||
for _, r := range rs {
|
||||
fmt.Println(toShortPlainText(r))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conf.FormatFullText {
|
||||
for _, r := range rs {
|
||||
fmt.Println(toFullPlainText(r))
|
||||
}
|
||||
fmt.Println(text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/* Vuls - Vulnerability Scanner
|
||||
Copyright (C) 2016 Future Architect, Inc. Japan.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
// TextFileWriter writes results to file.
|
||||
type TextFileWriter struct {
|
||||
ScannedAt time.Time
|
||||
}
|
||||
|
||||
func (w TextFileWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
path, err := ensureResultDir(w.ScannedAt)
|
||||
all := []string{}
|
||||
for _, r := range scanResults {
|
||||
textFilePath := ""
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
textFilePath = filepath.Join(path, fmt.Sprintf("%s.txt", r.ServerName))
|
||||
} else {
|
||||
textFilePath = filepath.Join(path,
|
||||
fmt.Sprintf("%s_%s.txt", r.ServerName, r.Container.Name))
|
||||
}
|
||||
text, err := toPlainText(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
all = append(all, text)
|
||||
b := []byte(text)
|
||||
if err := ioutil.WriteFile(textFilePath, b, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write text files. path: %s, err: %s", textFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
text := strings.Join(all, "\n\n")
|
||||
b := []byte(text)
|
||||
allPath := filepath.Join(path, "all.txt")
|
||||
if err := ioutil.WriteFile(allPath, b, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write text files. path: %s, err: %s", allPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package report
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -40,13 +40,8 @@ var currentCveInfo int
|
||||
var currentDetailLimitY int
|
||||
|
||||
// RunTui execute main logic
|
||||
func RunTui(jsonDirName string) subcommands.ExitStatus {
|
||||
var err error
|
||||
scanHistory, err = selectScanHistory(jsonDirName)
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
func RunTui(history models.ScanHistory) subcommands.ExitStatus {
|
||||
scanHistory = history
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputNormal)
|
||||
if err != nil {
|
||||
@@ -64,34 +59,14 @@ func RunTui(jsonDirName string) subcommands.ExitStatus {
|
||||
g.SelFgColor = gocui.ColorBlack
|
||||
g.Cursor = true
|
||||
|
||||
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||
if err := g.MainLoop(); err != nil {
|
||||
g.Close()
|
||||
log.Errorf("%s", err)
|
||||
return subcommands.ExitFailure
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
func selectScanHistory(jsonDirName string) (latest models.ScanHistory, err error) {
|
||||
var jsonDir string
|
||||
if 0 < len(jsonDirName) {
|
||||
jsonDir = filepath.Join(config.Conf.ResultsDir, jsonDirName)
|
||||
} else {
|
||||
var jsonDirs JSONDirs
|
||||
if jsonDirs, err = GetValidJSONDirs(); err != nil {
|
||||
return
|
||||
}
|
||||
if len(jsonDirs) == 0 {
|
||||
return latest, fmt.Errorf("No scan results are found in %s", config.Conf.ResultsDir)
|
||||
}
|
||||
jsonDir = jsonDirs[0]
|
||||
}
|
||||
if latest, err = LoadOneScanHistory(jsonDir); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func keybindings(g *gocui.Gui) (err error) {
|
||||
errs := []error{}
|
||||
|
||||
@@ -537,6 +512,9 @@ func setSideLayout(g *gocui.Gui) error {
|
||||
for _, result := range scanHistory.ScanResults {
|
||||
fmt.Fprintln(v, result.ServerInfoTui())
|
||||
}
|
||||
if len(scanHistory.ScanResults) == 0 {
|
||||
return fmt.Errorf("No scan results")
|
||||
}
|
||||
currentScanResult = scanHistory.ScanResults[0]
|
||||
if _, err := g.SetCurrentView("side"); err != nil {
|
||||
return err
|
||||
@@ -666,7 +644,7 @@ type dataForTmpl struct {
|
||||
VulnSiteLinks []string
|
||||
References []cve.Reference
|
||||
Packages []string
|
||||
CpeNames []models.CpeName
|
||||
CpeNames []string
|
||||
PublishedDate time.Time
|
||||
LastModifiedDate time.Time
|
||||
}
|
||||
@@ -780,8 +758,8 @@ Package/CPE
|
||||
{{range $pack := .Packages -}}
|
||||
* {{$pack}}
|
||||
{{end -}}
|
||||
{{range .CpeNames -}}
|
||||
* {{.Name}}
|
||||
{{range $name := .CpeNames -}}
|
||||
* {{$name}}
|
||||
{{end}}
|
||||
Links
|
||||
--------------
|
||||
|
||||
214
report/util.go
214
report/util.go
@@ -20,89 +20,49 @@ package report
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/gosuri/uitable"
|
||||
)
|
||||
|
||||
func ensureResultDir(scannedAt time.Time) (path string, err error) {
|
||||
jsonDirName := scannedAt.Format(time.RFC3339)
|
||||
const maxColWidth = 80
|
||||
|
||||
resultsDir := config.Conf.ResultsDir
|
||||
if len(resultsDir) == 0 {
|
||||
wd, _ := os.Getwd()
|
||||
resultsDir = filepath.Join(wd, "results")
|
||||
}
|
||||
jsonDir := filepath.Join(resultsDir, jsonDirName)
|
||||
|
||||
if err := os.MkdirAll(jsonDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("Failed to create dir: %s", err)
|
||||
}
|
||||
|
||||
symlinkPath := filepath.Join(resultsDir, "current")
|
||||
if _, err := os.Lstat(symlinkPath); err == nil {
|
||||
if err := os.Remove(symlinkPath); err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"Failed to remove symlink. path: %s, err: %s", symlinkPath, err)
|
||||
func toScanSummary(rs ...models.ScanResult) string {
|
||||
table := uitable.New()
|
||||
table.MaxColWidth = maxColWidth
|
||||
table.Wrap = true
|
||||
for _, r := range rs {
|
||||
cols := []interface{}{
|
||||
r.ServerName,
|
||||
fmt.Sprintf("%s%s", r.Family, r.Release),
|
||||
fmt.Sprintf("%d CVEs", len(r.ScannedCves)),
|
||||
r.Packages.ToUpdatablePacksSummary(),
|
||||
}
|
||||
table.AddRow(cols...)
|
||||
}
|
||||
|
||||
if err := os.Symlink(jsonDir, symlinkPath); err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"Failed to create symlink: path: %s, err: %s", symlinkPath, err)
|
||||
}
|
||||
return jsonDir, nil
|
||||
return fmt.Sprintf("%s\n", table)
|
||||
}
|
||||
|
||||
func toPlainText(scanResult models.ScanResult) (string, error) {
|
||||
serverInfo := scanResult.ServerInfo()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
for i := 0; i < len(serverInfo); i++ {
|
||||
buffer.WriteString("=")
|
||||
func toOneLineSummary(rs ...models.ScanResult) string {
|
||||
table := uitable.New()
|
||||
table.MaxColWidth = maxColWidth
|
||||
table.Wrap = true
|
||||
for _, r := range rs {
|
||||
cols := []interface{}{
|
||||
r.ServerName,
|
||||
r.CveSummary(),
|
||||
r.Packages.ToUpdatablePacksSummary(),
|
||||
}
|
||||
table.AddRow(cols...)
|
||||
}
|
||||
header := fmt.Sprintf("%s\n%s", serverInfo, buffer.String())
|
||||
|
||||
if len(scanResult.KnownCves) == 0 && len(scanResult.UnknownCves) == 0 {
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
No unsecure packages.
|
||||
`, header), nil
|
||||
}
|
||||
|
||||
summary := ToPlainTextSummary(scanResult)
|
||||
scoredReport, unscoredReport := []string{}, []string{}
|
||||
scoredReport, unscoredReport = toPlainTextDetails(scanResult, scanResult.Family)
|
||||
|
||||
scored := strings.Join(scoredReport, "\n\n")
|
||||
|
||||
unscored := ""
|
||||
if !config.Conf.IgnoreUnscoredCves {
|
||||
unscored = strings.Join(unscoredReport, "\n\n")
|
||||
}
|
||||
|
||||
detail := fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
%s
|
||||
`,
|
||||
scored,
|
||||
unscored,
|
||||
)
|
||||
text := fmt.Sprintf("%s\n%s\n%s\n", header, summary, detail)
|
||||
|
||||
return text, nil
|
||||
return fmt.Sprintf("%s\n", table)
|
||||
}
|
||||
|
||||
// ToPlainTextSummary format summary for plain text.
|
||||
func ToPlainTextSummary(r models.ScanResult) string {
|
||||
func toShortPlainText(r models.ScanResult) string {
|
||||
stable := uitable.New()
|
||||
stable.MaxColWidth = 84
|
||||
stable.MaxColWidth = maxColWidth
|
||||
stable.Wrap = true
|
||||
|
||||
cves := r.KnownCves
|
||||
@@ -110,14 +70,45 @@ func ToPlainTextSummary(r models.ScanResult) string {
|
||||
cves = append(cves, r.UnknownCves...)
|
||||
}
|
||||
|
||||
for _, d := range cves {
|
||||
var scols []string
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < len(r.ServerInfo()); i++ {
|
||||
buf.WriteString("=")
|
||||
}
|
||||
header := fmt.Sprintf("%s\n%s\n%s\t%s\n\n",
|
||||
r.ServerInfo(),
|
||||
buf.String(),
|
||||
r.CveSummary(),
|
||||
r.Packages.ToUpdatablePacksSummary(),
|
||||
)
|
||||
|
||||
if len(cves) == 0 {
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
No CVE-IDs are found in updatable packages.
|
||||
%s
|
||||
`, header, r.Packages.ToUpdatablePacksSummary())
|
||||
}
|
||||
|
||||
for _, d := range cves {
|
||||
var packsVer string
|
||||
for _, p := range d.Packages {
|
||||
packsVer += fmt.Sprintf(
|
||||
"%s -> %s\n", p.ToStringCurrentVersion(), p.ToStringNewVersion())
|
||||
}
|
||||
for _, n := range d.CpeNames {
|
||||
packsVer += n
|
||||
}
|
||||
|
||||
var scols []string
|
||||
switch {
|
||||
case config.Conf.Lang == "ja" &&
|
||||
0 < d.CveDetail.Jvn.CvssScore():
|
||||
|
||||
summary := d.CveDetail.Jvn.CveTitle()
|
||||
summary := fmt.Sprintf("%s\n%s\n%s\n%s",
|
||||
d.CveDetail.Jvn.CveTitle(),
|
||||
d.CveDetail.Jvn.Link(),
|
||||
distroLinks(d, r.Family)[0].url,
|
||||
packsVer,
|
||||
)
|
||||
scols = []string{
|
||||
d.CveDetail.CveID,
|
||||
fmt.Sprintf("%-4.1f (%s)",
|
||||
@@ -126,8 +117,15 @@ func ToPlainTextSummary(r models.ScanResult) string {
|
||||
),
|
||||
summary,
|
||||
}
|
||||
|
||||
case 0 < d.CveDetail.CvssScore("en"):
|
||||
summary := d.CveDetail.Nvd.CveSummary()
|
||||
summary := fmt.Sprintf("%s\n%s/%s\n%s\n%s",
|
||||
d.CveDetail.Nvd.CveSummary(),
|
||||
cveDetailsBaseURL,
|
||||
d.CveDetail.CveID,
|
||||
distroLinks(d, r.Family)[0].url,
|
||||
packsVer,
|
||||
)
|
||||
scols = []string{
|
||||
d.CveDetail.CveID,
|
||||
fmt.Sprintf("%-4.1f (%s)",
|
||||
@@ -137,10 +135,12 @@ func ToPlainTextSummary(r models.ScanResult) string {
|
||||
summary,
|
||||
}
|
||||
default:
|
||||
summary := fmt.Sprintf("%s\n%s",
|
||||
distroLinks(d, r.Family)[0].url, packsVer)
|
||||
scols = []string{
|
||||
d.CveDetail.CveID,
|
||||
"?",
|
||||
d.CveDetail.Nvd.CveSummary(),
|
||||
summary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +149,55 @@ func ToPlainTextSummary(r models.ScanResult) string {
|
||||
cols[i] = scols[i]
|
||||
}
|
||||
stable.AddRow(cols...)
|
||||
stable.AddRow("")
|
||||
}
|
||||
return fmt.Sprintf("%s", stable)
|
||||
return fmt.Sprintf("%s\n%s\n", header, stable)
|
||||
}
|
||||
|
||||
func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) {
|
||||
for _, cve := range data.KnownCves {
|
||||
func toFullPlainText(r models.ScanResult) string {
|
||||
serverInfo := r.ServerInfo()
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < len(serverInfo); i++ {
|
||||
buf.WriteString("=")
|
||||
}
|
||||
header := fmt.Sprintf("%s\n%s\n%s\t%s\n",
|
||||
r.ServerInfo(),
|
||||
buf.String(),
|
||||
r.CveSummary(),
|
||||
r.Packages.ToUpdatablePacksSummary(),
|
||||
)
|
||||
|
||||
if len(r.KnownCves) == 0 && len(r.UnknownCves) == 0 {
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
No CVE-IDs are found in updatable packages.
|
||||
%s
|
||||
`, header, r.Packages.ToUpdatablePacksSummary())
|
||||
}
|
||||
|
||||
scoredReport, unscoredReport := []string{}, []string{}
|
||||
scoredReport, unscoredReport = toPlainTextDetails(r, r.Family)
|
||||
|
||||
unscored := ""
|
||||
if !config.Conf.IgnoreUnscoredCves {
|
||||
unscored = strings.Join(unscoredReport, "\n\n")
|
||||
}
|
||||
|
||||
scored := strings.Join(scoredReport, "\n\n")
|
||||
detail := fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
%s
|
||||
`,
|
||||
scored,
|
||||
unscored,
|
||||
)
|
||||
return fmt.Sprintf("%s\n%s\n", header, detail)
|
||||
}
|
||||
|
||||
func toPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) {
|
||||
for _, cve := range r.KnownCves {
|
||||
switch config.Conf.Lang {
|
||||
case "en":
|
||||
if 0 < cve.CveDetail.Nvd.CvssScore() {
|
||||
@@ -177,7 +220,7 @@ func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport,
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, cve := range data.UnknownCves {
|
||||
for _, cve := range r.UnknownCves {
|
||||
unscoredReport = append(
|
||||
unscoredReport, toPlainTextUnknownCve(cve, osFamily))
|
||||
}
|
||||
@@ -187,7 +230,7 @@ func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport,
|
||||
func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string {
|
||||
cveID := cveInfo.CveDetail.CveID
|
||||
dtable := uitable.New()
|
||||
dtable.MaxColWidth = 100
|
||||
dtable.MaxColWidth = maxColWidth
|
||||
dtable.Wrap = true
|
||||
dtable.AddRow(cveID)
|
||||
dtable.AddRow("-------------")
|
||||
@@ -201,6 +244,8 @@ func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string {
|
||||
for _, link := range dlinks {
|
||||
dtable.AddRow(link.title, link.url)
|
||||
}
|
||||
dtable = addPackageInfos(dtable, cveInfo.Packages)
|
||||
dtable = addCpeNames(dtable, cveInfo.CpeNames)
|
||||
|
||||
return fmt.Sprintf("%s", dtable)
|
||||
}
|
||||
@@ -211,7 +256,7 @@ func toPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string {
|
||||
jvn := cveDetail.Jvn
|
||||
|
||||
dtable := uitable.New()
|
||||
dtable.MaxColWidth = 100
|
||||
dtable.MaxColWidth = maxColWidth
|
||||
dtable.Wrap = true
|
||||
dtable.AddRow(cveID)
|
||||
dtable.AddRow("-------------")
|
||||
@@ -253,7 +298,7 @@ func toPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string {
|
||||
nvd := cveDetail.Nvd
|
||||
|
||||
dtable := uitable.New()
|
||||
dtable.MaxColWidth = 100
|
||||
dtable.MaxColWidth = maxColWidth
|
||||
dtable.Wrap = true
|
||||
dtable.AddRow(cveID)
|
||||
dtable.AddRow("-------------")
|
||||
@@ -357,13 +402,12 @@ func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO
|
||||
// addPackageInfos add package information related the CVE to table
|
||||
func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table {
|
||||
for i, p := range packs {
|
||||
var title string
|
||||
if i == 0 {
|
||||
title = "Package/CPE"
|
||||
title = "Package"
|
||||
}
|
||||
ver := fmt.Sprintf(
|
||||
"%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion())
|
||||
@@ -372,9 +416,9 @@ func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.
|
||||
return table
|
||||
}
|
||||
|
||||
func addCpeNames(table *uitable.Table, names []models.CpeName) *uitable.Table {
|
||||
for _, p := range names {
|
||||
table.AddRow("CPE", fmt.Sprintf("%s", p.Name))
|
||||
func addCpeNames(table *uitable.Table, names []string) *uitable.Table {
|
||||
for _, n := range names {
|
||||
table.AddRow("CPE", fmt.Sprintf("%s", n))
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
@@ -17,7 +17,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package report
|
||||
|
||||
import "github.com/future-architect/vuls/models"
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
const (
|
||||
nvdBaseURL = "https://web.nvd.nist.gov/view/vuln/detail"
|
||||
@@ -33,9 +38,27 @@ const (
|
||||
debianTrackerBaseURL = "https://security-tracker.debian.org/tracker"
|
||||
|
||||
freeBSDVuXMLBaseURL = "https://vuxml.freebsd.org/freebsd/%s.html"
|
||||
|
||||
vulsOpenTag = "<vulsreport>"
|
||||
vulsCloseTag = "</vulsreport>"
|
||||
)
|
||||
|
||||
// ResultWriter Interface
|
||||
type ResultWriter interface {
|
||||
Write([]models.ScanResult) error
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
const (
|
||||
vulsOpenTag = "<vulsreport>"
|
||||
vulsCloseTag = "</vulsreport>"
|
||||
)
|
||||
|
||||
// XMLWriter writes results to file.
|
||||
type XMLWriter struct {
|
||||
ScannedAt time.Time
|
||||
}
|
||||
|
||||
func (w XMLWriter) Write(scanResults []models.ScanResult) (err error) {
|
||||
var path string
|
||||
if path, err = ensureResultDir(w.ScannedAt); err != nil {
|
||||
return fmt.Errorf("Failed to make direcotory/symlink : %s", err)
|
||||
}
|
||||
|
||||
for _, scanResult := range scanResults {
|
||||
scanResult.ScannedAt = w.ScannedAt
|
||||
}
|
||||
|
||||
var xmlBytes []byte
|
||||
for _, r := range scanResults {
|
||||
xmlPath := ""
|
||||
if len(r.Container.ContainerID) == 0 {
|
||||
xmlPath = filepath.Join(path, fmt.Sprintf("%s.xml", r.ServerName))
|
||||
} else {
|
||||
xmlPath = filepath.Join(path,
|
||||
fmt.Sprintf("%s_%s.xml", r.ServerName, r.Container.Name))
|
||||
}
|
||||
|
||||
if xmlBytes, err = xml.Marshal(r); err != nil {
|
||||
return fmt.Errorf("Failed to Marshal to XML: %s", err)
|
||||
}
|
||||
|
||||
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), xmlBytes, []byte(vulsCloseTag)}, []byte{})
|
||||
if err := ioutil.WriteFile(xmlPath, allBytes, 0600); err != nil {
|
||||
return fmt.Errorf("Failed to write XML. path: %s, err: %s", xmlPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user