Compare commits

...

6 Commits

Author SHA1 Message Date
knqyf263
507fac9284 Move remic 2019-05-11 14:45:52 +09:00
Masahiro
22abb9dab1 Add options for remic (#4)
* Fix filename

* Add options
2019-05-11 14:36:20 +09:00
knqyf263
6463176bc0 Update README 2019-05-08 19:14:48 +09:00
knqyf263
b208bc4c12 Add --refresh option 2019-05-08 19:10:08 +09:00
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
15 changed files with 255 additions and 235 deletions

View File

@@ -5,12 +5,31 @@
[![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
A Simple and Comprehensive Vulnerability Scanner for Containers, Compatible with CI
# Abstract
Scan containers
`Trivy` is a simple and comprehensive vulnerability scanner for containers.
`Trivy` detects vulnerabilities of OS packages (Alpine, RHEL, CentOS, etc.) and application dependencies (Bundler, Composer, npm, etc.).
`Trivy` is easy to use. Just install the binary and you're ready to scan. It can be scanned just by specifying a container image name.
It is considered to be used in CI. Before pushing to a container registry, you can scan your local container image easily.
See [here](#continuous-integration-ci) for details.
# Features
- Detect comprehensive vulnerabilities
- OS packages (Alpine, Red Hat Enterprise Linux, CentOS, Debian, Ubuntu)
- **Application dependencies** (Bundler, Composer, Pipenv, npm)
- Simple
- Specify only an image name
- Easy installation
- **No need for prerequirements** such as installation of DB, libraries, etc.
- `apt-get install`, `yum install` and `brew install` is possible (See [Installation](#installation))
- High accuracy
- Especially Alpine
- **Compatible with CI**
- See [CI Example](#continuous-integration-ci)
# Installation
@@ -141,11 +160,10 @@ repository: https://github.com/knqyf263/trivy-ci-test
# Usage
```
$ trivy -h
NAME:
trivy - A simple and comprehensive vulnerability scanner for containers
USAGE:
main [options] image_name
trivy [options] image_name
VERSION:
0.0.3
OPTIONS:
@@ -157,10 +175,11 @@ OPTIONS:
--skip-update skip db update
--clean, -c clean all cache
--quiet, -q suppress progress bar
--ignore-unfixed display only fixed vulnerabilities
--refresh refresh DB (usually used after version update of trivy
--debug, -d debug mode
--help, -h show help
--version, -v print the version
```
# Q&A
@@ -199,11 +218,18 @@ $ brew install knqyf263/trivy/trivy
```
## Others
### Detected version update of trivy. Please try again with --refresh option
Try again with `--refresh` option
```
$ trivy --refresh alpine:3.9
```
### Unknown error
Try again with `--clean` option
```
$ trivy --clean alpine:3.8
$ trivy --clean
```
# Contribute

View File

@@ -1,67 +0,0 @@
package main
import (
"os"
"strings"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"github.com/knqyf263/trivy/pkg/remic"
"github.com/urfave/cli"
"github.com/knqyf263/trivy/pkg/log"
)
func main() {
cli.AppHelpTemplate = `NAME:
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
VERSION:
{{.Version}}{{end}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if len .Authors}}
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}}
{{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}}
OPTIONS:
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}
`
app := cli.NewApp()
app.Name = "remic"
app.Version = "0.0.1"
app.ArgsUsage = "file"
app.Usage = "A simple and fast tool for detecting vulnerabilities in application dependencies"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "format, f",
Value: "table",
Usage: "format (table, json)",
},
cli.StringFlag{
Name: "severity, s",
Value: strings.Join(vulnerability.SeverityNames, ","),
Usage: "severity of vulnerabilities to be displayed",
},
cli.StringFlag{
Name: "output, o",
Usage: "output file name",
},
cli.BoolFlag{
Name: "debug, d",
Usage: "debug mode",
},
}
app.Action = func(c *cli.Context) error {
return remic.Run(c)
}
err := app.Run(os.Args)
if err != nil {
log.Logger.Fatal(err)
}
}

View File

@@ -81,6 +81,10 @@ OPTIONS:
Name: "ignore-unfixed",
Usage: "display only fixed vulnerabilities",
},
cli.BoolFlag{
Name: "refresh",
Usage: "refresh DB (usually used after version update of trivy)",
},
cli.BoolFlag{
Name: "debug, d",
Usage: "debug mode",

View File

@@ -2,10 +2,11 @@ package db
import (
"encoding/json"
"github.com/knqyf263/trivy/pkg/log"
"os"
"path/filepath"
"github.com/knqyf263/trivy/pkg/log"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/utils"
@@ -14,11 +15,11 @@ import (
)
var (
db *bolt.DB
db *bolt.DB
dbDir = filepath.Join(utils.CacheDir(), "db")
)
func Init() (err error) {
dbDir := filepath.Join(utils.CacheDir(), "db")
if err = os.MkdirAll(dbDir, 0700); err != nil {
return xerrors.Errorf("failed to mkdir: %w", err)
}
@@ -32,6 +33,33 @@ func Init() (err error) {
return nil
}
func Reset() error {
if err := os.RemoveAll(dbDir); err != nil {
return xerrors.Errorf("failed to reset DB: %w", err)
}
return nil
}
func GetVersion() string {
var version string
value, err := Get("trivy", "metadata", "version")
if err != nil {
return ""
}
if err = json.Unmarshal(value, &version); err != nil {
return ""
}
return version
}
func SetVersion(version string) error {
err := Update("trivy", "metadata", "version", version)
if err != nil {
return xerrors.Errorf("failed to save DB version: %w", err)
}
return nil
}
func Update(rootBucket, nestedBucket, key string, value interface{}) error {
err := db.Update(func(tx *bolt.Tx) error {
return PutNestedBucket(tx, rootBucket, nestedBucket, key, value)

View File

@@ -5,6 +5,8 @@ import (
"path/filepath"
"strings"
"github.com/knqyf263/trivy/pkg/db"
"github.com/knqyf263/trivy/pkg/log"
"github.com/knqyf263/trivy/pkg/utils"
"golang.org/x/xerrors"
@@ -52,6 +54,10 @@ func CloneOrPull(url, repoPath string) (map[string]struct{}, error) {
return nil, xerrors.Errorf("failed to clone repository: %w", err)
}
}
// Need to refresh all vulnerabilities
if db.GetVersion() == "" {
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil

View File

@@ -1,85 +0,0 @@
package remic
import (
l "log"
"os"
"strings"
"github.com/knqyf263/trivy/pkg/scanner"
"github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability"
"github.com/knqyf263/trivy/pkg/vulnsrc"
"github.com/urfave/cli"
"golang.org/x/xerrors"
"github.com/knqyf263/trivy/pkg/db"
"github.com/knqyf263/trivy/pkg/log"
"github.com/knqyf263/trivy/pkg/report"
)
func Run(c *cli.Context) (err error) {
debug := c.Bool("debug")
if err = log.InitLogger(debug); err != nil {
l.Fatal(err)
}
args := c.Args()
if len(args) == 0 {
return xerrors.New(`remic" requires at least 1 argument.`)
}
o := c.String("output")
output := os.Stdout
if o != "" {
if output, err = os.Create(o); err != nil {
return err
}
}
var severities []vulnerability.Severity
for _, s := range strings.Split(c.String("severity"), ",") {
severity, err := vulnerability.NewSeverity(s)
if err != nil {
return err
}
severities = append(severities, severity)
}
if err = db.Init(); err != nil {
return err
}
if err = vulnsrc.Update(); err != nil {
return err
}
fileName := args[0]
f, err := os.Open(fileName)
if err != nil {
return xerrors.Errorf("failed to open a file: %w", err)
}
defer f.Close()
result, err := scanner.ScanFile(f, severities)
if err != nil {
return xerrors.Errorf("failed to scan a file: %w", err)
}
var writer report.Writer
switch c.String("format") {
case "table":
writer = &report.TableWriter{Output: output}
case "json":
writer = &report.JsonWriter{Output: output}
default:
return xerrors.New("unknown format")
}
if err = writer.Write([]report.Result{result}); err != nil {
return xerrors.Errorf("failed to write results: %w", err)
}
return nil
}

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

@@ -23,21 +23,14 @@ import (
)
func Run(c *cli.Context) (err error) {
cliVersion := c.App.Version
debug := c.Bool("debug")
if err = log.InitLogger(debug); err != nil {
l.Fatal(err)
}
log.Logger.Debugf("cache dir: %s", utils.CacheDir())
args := c.Args()
filePath := c.String("input")
if filePath == "" && len(args) == 0 {
log.Logger.Info(`trivy" requires at least 1 argument or --input option.`)
cli.ShowAppHelpAndExit(c, 1)
}
utils.Quiet = c.Bool("quiet")
clean := c.Bool("clean")
if clean {
log.Logger.Info("Cleaning caches...")
@@ -47,7 +40,18 @@ func Run(c *cli.Context) (err error) {
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 {
log.Logger.Info(`trivy" requires at least 1 argument or --input option.`)
cli.ShowAppHelpAndExit(c, 1)
}
utils.Quiet = c.Bool("quiet")
o := c.String("output")
output := os.Stdout
if o != "" {
@@ -66,10 +70,22 @@ func Run(c *cli.Context) (err error) {
severities = append(severities, severity)
}
if c.Bool("refresh") {
log.Logger.Info("Resetting DB...")
if err = db.Reset(); err != nil {
return xerrors.Errorf("error in refresh DB: %w", err)
}
}
if err = db.Init(); err != nil {
return xerrors.Errorf("error in vulnerability DB initialize: %w", err)
}
dbVersion := db.GetVersion()
if dbVersion != "" && dbVersion != cliVersion {
log.Logger.Fatal("Detected version update of trivy. Please try again with --refresh option")
}
if !c.Bool("skip-update") {
if err = vulnsrc.Update(); err != nil {
return xerrors.Errorf("error in vulnerability DB update: %w", err)
@@ -101,9 +117,16 @@ func Run(c *cli.Context) (err error) {
return xerrors.Errorf("failed to write results: %w", err)
}
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(c.Int("exit-code"))
if err = db.SetVersion(cliVersion); err != nil {
return xerrors.Errorf("unexpected error: %w", err)
}
exitCode := c.Int("exit-code")
if exitCode != 0 {
for _, result := range results {
if len(result.Vulnerabilities) > 0 {
os.Exit(exitCode)
}
}
}

View File

@@ -72,7 +72,7 @@ func Scan(files extractor.FileMap) (map[string][]types.Vulnerability, error) {
}
func ScanFile(f *os.File) ([]types.Vulnerability, error) {
scanner := NewScanner(f.Name())
scanner := NewScanner(filepath.Base(f.Name()))
if scanner == nil {
return nil, xerrors.New("unknown file type")
}

View File

@@ -7,6 +7,8 @@ import (
"os"
"sort"
"github.com/genuinetools/reg/registry"
"github.com/knqyf263/trivy/pkg/log"
"github.com/knqyf263/trivy/pkg/report"
@@ -37,6 +39,14 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity,
var err error
ctx := context.Background()
image, err := registry.ParseImage(imageName)
if err != nil {
return nil, xerrors.Errorf("invalid image: %w", err)
}
if image.Tag == "latest" {
log.Logger.Warn("You should avoid using the :latest tag as it is cached. You need to specify '--clean' option when :latest image is changed")
}
var target string
var files extractor.FileMap
if imageName != "" {
@@ -62,7 +72,7 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity,
osFamily, osVersion, osVulns, err := ospkg.Scan(files)
if err != nil {
return nil, xerrors.New("failed to scan image")
return nil, xerrors.Errorf("failed to scan image: %w", err)
}
@@ -73,7 +83,7 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity,
libVulns, err := library.Scan(files)
if err != nil {
return nil, xerrors.New("failed to scan libraries")
return nil, xerrors.Errorf("failed to scan libraries: %w", err)
}
for path, vulns := range libVulns {
results = append(results, report.Result{
@@ -85,14 +95,14 @@ func ScanImage(imageName, filePath string, severities []vulnerability.Severity,
return results, nil
}
func ScanFile(f *os.File, severities []vulnerability.Severity) (report.Result, error) {
func ScanFile(f *os.File, severities []vulnerability.Severity, ignoreUnfixed bool) (report.Result, error) {
vulns, err := library.ScanFile(f)
if err != nil {
return report.Result{}, xerrors.New("failed to scan libraries in file")
return report.Result{}, xerrors.Errorf("failed to scan libraries in file: %w", err)
}
result := report.Result{
FileName: f.Name(),
Vulnerabilities: processVulnerabilties(vulns, severities, false),
Vulnerabilities: processVulnerabilties(vulns, severities, ignoreUnfixed),
}
return result, nil
}
@@ -100,13 +110,15 @@ func ScanFile(f *os.File, severities []vulnerability.Severity) (report.Result, e
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 == "" {
@@ -138,17 +150,15 @@ 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 {
@@ -183,6 +193,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
}

View File

@@ -18,9 +18,7 @@ import (
)
const (
nvdDir = "nvd"
rootBucket = "NVD"
nestedBucket = "dummy"
nvdDir = "nvd"
)
func Update(dir string, updatedFiles map[string]struct{}) error {
@@ -36,9 +34,9 @@ func Update(dir string, updatedFiles map[string]struct{}) error {
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)
}
@@ -57,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

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

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