Compare commits

..

11 Commits

Author SHA1 Message Date
knqyf263
53ad8c2f35 Output description and references to JSON 2019-05-08 15:25:35 +09:00
knqyf263
34ba0ca8d7 Use description as title 2019-05-08 15:12:34 +09:00
knqyf263
6d82700032 Add --ignore-unfixed option and sort vulnerabilities by a severity 2019-05-08 13:31:51 +09:00
knqyf263
a0a991ca16 Update README 2019-05-08 07:47:47 +09:00
knqyf263
989b893bf2 Add Dockerfile 2019-05-07 23:07:21 +09:00
knqyf263
d270edea75 Support --quiet option 2019-05-07 23:06:57 +09:00
knqyf263
7aa407099c Support --exit-code 2019-05-07 22:32:27 +09:00
knqyf263
abeeb37e75 Clear cache 2019-05-07 21:34:00 +09:00
Teppei Fukuda
1291b9a194 Display advisory information (#2)
* Use fork tablewriter

* Display advisory information

* Update README
2019-05-07 20:46:11 +09:00
knqyf263
4a0b8c5e49 Update CI config 2019-05-07 18:24:48 +09:00
knqyf263
4bfa1fb821 Update README 2019-05-07 18:24:46 +09:00
30 changed files with 579 additions and 125 deletions

View File

@@ -2,9 +2,16 @@ defaults: &defaults
docker :
- image: knqyf263/ci-trivy:latest
environment:
CGO_ENABLED: "1"
CGO_ENABLED: "0"
jobs:
test:
<<: *defaults
steps:
- checkout
- run:
name: Test
command: go test ./...
release:
<<: *defaults
steps:
@@ -34,6 +41,7 @@ workflows:
version: 2
release:
jobs:
- test
- release:
filters:
branches:

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
imgs

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM golang:1.12-alpine AS builder
ADD go.mod go.sum /app/
WORKDIR /app/
RUN apk --no-cache add git
RUN go mod download
ADD . /app/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o /trivy cmd/trivy/main.go
FROM alpine:3.9
RUN apk --no-cache add ca-certificates git
COPY --from=builder /trivy /usr/local/bin/trivy
RUN chmod +x /usr/local/bin/trivy
CMD ["trivy"]

View File

@@ -1,10 +1,12 @@
# trivy
<img src="imgs/logo.png" width="300">
[![GitHub release](https://img.shields.io/github/release/knqyf263/trivy.svg)](https://github.com/knqyf263/trivy/releases/latest)
[![CircleCI](https://circleci.com/gh/knqyf263/trivy.svg?style=svg)](https://circleci.com/gh/knqyf263/trivy)
[![Go Report Card](https://goreportcard.com/badge/github.com/knqyf263/trivy)](https://goreportcard.com/report/github.com/knqyf263/trivy)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/knqyf263/trivy/blob/master/LICENSE)
A Simple and Comprehensive Vulnerability Scanner for Containers
# Abstract
Scan containers
@@ -27,6 +29,12 @@ $ sudo yum -y update
$ sudo yum -y install trivy
```
or
```
$ rpm -ivh https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.rpm
```
## Debian/Ubuntu
Replace `[CODE_NAME]` with your code name
@@ -41,6 +49,14 @@ $ sudo apt-get update
$ sudo apt-get install trivy
```
or
```
$ sudo apt-get install rpm
$ wget https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.deb
$ sudo dpkg -i trivy_0.0.3_Linux-64bit.deb
```
## Mac OS X / Homebrew
You can use homebrew on OS X.
```
@@ -58,6 +74,69 @@ $ go get -u github.com/knqyf263/trivy
```
# Examples
## Continuous Integration (CI)
Scan your image built in Travis CI/CircleCI. The test will fail if a vulnerability is found. When you don't want to fail the test, specify `--exit-code 0` .
**Note**: The first time take a while (faster by cache after the second time)
### Travis CI
```
$ cat .travis.yml
services:
- docker
before_install:
- docker build -t trivy-ci-test:latest .
- wget https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.tar.gz
- tar zxvf trivy_0.0.3_Linux-64bit.tar.gz
script:
- ./trivy --exit-code 1 --quiet trivy-ci-test:latest
cache:
directories:
- $HOME/.cache/trivy
```
example: https://travis-ci.org/knqyf263/trivy-ci-test
repository: https://github.com/knqyf263/trivy-ci-test
### Circle CI
```
$ cat .circleci/config.yml
jobs:
build:
docker:
- image: docker:18.09-git
steps:
- checkout
- setup_remote_docker
- restore_cache:
key: vulnerability-db
- run:
name: Build image
command: docker build -t trivy-ci-test:latest .
- run:
name: Install trivy
command: |
wget https://github.com/knqyf263/trivy/releases/download/v0.0.4/trivy_0.0.4_Linux-64bit.tar.gz
tar zxvf trivy_0.0.4_Linux-64bit.tar.gz
mv trivy /usr/local/bin
- run:
name: Scan the local image with trivy
command: trivy --exit-code 1 --quiet trivy-ci-test:latest
- save_cache:
key: vulnerability-db
paths:
- $HOME/.cache/trivy
workflows:
version: 2
release:
jobs:
- build
```
example: https://circleci.com/gh/knqyf263/trivy-ci-test
repository: https://github.com/knqyf263/trivy-ci-test
# Usage
@@ -68,17 +147,20 @@ NAME:
USAGE:
main [options] image_name
VERSION:
0.0.1
0.0.3
OPTIONS:
--format value, -f value format (table, json) (default: "table")
--input value, -i value input file path instead of image name
--severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN")
--severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL")
--output value, -o value output file name
--exit-code value Exit code when vulnerabilities were found (default: 0)
--skip-update skip db update
--clean, -c clean all cache
--quiet, -q suppress progress bar
--debug, -d debug mode
--help, -h show help
--version, -v print the version
```
# Q&A
@@ -137,6 +219,9 @@ $ trivy --clean alpine:3.8
----
# Credits
Special thanks to [Tomoya Amachi](https://github.com/tomoyamachi)
# License
MIT

View File

@@ -1,6 +1,7 @@
package main
import (
l "log"
"os"
"strings"
@@ -59,6 +60,11 @@ OPTIONS:
Name: "output, o",
Usage: "output file name",
},
cli.IntFlag{
Name: "exit-code",
Usage: "Exit code when vulnerabilities were found",
Value: 0,
},
cli.BoolFlag{
Name: "skip-update",
Usage: "skip db update",
@@ -67,6 +73,14 @@ OPTIONS:
Name: "clean, c",
Usage: "clean all cache",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "suppress progress bar",
},
cli.BoolFlag{
Name: "ignore-unfixed",
Usage: "display only fixed vulnerabilities",
},
cli.BoolFlag{
Name: "debug, d",
Usage: "debug mode",
@@ -79,6 +93,9 @@ OPTIONS:
err := app.Run(os.Args)
if err != nil {
log.Logger.Fatal(err)
if log.Logger != nil {
log.Logger.Fatal(err)
}
l.Fatal(err)
}
}

4
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/fatih/color v1.7.0
github.com/gliderlabs/ssh v0.1.3 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/knqyf263/fanal v0.0.0-20190506110705-2b5cb3000ff6
github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70
github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b
github.com/knqyf263/go-dep-parser v0.0.0-20190429154931-c377a5391790
github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936
@@ -37,3 +37,5 @@ require (
)
replace github.com/genuinetools/reg => github.com/tomoyamachi/reg v0.16.2-0.20190418055600-c6010b917a55
replace github.com/olekukonko/tablewriter => github.com/knqyf263/tablewriter v0.0.2

4
go.sum
View File

@@ -117,6 +117,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/knqyf263/berkeleydb v0.0.0-20190501065933-fafe01fb9662/go.mod h1:bu1CcN4tUtoRcI/B/RFHhxMNKFHVq/c3SV+UTyduoXg=
github.com/knqyf263/fanal v0.0.0-20190506110705-2b5cb3000ff6 h1:iSztZNfwEPMN2CvUX1SxNEclRZn+rwRMdsnAegxRJk4=
github.com/knqyf263/fanal v0.0.0-20190506110705-2b5cb3000ff6/go.mod h1:OiuWIClssf5WzbMcR8lfspdBVaP+vRQndY4kHeFgrDw=
github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70 h1:L27WBZxk7N70WilG91kgvs0EnV+JVCoOTsNQa8tMBJs=
github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70/go.mod h1:OiuWIClssf5WzbMcR8lfspdBVaP+vRQndY4kHeFgrDw=
github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b h1:DiDMmSwuY27PJxA2Gs0+uI/bQ/ehKARaGXRdlp+wFis=
github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao=
github.com/knqyf263/go-dep-parser v0.0.0-20190429154931-c377a5391790 h1:c02gG0yRNr25lcLOH+678SuuxxMUq36i48PQnmAweWk=
@@ -128,6 +130,8 @@ github.com/knqyf263/go-version v1.1.1 h1:+MpcBC9b7rk5ihag8Y/FLG8get1H2GjniwKQ+9D
github.com/knqyf263/go-version v1.1.1/go.mod h1:0tBvHvOBSf5TqGNcY+/ih9o8qo3R16iZCpB9rP0D3VM=
github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc=
github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk=
github.com/knqyf263/tablewriter v0.0.2 h1:lGaBruL/oJt8FAlMVy9KU0oQ/6NXAJjvK7wBgZyc+Og=
github.com/knqyf263/tablewriter v0.0.2/go.mod h1:NDOJQAZxabBL3e13jQVktkvbr6bxXXPon8Lyh7fRPPc=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=

BIN
imgs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -4,9 +4,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/knqyf263/trivy/pkg/log"
"github.com/knqyf263/trivy/pkg/utils"
"golang.org/x/xerrors"
@@ -33,10 +31,13 @@ func CloneOrPull(url, repoPath string) (map[string]struct{}, error) {
updatedFiles[strings.TrimSpace(filename)] = struct{}{}
}
} else {
if !utils.IsCommandAvailable("git") {
log.Logger.Warn("Recommend installing git (if not, DB update is very slow)")
}
log.Logger.Debug("remove an existed directory")
s := spinner.New(spinner.CharSets[36], 100*time.Millisecond)
s.Suffix = " The first time will take a while..."
suffix := " The first time will take a while..."
s := utils.NewSpinner(suffix)
s.Start()
defer s.Stop()
@@ -74,7 +75,6 @@ func clone(url, repoPath string) error {
if utils.IsCommandAvailable("git") {
return cloneByOSCommand(url, repoPath)
}
log.Logger.Warn("Recommend installing git (if not, DB update is very slow)")
_, err := git.PlainClone(repoPath, false, &git.CloneOptions{
URL: url,

View File

@@ -42,8 +42,17 @@ func (tw TableWriter) write(result Result) {
severityCount := map[string]int{}
for _, v := range result.Vulnerabilities {
severityCount[v.Severity]++
title := v.Title
if title == "" {
title = v.Description
}
splittedTitle := strings.Split(title, " ")
if len(splittedTitle) >= 12 {
title = strings.Join(splittedTitle[:12], " ") + "..."
}
table.Append([]string{v.PkgName, v.VulnerabilityID, vulnerability.ColorizeSeverity(v.Severity),
v.InstalledVersion, v.FixedVersion, v.Title})
v.InstalledVersion, v.FixedVersion, title})
}
var results []string

View File

@@ -5,6 +5,8 @@ import (
"os"
"strings"
"github.com/knqyf263/fanal/cache"
"github.com/knqyf263/trivy/pkg/utils"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
@@ -27,6 +29,18 @@ func Run(c *cli.Context) (err error) {
}
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
clean := c.Bool("clean")
if clean {
log.Logger.Info("Cleaning caches...")
if err = cache.Clear(); err != nil {
return xerrors.New("failed to remove image layer cache")
}
if err = os.RemoveAll(utils.CacheDir()); err != nil {
return xerrors.New("failed to remove cache")
}
return nil
}
args := c.Args()
filePath := c.String("input")
if filePath == "" && len(args) == 0 {
@@ -34,13 +48,8 @@ func Run(c *cli.Context) (err error) {
cli.ShowAppHelpAndExit(c, 1)
}
clean := c.Bool("clean")
if clean {
log.Logger.Info("Cleaning caches...")
if err = os.RemoveAll(utils.CacheDir()); err != nil {
return xerrors.New("failed to remove cache")
}
}
utils.Quiet = c.Bool("quiet")
o := c.String("output")
output := os.Stdout
if o != "" {
@@ -53,7 +62,8 @@ func Run(c *cli.Context) (err error) {
for _, s := range strings.Split(c.String("severity"), ",") {
severity, err := vulnerability.NewSeverity(s)
if err != nil {
return xerrors.Errorf("error in severity option: %w", err)
log.Logger.Infof("error in severity option: %s", err)
cli.ShowAppHelpAndExit(c, 1)
}
severities = append(severities, severity)
}
@@ -68,11 +78,13 @@ func Run(c *cli.Context) (err error) {
}
}
ignoreUnfixed := c.Bool("ignore-unfixed")
var imageName string
if filePath == "" {
imageName = args[0]
}
results, err := scanner.ScanImage(imageName, filePath, severities)
results, err := scanner.ScanImage(imageName, filePath, severities, ignoreUnfixed)
if err != nil {
return xerrors.Errorf("error in image scan: %w", err)
}
@@ -88,7 +100,13 @@ func Run(c *cli.Context) (err error) {
}
if err = writer.Write(results); err != nil {
return err
return xerrors.Errorf("failed to write results: %w", err)
}
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(c.Int("exit-code"))
}
}
return nil

View File

@@ -1,10 +1,16 @@
package bundler
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/etcd-io/bbolt"
"github.com/knqyf263/trivy/pkg/db"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/git"
@@ -28,6 +34,9 @@ type Advisory struct {
Osvdb string
Title string
Url string
Description string
CvssV2 float64 `yaml:"cvss_v2"`
CvssV3 float64 `yaml:"cvss_v3"`
PatchedVersions []string `yaml:"patched_versions"`
UnaffectedVersions []string `yaml:"unaffected_versions"`
Related Related
@@ -42,14 +51,15 @@ func (s *Scanner) UpdateDB() (err error) {
if _, err := git.CloneOrPull(dbURL, repoPath); err != nil {
return xerrors.Errorf("error in %s security DB update: %w", s.Type(), err)
}
s.db, err = walk()
s.db, err = s.walk()
return err
}
func walk() (AdvisoryDB, error) {
func (s *Scanner) walk() (AdvisoryDB, error) {
advisoryDB := AdvisoryDB{}
root := filepath.Join(repoPath, "gems")
var vulns []vulnerability.Vulnerability
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
@@ -65,16 +75,48 @@ func walk() (AdvisoryDB, error) {
return xerrors.Errorf("failed to unmarshal YAML: %w", err)
}
// for detecting vulnerabilities
advisories, ok := advisoryDB[advisory.Gem]
if !ok {
advisories = []Advisory{}
}
advisoryDB[advisory.Gem] = append(advisories, advisory)
// for displaying vulnerability detail
var vulnerabilityID string
if advisory.Cve != "" {
vulnerabilityID = fmt.Sprintf("CVE-%s", advisory.Cve)
} else if advisory.Osvdb != "" {
vulnerabilityID = fmt.Sprintf("OSVDB-%s", advisory.Osvdb)
}
vulns = append(vulns, vulnerability.Vulnerability{
ID: vulnerabilityID,
CvssScore: advisory.CvssV2,
CvssScoreV3: advisory.CvssV3,
References: append([]string{advisory.Url}, advisory.Related.Url...),
Title: advisory.Title,
Description: advisory.Description,
})
return nil
})
if err != nil {
return nil, xerrors.Errorf("error in file wakl: %w", err)
return nil, xerrors.Errorf("error in file walk: %w", err)
}
if err = s.saveVulnerabilities(vulns); err != nil {
return nil, err
}
return advisoryDB, nil
}
func (s *Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error {
return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error {
for _, vuln := range vulns {
if err := db.Put(b, vuln.ID, vulnerability.RubySec, vuln); err != nil {
return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err)
}
}
return nil
})
}

View File

@@ -6,7 +6,13 @@ import (
"path/filepath"
"strings"
"github.com/etcd-io/bbolt"
"github.com/knqyf263/trivy/pkg/db"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/utils"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"github.com/knqyf263/trivy/pkg/git"
"gopkg.in/yaml.v2"
@@ -38,12 +44,13 @@ func (s *Scanner) UpdateDB() (err error) {
if _, err := git.CloneOrPull(dbURL, repoPath); err != nil {
return err
}
s.db, err = walk()
s.db, err = s.walk()
return err
}
func walk() (AdvisoryDB, error) {
func (s *Scanner) walk() (AdvisoryDB, error) {
advisoryDB := AdvisoryDB{}
var vulns []vulnerability.Vulnerability
err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() || !strings.HasPrefix(info.Name(), "CVE-") {
return nil
@@ -58,16 +65,39 @@ func walk() (AdvisoryDB, error) {
if err != nil {
return err
}
// for detecting vulnerabilities
advisories, ok := advisoryDB[advisory.Reference]
if !ok {
advisories = []Advisory{}
}
advisoryDB[advisory.Reference] = append(advisories, advisory)
// for displaying vulnerability detail
vulns = append(vulns, vulnerability.Vulnerability{
ID: advisory.Cve,
References: []string{advisory.Link},
Title: advisory.Title,
})
return nil
})
if err != nil {
return nil, xerrors.Errorf("error in file walk: %w", err)
}
if err = s.saveVulnerabilities(vulns); err != nil {
return nil, err
}
return advisoryDB, nil
}
func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error {
return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error {
for _, vuln := range vulns {
if err := db.Put(b, vuln.ID, vulnerability.PhpSecurityAdvisories, vuln); err != nil {
return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err)
}
}
return nil
})
}

View File

@@ -2,12 +2,19 @@ package npm
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/etcd-io/bbolt"
"github.com/knqyf263/trivy/pkg/db"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/utils"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"github.com/knqyf263/trivy/pkg/git"
)
@@ -29,6 +36,7 @@ type Advisory struct {
Cves []string
VulnerableVersions string `json:"vulnerable_versions"`
PatchedVersions string `json:"patched_versions"`
Overview string
Recommendation string
References []string
CvssScoreNumber json.Number `json:"cvss_score"`
@@ -39,12 +47,13 @@ func (s *Scanner) UpdateDB() (err error) {
if _, err := git.CloneOrPull(dbURL, repoPath); err != nil {
return err
}
s.db, err = walk()
s.db, err = s.walk()
return err
}
func walk() (AdvisoryDB, error) {
func (s *Scanner) walk() (AdvisoryDB, error) {
advisoryDB := AdvisoryDB{}
var vulns []vulnerability.Vulnerability
err := filepath.Walk(filepath.Join(repoPath, "vuln"), func(path string, info os.FileInfo, err error) error {
if info.IsDir() || !strings.HasSuffix(info.Name(), ".json") {
return nil
@@ -68,16 +77,46 @@ func walk() (AdvisoryDB, error) {
advisory.CvssScore = -1
}
// for detecting vulnerabilities
advisories, ok := advisoryDB[advisory.ModuleName]
if !ok {
advisories = []Advisory{}
}
advisoryDB[advisory.ModuleName] = append(advisories, advisory)
// for displaying vulnerability detail
vulnerabilityIDs := advisory.Cves
if len(vulnerabilityIDs) == 0 {
vulnerabilityIDs = []string{fmt.Sprintf("NSWG-ECO-%d", advisory.ID)}
}
for _, vulnID := range vulnerabilityIDs {
vulns = append(vulns, vulnerability.Vulnerability{
ID: vulnID,
CvssScore: advisory.CvssScore,
References: advisory.References,
Title: advisory.Title,
Description: advisory.Overview,
})
}
return nil
})
if err != nil {
return nil, xerrors.Errorf("error in file walk: %w", err)
}
if err = s.saveVulnerabilities(vulns); err != nil {
return nil, err
}
return advisoryDB, nil
}
func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error {
return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error {
for _, vuln := range vulns {
if err := db.Put(b, vuln.ID, vulnerability.NodejsSecurityWg, vuln); err != nil {
return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err)
}
}
return nil
})
}

View File

@@ -5,9 +5,14 @@ import (
"os"
"path/filepath"
"github.com/etcd-io/bbolt"
"github.com/knqyf263/trivy/pkg/db"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/utils"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"github.com/knqyf263/trivy/pkg/git"
)
@@ -34,14 +39,14 @@ func (s *Scanner) UpdateDB() (err error) {
if _, err := git.CloneOrPull(dbURL, repoPath); err != nil {
return err
}
s.db, err = parse()
s.db, err = s.parse()
if err != nil {
return xerrors.Errorf("failed to parse python safety-db: %w", err)
}
return nil
}
func parse() (AdvisoryDB, error) {
func (s *Scanner) parse() (AdvisoryDB, error) {
advisoryDB := AdvisoryDB{}
f, err := os.Open(filepath.Join(repoPath, "data", "insecure_full.json"))
if err != nil {
@@ -49,9 +54,39 @@ func parse() (AdvisoryDB, error) {
}
defer f.Close()
// for detecting vulnerabilities
if err = json.NewDecoder(f).Decode(&advisoryDB); err != nil {
return nil, err
}
// for displaying vulnerability detail
var vulns []vulnerability.Vulnerability
for _, advisories := range advisoryDB {
for _, advisory := range advisories {
vulnerabilityID := advisory.Cve
if vulnerabilityID == "" {
vulnerabilityID = advisory.ID
}
vulns = append(vulns, vulnerability.Vulnerability{
ID: vulnerabilityID,
Title: advisory.Advisory,
})
}
}
if err = s.saveVulnerabilities(vulns); err != nil {
return nil, err
}
return advisoryDB, nil
}
func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error {
return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error {
for _, vuln := range vulns {
if err := db.Put(b, vuln.ID, vulnerability.PythonSafetyDB, vuln); err != nil {
return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err)
}
}
return nil
})
}

View File

@@ -96,7 +96,7 @@ func scan(scanner Scanner, pkgs []ptypes.Library) ([]types.Vulnerability, error)
return nil, xerrors.Errorf("failed to update %s advisories: %w", scanner.Type(), err)
}
log.Logger.Infof("Detect %s vulnerabilities", scanner.Type())
log.Logger.Infof("Detecting %s vulnerabilities...", scanner.Type())
var vulnerabilities []types.Vulnerability
for _, pkg := range pkgs {
v, err := version.NewVersion(pkg.Version)

View File

@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"os"
"sort"
"github.com/knqyf263/trivy/pkg/log"
@@ -25,7 +26,13 @@ import (
"github.com/knqyf263/fanal/extractor"
)
func ScanImage(imageName, filePath string, severities []vulnerability.Severity) (report.Results, error) {
var (
sources = []string{vulnerability.Nvd, vulnerability.RedHat, vulnerability.Debian,
vulnerability.DebianOVAL, vulnerability.Alpine, vulnerability.RubySec, vulnerability.PhpSecurityAdvisories,
vulnerability.NodejsSecurityWg, vulnerability.PythonSafetyDB}
)
func ScanImage(imageName, filePath string, severities []vulnerability.Severity, ignoreUnfixed bool) (report.Results, error) {
var results report.Results
var err error
ctx := context.Background()
@@ -61,7 +68,7 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity)
results = append(results, report.Result{
FileName: fmt.Sprintf("%s (%s %s)", target, osFamily, osVersion),
Vulnerabilities: processVulnerabilties(osVulns, severities),
Vulnerabilities: processVulnerabilties(osVulns, severities, ignoreUnfixed),
})
libVulns, err := library.Scan(files)
@@ -71,7 +78,7 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity)
for path, vulns := range libVulns {
results = append(results, report.Result{
FileName: path,
Vulnerabilities: processVulnerabilties(vulns, severities),
Vulnerabilities: processVulnerabilties(vulns, severities, ignoreUnfixed),
})
}
@@ -85,26 +92,39 @@ func ScanFile(f *os.File, severities []vulnerability.Severity) (report.Result, e
}
result := report.Result{
FileName: f.Name(),
Vulnerabilities: processVulnerabilties(vulns, severities),
Vulnerabilities: processVulnerabilties(vulns, severities, false),
}
return result, nil
}
func processVulnerabilties(vulns []types.Vulnerability, severities []vulnerability.Severity) []types.Vulnerability {
func processVulnerabilties(vulns []types.Vulnerability, severities []vulnerability.Severity, ignoreUnfixed bool) []types.Vulnerability {
var vulnerabilities []types.Vulnerability
for _, vuln := range vulns {
sev, title := getDetail(vuln.VulnerabilityID)
sev, title, description, references := getDetail(vuln.VulnerabilityID)
// Filter vulnerabilities by severity
for _, s := range severities {
if s == sev {
vuln.Severity = fmt.Sprint(sev)
vuln.Title = title
vuln.Description = description
vuln.References = references
// Ignore unfixed vulnerabilities
if ignoreUnfixed && vuln.FixedVersion == "" {
continue
}
vulnerabilities = append(vulnerabilities, vuln)
break
}
}
}
sort.Slice(vulnerabilities, func(i, j int) bool {
if vulnerabilities[i].PkgName != vulnerabilities[j].PkgName {
return vulnerabilities[i].PkgName < vulnerabilities[j].PkgName
}
return vulnerability.CompareSeverityString(vulnerabilities[j].Severity, vulnerabilities[i].Severity)
})
return vulnerabilities
}
@@ -120,22 +140,19 @@ func openStream(path string) (*os.File, error) {
return os.Open(path)
}
func getDetail(vulnID string) (vulnerability.Severity, string) {
func getDetail(vulnID string) (vulnerability.Severity, string, string, []string) {
details, err := vulnerability.Get(vulnID)
if err != nil {
log.Logger.Debug(err)
return vulnerability.SeverityUnknown, ""
return vulnerability.SeverityUnknown, "", "", nil
} else if len(details) == 0 {
return vulnerability.SeverityUnknown, ""
return vulnerability.SeverityUnknown, "", "", nil
}
severity := getSeverity(details)
title := getTitle(details)
return severity, title
return getSeverity(details), getTitle(details), getDescription(details), getReferences(details)
}
func getSeverity(details map[string]vulnerability.Vulnerability) vulnerability.Severity {
for _, source := range []string{vulnerability.Nvd, vulnerability.RedHat, vulnerability.Debian,
vulnerability.DebianOVAL, vulnerability.Alpine} {
for _, source := range sources {
d, ok := details[source]
if !ok {
continue
@@ -154,8 +171,7 @@ func getSeverity(details map[string]vulnerability.Vulnerability) vulnerability.S
}
func getTitle(details map[string]vulnerability.Vulnerability) string {
for _, source := range []string{vulnerability.Nvd, vulnerability.RedHat, vulnerability.Debian,
vulnerability.DebianOVAL, vulnerability.Alpine} {
for _, source := range sources {
d, ok := details[source]
if !ok {
continue
@@ -167,6 +183,37 @@ func getTitle(details map[string]vulnerability.Vulnerability) string {
return ""
}
func getDescription(details map[string]vulnerability.Vulnerability) string {
for _, source := range sources {
d, ok := details[source]
if !ok {
continue
}
if d.Description != "" {
return d.Description
}
}
return ""
}
func getReferences(details map[string]vulnerability.Vulnerability) []string {
references := map[string]struct{}{}
for _, source := range sources {
d, ok := details[source]
if !ok {
continue
}
for _, ref := range d.References {
references[ref] = struct{}{}
}
}
var refs []string
for ref := range references {
refs = append(refs, ref)
}
return refs
}
func scoreToSeverity(score float64) vulnerability.Severity {
if score >= 9.0 {
return vulnerability.SeverityCritical

View File

@@ -6,6 +6,8 @@ type Vulnerability struct {
InstalledVersion string
FixedVersion string
Title string
Severity string
Title string
Description string
Severity string
References []string
}

63
pkg/utils/progress.go Normal file
View File

@@ -0,0 +1,63 @@
package utils
import (
"time"
"github.com/briandowns/spinner"
pb "gopkg.in/cheggaaa/pb.v1"
)
var (
Quiet = false
)
type Spinner struct {
client *spinner.Spinner
}
func NewSpinner(suffix string) *Spinner {
if Quiet {
return &Spinner{}
}
s := spinner.New(spinner.CharSets[36], 100*time.Millisecond)
s.Suffix = suffix
return &Spinner{client: s}
}
func (s *Spinner) Start() {
if s.client == nil {
return
}
s.client.Start()
}
func (s *Spinner) Stop() {
if s.client == nil {
return
}
s.client.Stop()
}
type ProgressBar struct {
client *pb.ProgressBar
}
func PbStartNew(total int) *ProgressBar {
if Quiet {
return &ProgressBar{}
}
bar := pb.StartNew(total)
return &ProgressBar{client: bar}
}
func (p *ProgressBar) Increment() {
if p.client == nil {
return
}
p.client.Increment()
}
func (p *ProgressBar) Finish() {
if p.client == nil {
return
}
p.client.Finish()
}

View File

@@ -3,7 +3,6 @@ package alpine
import (
"encoding/json"
"fmt"
"gopkg.in/cheggaaa/pb.v1"
"io"
"path/filepath"
@@ -37,7 +36,7 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("Alpine updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var cves []AlpineCVE

View File

@@ -3,7 +3,6 @@ package debianoval
import (
"encoding/json"
"fmt"
"gopkg.in/cheggaaa/pb.v1"
"io"
"os"
"path/filepath"
@@ -39,7 +38,7 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("Debian OVAL updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var cves []DebianOVAL

View File

@@ -3,7 +3,6 @@ package debian
import (
"encoding/json"
"fmt"
"gopkg.in/cheggaaa/pb.v1"
"io"
"path/filepath"
"strings"
@@ -45,7 +44,7 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("Debian updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var cves []DebianCVE

View File

@@ -2,7 +2,6 @@ package nvd
import (
"encoding/json"
"gopkg.in/cheggaaa/pb.v1"
"io"
"path/filepath"
@@ -19,9 +18,7 @@ import (
)
const (
nvdDir = "nvd"
rootBucket = "NVD"
nestedBucket = "dummy"
nvdDir = "nvd"
)
func Update(dir string, updatedFiles map[string]struct{}) error {
@@ -35,11 +32,11 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("NVD updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var items []vulnerability.Item
var items []Item
err = utils.FileWalk(rootDir, targets, func(r io.Reader, _ string) error {
item := vulnerability.Item{}
item := Item{}
if err := json.NewDecoder(r).Decode(&item); err != nil {
return xerrors.Errorf("failed to decode NVD JSON: %w", err)
}
@@ -58,20 +55,33 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
return nil
}
func save(items []vulnerability.Item) error {
func save(items []Item) error {
log.Logger.Debug("NVD batch update")
err := vulnerability.BatchUpdate(func(b *bolt.Bucket) error {
for _, item := range items {
cveID := item.Cve.Meta.ID
severity, _ := vulnerability.NewSeverity(item.Impact.BaseMetricV2.Severity)
severityV3, _ := vulnerability.NewSeverity(item.Impact.BaseMetricV3.CvssV3.BaseSeverity)
var references []string
for _, ref := range item.Cve.References.ReferenceDataList {
references = append(references, ref.URL)
}
var description string
for _, d := range item.Cve.Description.DescriptionDataList {
if d.Value != "" {
description = d.Value
break
}
}
vuln := vulnerability.Vulnerability{
Severity: severity,
SeverityV3: severityV3,
// TODO
References: []string{},
Severity: severity,
SeverityV3: severityV3,
References: references,
Title: "",
Description: "",
Description: description,
}
if err := db.Put(b, cveID, vulnerability.Nvd, vuln); err != nil {

55
pkg/vulnsrc/nvd/types.go Normal file
View File

@@ -0,0 +1,55 @@
package nvd
type NVD struct {
CVEItems []Item `json:"CVE_Items"`
}
type Item struct {
Cve Cve
Impact Impact
}
type Cve struct {
Meta Meta `json:"CVE_data_meta"`
References References
Description Description
}
type Meta struct {
ID string
}
type Impact struct {
BaseMetricV2 BaseMetricV2
BaseMetricV3 BaseMetricV3
}
type BaseMetricV2 struct {
Severity string
}
type BaseMetricV3 struct {
CvssV3 CvssV3
}
type CvssV3 struct {
BaseSeverity string
}
type References struct {
ReferenceDataList []ReferenceData `json:"reference_data"`
}
type ReferenceData struct {
Name string
Refsource string
URL string
}
type Description struct {
DescriptionDataList []DescriptionData `json:"description_data"`
}
type DescriptionData struct {
Lang string
Value string
}

View File

@@ -3,7 +3,6 @@ package redhat
import (
"encoding/json"
"fmt"
"gopkg.in/cheggaaa/pb.v1"
"io"
"io/ioutil"
"path/filepath"
@@ -41,7 +40,7 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("Red Hat updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var cves []RedhatCVE

View File

@@ -3,7 +3,6 @@ package ubuntu
import (
"encoding/json"
"fmt"
"gopkg.in/cheggaaa/pb.v1"
"io"
"path/filepath"
@@ -56,7 +55,7 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
}
log.Logger.Debugf("Ubuntu OVAL updated files: %d", len(targets))
bar := pb.StartNew(len(targets))
bar := utils.PbStartNew(len(targets))
defer bar.Finish()
var cves []UbuntuCVE

View File

@@ -2,13 +2,17 @@ package vulnerability
const (
// Data source
Nvd = "nvd"
RedHat = "redhat"
Debian = "debian"
DebianOVAL = "debian-oval"
Ubuntu = "ubuntu"
CentOS = "centos"
Fedora = "fedora"
Amazon = "amazon"
Alpine = "alpine"
Nvd = "nvd"
RedHat = "redhat"
Debian = "debian"
DebianOVAL = "debian-oval"
Ubuntu = "ubuntu"
CentOS = "centos"
Fedora = "fedora"
Amazon = "amazon"
Alpine = "alpine"
RubySec = "ruby-advisory-db"
PhpSecurityAdvisories = "php-security-advisories"
NodejsSecurityWg = "nodejs-security-wg"
PythonSafetyDB = "python-safety-db"
)

View File

@@ -19,18 +19,18 @@ const (
var (
SeverityNames = []string{
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
"UNKNOWN",
"LOW",
"MEDIUM",
"HIGH",
"CRITICAL",
}
SeverityColor = []func(a ...interface{}) string{
color.New(color.FgRed).SprintFunc(),
color.New(color.FgHiRed).SprintFunc(),
color.New(color.FgYellow).SprintFunc(),
color.New(color.FgBlue).SprintFunc(),
color.New(color.FgCyan).SprintFunc(),
color.New(color.FgBlue).SprintFunc(),
color.New(color.FgYellow).SprintFunc(),
color.New(color.FgHiRed).SprintFunc(),
color.New(color.FgRed).SprintFunc(),
}
)
@@ -43,6 +43,12 @@ func NewSeverity(severity string) (Severity, error) {
return SeverityUnknown, fmt.Errorf("unknown severity: %s", severity)
}
func CompareSeverityString(sev1, sev2 string) bool {
s1, _ := NewSeverity(sev1)
s2, _ := NewSeverity(sev2)
return s1 < s2
}
func ColorizeSeverity(severity string) string {
for i, name := range SeverityNames {
if severity == name {
@@ -59,37 +65,3 @@ func (s Severity) String() string {
type LastUpdated struct {
Date time.Time
}
type NVD struct {
CVEItems []Item `json:"CVE_Items"`
}
type Item struct {
Cve Cve
Impact Impact
}
type Cve struct {
Meta Meta `json:"CVE_data_meta"`
}
type Meta struct {
ID string
}
type Impact struct {
BaseMetricV2 BaseMetricV2
BaseMetricV3 BaseMetricV3
}
type BaseMetricV2 struct {
Severity string
}
type BaseMetricV3 struct {
CvssV3 CvssV3
}
type CvssV3 struct {
BaseSeverity string
}

View File

@@ -13,6 +13,7 @@ const (
)
type Vulnerability struct {
ID string // e.g. CVE-2019-8331, OSVDB-104365
CvssScore float64
CvssScoreV3 float64
Severity Severity

View File

@@ -1,6 +1,8 @@
package vulnsrc
import (
"path/filepath"
"github.com/knqyf263/trivy/pkg/git"
"github.com/knqyf263/trivy/pkg/log"
"github.com/knqyf263/trivy/pkg/utils"
@@ -11,7 +13,6 @@ import (
"github.com/knqyf263/trivy/pkg/vulnsrc/redhat"
"github.com/knqyf263/trivy/pkg/vulnsrc/ubuntu"
"golang.org/x/xerrors"
"path/filepath"
)
const (