Add report subcommand, change scan options. Bump up ver #239

This commit is contained in:
Kota Kanbe
2016-11-17 14:24:31 +09:00
parent cb29289167
commit 155cadf901
43 changed files with 2761 additions and 1979 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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