Compare commits

..

67 Commits

Author SHA1 Message Date
Kota Kanbe
740781af56 feat(logging): add -log-to-file and don't output to file by default (#1209)
* feat(logging): add -log-to-file and don't output to file by default

* update go-cve-dict

* fix lint err
2021-04-05 17:41:07 +09:00
Kota Kanbe
36c9c229b8 fix(report): avoid nil pointer when report FreeBSD (#1208) 2021-04-05 12:54:27 +09:00
Norihiro NAKAOKA
183fdcbdef fix: support for missing files in the results or results directory (#1206)
* fix: support for missing files in the results or results directory

* fix: support for missing files in the results or results directory
2021-04-05 07:28:20 +09:00
Kota Kanbe
a2a697900a refactor: move const to constant pkg (#1205) 2021-04-02 15:33:02 +09:00
Kota Kanbe
6fef4db8a0 fix .goreleaser.yml (#1204)
* fix .goreleaser.yml

* chore: fix lint warnings
2021-04-01 17:43:54 +09:00
sadayuki-matsuno
e879ff1e9e feat(scanner) export pkg list scan method (#1203)
* feat(scanner) export pkg list scan method

* fix args

* fix func

* fix init debian
2021-04-01 17:38:20 +09:00
Kota Kanbe
9bfe0627ae refactor: don't use global Config in private func (#1197)
* refactor: cve_client.go

* refactor: don't use global Config in private func

* remove import alias for config

* refactor: dbclient

* refactor: resultDir

* refactor: resultsDir

* refactor

* refactor: gost

* refactor: db client

* refactor: cveDB

* refactor: cvedb

* refactor: exploitDB

* refactor: remove detector/dbclient.go

* refactor: writer

* refactor: syslog writer

* refactor: ips

* refactor: ensureResultDir

* refactor: proxy

* fix(db): call CloseDB

* add integration test

* feat(report): sort array in json

* sort func for json diff

* add build-int to makefile

* add int-rds-redis to makefile

* fix: test case, makefile

* fix makefile

* show cve count after diff

* make diff

* diff -c

* sort exploits in json for diff

* sort metasploit, exploit
2021-04-01 13:36:24 +09:00
Tomoya Amachi
0179f4299a fix(trivy-to-vuls): converts even if null vulnerabilities (#1201) 2021-03-22 19:32:08 +09:00
Kota Kanbe
56017e57a0 feat(trivy): update trivy (#1196) 2021-03-12 09:31:48 +09:00
Kota Kanbe
cda91e0906 refactor: loading owasp dependency check xml (#1195) 2021-03-11 08:51:44 +09:00
Kota Kanbe
5d47adb5c9 fix(report): prioritize env vars over config.toml (#1194) 2021-03-10 07:39:58 +09:00
Kota Kanbe
54e73c2f54 fix(wordpress): enable to detect vulns of WordPress Core (#1193) 2021-03-09 10:40:52 +09:00
segatomo
2d075079f1 fix(log): remove log output of opening and migrating db (#1191)
* fix(log): remove log output of opening and migrating db

* fix(log): remove log output of opening and migrating db
2021-03-05 16:16:10 +09:00
Kota Kanbe
2a8ee4b22b refactor(report): azure and aws writer (#1190) 2021-03-04 07:42:38 +09:00
Kota Kanbe
1ec31d7be9 fix(configtest): all servers in the config if no args #1184 (#1189) 2021-03-03 12:51:07 +09:00
Kota Kanbe
02286b0c59 fix(scan): scan all servers in the config if no args #1184 (#1188) 2021-03-03 12:30:30 +09:00
Kota Kanbe
1d0c5dea9f fix(ubuntu): Fix deferred packages not showing as affected (#1187)
* fix(ubuntu): Fix deferred packages not showing as affected

https://github.com/kotakanbe/goval-dictionary/pull/122

* chore: Go version up
2021-03-02 07:50:35 +09:00
Kota Kanbe
1c4a12c4b7 refactor(report): initialize DB connection (#1186) 2021-03-02 06:34:46 +09:00
Kota Kanbe
3f2ac45d71 Refactor logger (#1185)
* refactor: logger

* refactor: logging

* refactor: rename func

* refactor: logging

* refactor: logging format
2021-02-26 10:36:58 +09:00
Kota Kanbe
518f4dc039 refactor: VulnDict (#1183) 2021-02-25 10:13:51 +09:00
Kota Kanbe
2cdeef4ffe refactor(config): validateOnReport (#1182) 2021-02-25 07:41:49 +09:00
Kota Kanbe
03579126fd refactor(config): localize config used like a global variable (#1179)
* refactor(report): LocalFileWriter

* refactor -format-json

* refacotr: -format-one-email

* refactor: -format-csv

* refactor: -gzip

* refactor: -format-full-text

* refactor: -format-one-line-text

* refactor: -format-list

* refacotr: remove -to-* from config

* refactor: IgnoreGitHubDismissed

* refactor: GitHub

* refactor: IgnoreUnsocred

* refactor: diff

* refacotr: lang

* refacotr: cacheDBPath

* refactor: Remove config references

* refactor: ScanResults

* refacotr: constant pkg

* chore: comment

* refactor: scanner

* refactor: scanner

* refactor: serverapi.go

* refactor: serverapi

* refactor: change pkg structure

* refactor: serverapi.go

* chore: remove emtpy file

* fix(scan): remove -ssh-native-insecure option

* fix(scan): remove the deprecated option `keypassword`
2021-02-25 05:54:17 +09:00
Kota Kanbe
e3c27e1817 fix(saas): Don't overwrite config.toml if UUID already set (#1180)
* fix(saas): Don't overwrite config.toml if UUID already set

* add a test case
2021-02-19 06:42:22 +09:00
Richard Alloway
aeaf308679 Add test-case to verify proper version comparison in lessThan() (#1178)
* Add test-case to verify proper version comparison when either/both/neither of newVer and ovalmodels.Package contain "_<minor version>"

* Rename vera to newVer in Test_lessThan()

* Fix oval/util_test.go formatting (make fmt)

Co-authored-by: Richard Alloway (OpenLogic) <ralloway@perforce.com>
2021-02-14 05:30:07 +09:00
Kota Kanbe
f5e47bea40 chore: add a test-case to #1176 (#1177) 2021-02-12 13:46:29 +09:00
Richard Alloway
50cf13a7f2 Pass packInOVAL.Version through centOSVersionToRHEL() to remove the "_<point release>" portion so that packInOVAL.Version strings like 1.8.23-10.el7_9.1 become 1.8.23-10.el7.1 (same behavior as newVer, which now allows packInOVAL.Version and newVer to be directly compared). (#1176)
Co-authored-by: Richard Alloway (OpenLogic) <ralloway@perforce.com>
2021-02-12 13:33:36 +09:00
Kota Kanbe
abd8041772 fix(scan): yum ps warning for Red Hat family (#1174)
* fix(yumps): no debug message for known patterns

* refactor(scan): yum-ps

* refacotr(scan): pkgPs
2021-02-12 13:03:06 +09:00
Kota Kanbe
847c6438e7 chore: fix debug message (#1169) 2021-02-11 06:31:51 +09:00
Kota Kanbe
ef8309df27 chore: remove the heck binary (#1173) 2021-02-11 06:31:32 +09:00
sadayuki-matsuno
0dff6cf983 fix(gost/microsoft) add workaround into mitigation (#1170)
* fix(gost/microsoft) add workaround into mitigation

* fix(gost/microsoft) fix typo and delete workaround field from vulninfo
2021-02-10 19:37:28 +09:00
kazuminn
4c04acbd9e feat(report) : Differences between vulnerability patched items (#1157)
* add plusDiff() and minusDiff()
* add plusDiff minusDiff test

Co-authored-by: Kota Kanbe <kotakanbe@gmail.com>
2021-02-10 06:55:48 +09:00
Kota Kanbe
1c4f231572 fix(scan): ignore rpm -qf exit status (#1168) 2021-02-09 17:26:12 +09:00
Kota Kanbe
51b8e169d2 fix(scan): warning if lsof command not found (#1167) 2021-02-07 07:28:45 +09:00
Kota Kanbe
b4611ae9b7 fix(scan): fix yum-ps warning Failed to exec which -bash (#1166) 2021-02-07 07:23:12 +09:00
Kota Kanbe
cd6722017b fix(scan): yum-ps err Failed to find the package (#1165) 2021-02-06 08:42:06 +09:00
Kota Kanbe
290edffccf fix(log): output version to log for debugging purpose (#1163) 2021-02-04 07:47:56 +09:00
Kota Kanbe
64a6222bf9 fix(report): set created_at and updated_at of trivy to json (#1162) 2021-02-03 17:52:44 +09:00
Kota Kanbe
adb686b7c9 fix(report): set created_at and updated_at of wpscan.com to json (#1161) 2021-02-03 16:41:44 +09:00
Kota Kanbe
d4af341b0f fix(report): remove duplicated refreshing logic when report with -diff (#1160) 2021-02-03 07:37:19 +09:00
Kota Kanbe
fea7e93c8d chore: fix comment (#1158) 2021-02-02 06:06:49 +09:00
sadayuki-matsuno
8b6b8d0f2e feat(wordpress): define API limit exceed error for wpscan.com (#1155)
* feat(wordpress) specify wp err

* fix typo, chagne const name

Co-authored-by: Kota Kanbe <kotakanbe@gmail.com>
2021-01-30 09:53:41 +09:00
Kota Kanbe
4dcbd865cc fix(report): set http timeout 10 sec (#1154)
* fix(report): set http timeout 10 sec

* fix: add an error handling
2021-01-30 09:40:33 +09:00
Kota Kanbe
39b19444fe Merge branch 'master' of github.com:future-architect/vuls 2021-01-28 16:24:14 +09:00
Kota Kanbe
644d5a5462 fix(report): remove retry logic for wpscan.com (#1151)
* fix(saas) change saas upload s3 key (#1116)

* fix(report): remove retry logic for wpscan.com

Co-authored-by: sadayuki-matsuno <sadayuki.matsuno@gmail.com>
2021-01-28 16:21:33 +09:00
Kota Kanbe
8e18451e3f Merge branch 'master' of github.com:future-architect/vuls 2021-01-28 08:24:23 +09:00
Kota Kanbe
3dbdd01f97 fix(report): wordrpess scanning skipped when package is emtpy (#1150) 2021-01-28 08:24:03 +09:00
sadayuki-matsuno
a89079c005 fix(saas) change saas upload s3 key (#1116) 2021-01-28 08:20:13 +09:00
sadayuki-matsuno
a8c0926b4f fix(saas) change saas upload s3 key (#1116) 2021-01-27 14:43:09 +09:00
Kota Kanbe
dd2959a31b fix(eol): add eol for alpine 3.13 (#1149) 2021-01-27 12:52:07 +09:00
Kota Kanbe
51099f42c3 fix(tui): runtime panic when tui with docker-base-setup (#1148)
* fix(tui): runtime panic when tui with docker-base-setup

* pass test case
2021-01-26 09:40:26 +09:00
Kota Kanbe
63f170cc7a fix(report): set severity in Red Hat OVAL to both CVSS v3 and v2 #1146 (#1147) 2021-01-26 07:58:59 +09:00
Kota Kanbe
3c1489e588 feat(report): range notion calc by severity when no-cvss-score (#1145) 2021-01-25 13:22:55 +09:00
Kota Kanbe
e4f1e03f62 feat(github): display GitHub Security Advisory details (#1143) 2021-01-24 09:15:04 +09:00
Kota Kanbe
83d48ec990 Create codeql-analysis.yml 2021-01-24 09:06:13 +09:00
Kota Kanbe
b20d2b2684 fix(scan): skip wordpress scan for preudo servers (#1142) 2021-01-21 07:11:55 +09:00
Kota Kanbe
2b918c70ae fix(scan): config dump nocolor in debug mode. (#1141) 2021-01-21 06:38:37 +09:00
Kota Kanbe
1100c133ba feat(config): Default values for WordPress scanning to be set in config.toml (#1140)
* chore: update go mod

* fix(wordpress): set default if defined in config.toml
2021-01-21 06:22:25 +09:00
Kota Kanbe
88899f0e89 refactor: around CheckHTTPHealth (#1139) 2021-01-20 07:41:29 +09:00
Kota Kanbe
59dc0059bc fix(model): omit changelog from json if empty (#1137) 2021-01-19 09:01:35 +09:00
Kota Kanbe
986fb304c0 fix(scan): add --nogpgcheck to dnf mod list to avoid Error: Cache-only enabled but no cache for *** (#1136) 2021-01-19 08:05:20 +09:00
Kota Kanbe
d6435d2885 fix(xml): remove -format-xml #1068 (#1134) 2021-01-18 04:38:00 +09:00
shopper
affb456499 fix(email.go):Fix runtime error(invalid memory address) (#1133) 2021-01-18 04:08:14 +09:00
Kota Kanbe
705ed0a0ac fix(discover): change config.toml template (#1132) 2021-01-16 07:58:46 +09:00
Kota Kanbe
dfffe5b508 fix(config): err occurs when host not set in local-scan-mode (#1129)
If host is not set in local scan mode, an error occurs.
2021-01-14 09:22:04 +09:00
Shigechika AIKAWA
fca102edba fix dnf prompt and ssh user (#1126) 2021-01-14 08:22:06 +09:00
Kota Kanbe
554b6345a2 chore: go mod update (#1127) 2021-01-14 08:12:47 +09:00
Kota Kanbe
aa954dc84c fix(scan): kindness msg when no-cache err on dnf mod list (#1128) 2021-01-14 08:12:35 +09:00
147 changed files with 54283 additions and 6517 deletions

67
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '32 20 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -19,7 +19,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
go-version: 1.16
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2

View File

@@ -11,7 +11,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: 1.15.x
go-version: 1.16.x
id: go
- name: Check out code into the Go module directory

View File

@@ -19,4 +19,4 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
git_user_name: kotakanbe
git_user_email: kotakanbe@gmail.com
go_version: 1.15.6
go_version: 1.16.x

3
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.vscode
*.txt
*.json
*.sqlite3*
*.db
tags
@@ -10,7 +9,7 @@ issues/
vendor/
log/
results/
*config.toml
config.toml
!setup/docker/*
.DS_Store
dist/

View File

@@ -74,7 +74,6 @@ archives:
format: tar.gz
files:
- LICENSE
- NOTICE
- README*
- CHANGELOG.md
@@ -85,7 +84,6 @@ archives:
format: tar.gz
files:
- LICENSE
- NOTICE
- README*
- CHANGELOG.md
@@ -96,7 +94,6 @@ archives:
format: tar.gz
files:
- LICENSE
- NOTICE
- README*
- CHANGELOG.md
@@ -107,7 +104,6 @@ archives:
format: tar.gz
files:
- LICENSE
- NOTICE
- README*
- CHANGELOG.md
snapshot:

View File

@@ -80,3 +80,120 @@ build-trivy-to-vuls: pretest fmt
# future-vuls
build-future-vuls: pretest fmt
$(GO) build -o future-vuls contrib/future-vuls/cmd/*.go
# integration-test
BASE_DIR := '${PWD}/integration/results'
NOW=$(shell date --iso-8601=seconds)
NOW_JSON_DIR := '${BASE_DIR}/$(NOW)'
ONE_SEC_AFTER=$(shell date -d '+1 second' --iso-8601=seconds)
ONE_SEC_AFTER_JSON_DIR := '${BASE_DIR}/$(ONE_SEC_AFTER)'
diff:
# git clone git@github.com:vulsio/vulsctl.git
# cd vulsctl/docker
# ./update-all.sh
# cd /path/to/vuls
# vim integration/int-config.toml
# ln -s vuls vuls.new
# ln -s oldvuls vuls.old
# make int
# (ex. test 10 times: for i in `seq 10`; do make int ARGS=-quiet ; done)
ifneq ($(shell ls -U1 ${BASE_DIR} | wc -l), 0)
mv ${BASE_DIR}/* /tmp
endif
mkdir -p ${NOW_JSON_DIR}
cp integration/data/*.json ${NOW_JSON_DIR}
./vuls.old report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-config.toml $(ARGS)
mkdir -p ${ONE_SEC_AFTER_JSON_DIR}
cp integration/data/*.json ${ONE_SEC_AFTER_JSON_DIR}
./vuls.new report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-config.toml $(ARGS)
find ${NOW_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
find ${ONE_SEC_AFTER_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
diff -c ${NOW_JSON_DIR} ${ONE_SEC_AFTER_JSON_DIR}
echo "old: ${NOW_JSON_DIR} , new: ${ONE_SEC_AFTER_JSON_DIR}"
diff-redis:
# docker network create redis-nw
# docker run --name redis -d --network redis-nw -p 127.0.0.1:6379:6379 redis
# git clone git@github.com:vulsio/vulsctl.git
# cd vulsctl/docker
# ./update-all-redis.sh
# (or export DOCKER_NETWORK=redis-nw; cd /home/ubuntu/vulsctl/docker; ./update-all.sh --dbtype redis --dbpath "redis://redis/0")
# vim integration/int-redis-config.toml
# ln -s vuls vuls.new
# ln -s oldvuls vuls.old
# make int-redis
ifneq ($(shell ls -U1 ${BASE_DIR} | wc -l), 0)
mv ${BASE_DIR}/* /tmp
endif
mkdir -p ${NOW_JSON_DIR}
cp integration/data/*.json ${NOW_JSON_DIR}
./vuls.old report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-redis-config.toml
mkdir -p ${ONE_SEC_AFTER_JSON_DIR}
cp integration/data/*.json ${ONE_SEC_AFTER_JSON_DIR}
./vuls.new report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-redis-config.toml
find ${NOW_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
find ${ONE_SEC_AFTER_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
diff -c ${NOW_JSON_DIR} ${ONE_SEC_AFTER_JSON_DIR}
echo "old: ${NOW_JSON_DIR} , new: ${ONE_SEC_AFTER_JSON_DIR}"
diff-rdb-redis:
ifneq ($(shell ls -U1 ${BASE_DIR} | wc -l), 0)
mv ${BASE_DIR}/* /tmp
endif
mkdir -p ${NOW_JSON_DIR}
cp integration/data/*.json ${NOW_JSON_DIR}
./vuls.new report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-config.toml
mkdir -p ${ONE_SEC_AFTER_JSON_DIR}
cp integration/data/*.json ${ONE_SEC_AFTER_JSON_DIR}
./vuls.new report --format-json --refresh-cve --results-dir=${BASE_DIR} -config=./integration/int-redis-config.toml
# remove reportedAt line
find ${NOW_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
find ${ONE_SEC_AFTER_JSON_DIR} -type f -exec sed -i -e '/reportedAt/d' {} \;
# remove "Type": line
find ${NOW_JSON_DIR} -type f -exec sed -i -e '/"Type":/d' {} \;
find ${ONE_SEC_AFTER_JSON_DIR} -type f -exec sed -i -e '/"Type":/d' {} \;
# remove "SQLite3Path": line
find ${NOW_JSON_DIR} -type f -exec sed -i -e '/"SQLite3Path":/d' {} \;
find ${ONE_SEC_AFTER_JSON_DIR} -type f -exec sed -i -e '/"SQLite3Path":/d' {} \;
diff -c ${NOW_JSON_DIR} ${ONE_SEC_AFTER_JSON_DIR}
echo "old: ${NOW_JSON_DIR} , new: ${ONE_SEC_AFTER_JSON_DIR}"
for jsonfile in ${NOW_JSON_DIR}/*.json ; do \
echo $$jsonfile; cat $$jsonfile | jq ".scannedCves | length" ; \
done
for jsonfile in ${ONE_SEC_AFTER_JSON_DIR}/*.json ; do \
echo $$jsonfile; cat $$jsonfile | jq ".scannedCves | length" ; \
done
head= $(shell git rev-parse HEAD)
prev= $(shell git rev-parse HEAD^)
branch=$(shell git rev-parse --abbrev-ref HEAD)
build-integration:
git stash
# buld HEAD
git checkout ${head}
make build
mv -f ./vuls ./vuls.${head}
# HEAD^
git checkout ${prev}
make build
mv -f ./vuls ./vuls.${prev}
git checkout ${branch}
git stash apply stash@\{0\}
# working tree
make build
# for integration testing, vuls.new and vuls.old needed.
# ex)
# $ ln -s ./vuls ./vuls.new
# $ ln -s ./vuls.${head} ./vuls.old
# or
# $ ln -s ./vuls.${prev} ./vuls.old
# $ make int
# $ make int-redis

2
NOTICE
View File

@@ -1,2 +0,0 @@
Vuls Copyright (C) 2016 Future Corporation , Japan.

View File

@@ -177,6 +177,10 @@ For more information such as Installation, Tutorial, Usage, visit [vuls.io](http
kotakanbe ([@kotakanbe](https://twitter.com/kotakanbe)) created vuls and [these fine people](https://github.com/future-architect/vuls/graphs/contributors) have contributed.
## Contribute
see [vulsdoc](https://vuls.io/docs/en/how-to-contribute.html)
----
## Stargazers over time

12
cache/bolt.go vendored
View File

@@ -5,8 +5,8 @@ import (
"time"
"github.com/boltdb/bolt"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/util"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
@@ -14,12 +14,12 @@ import (
// boltdb is used to store a cache of Changelogs of Ubuntu/Debian
type Bolt struct {
Path string
Log *logrus.Entry
Log logging.Logger
db *bolt.DB
}
// SetupBolt opens a boltdb and creates a meta bucket if not exists.
func SetupBolt(path string, l *logrus.Entry) error {
func SetupBolt(path string, l logging.Logger) error {
l.Infof("Open boltDB: %s", path)
db, err := bolt.Open(path, 0600, nil)
if err != nil {
@@ -47,7 +47,7 @@ func (b Bolt) Close() error {
return b.db.Close()
}
// CreateBucketIfNotExists creates a buket that is specified by arg.
// CreateBucketIfNotExists creates a bucket that is specified by arg.
func (b *Bolt) createBucketIfNotExists(name string) error {
return b.db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(name))
@@ -93,7 +93,7 @@ func (b Bolt) RefreshMeta(meta Meta) error {
})
}
// EnsureBuckets puts a Meta information and create a buket that holds changelogs.
// EnsureBuckets puts a Meta information and create a bucket that holds changelogs.
func (b Bolt) EnsureBuckets(meta Meta) error {
jsonBytes, err := json.Marshal(meta)
if err != nil {
@@ -159,7 +159,7 @@ func (b Bolt) GetChangelog(servername, packName string) (changelog string, err e
return
}
// PutChangelog put the changelgo of specified packName into the Bucket
// PutChangelog put the changelog of specified packName into the Bucket
func (b Bolt) PutChangelog(servername, packName, changelog string) error {
return b.db.Update(func(tx *bolt.Tx) error {
bkt := tx.Bucket([]byte(servername))

8
cache/bolt_test.go vendored
View File

@@ -7,8 +7,8 @@ import (
"github.com/boltdb/bolt"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/sirupsen/logrus"
)
const path = "/tmp/vuls-test-cache-11111111.db"
@@ -29,7 +29,7 @@ var meta = Meta{
}
func TestSetupBolt(t *testing.T) {
log := logrus.NewEntry(&logrus.Logger{})
log := logging.NewNormalLogger()
err := SetupBolt(path, log)
if err != nil {
t.Errorf("Failed to setup bolt: %s", err)
@@ -57,7 +57,7 @@ func TestSetupBolt(t *testing.T) {
}
func TestEnsureBuckets(t *testing.T) {
log := logrus.NewEntry(&logrus.Logger{})
log := logging.NewNormalLogger()
if err := SetupBolt(path, log); err != nil {
t.Errorf("Failed to setup bolt: %s", err)
}
@@ -98,7 +98,7 @@ func TestEnsureBuckets(t *testing.T) {
func TestPutGetChangelog(t *testing.T) {
clog := "changelog-text"
log := logrus.NewEntry(&logrus.Logger{})
log := logging.NewNormalLogger()
if err := SetupBolt(path, log); err != nil {
t.Errorf("Failed to setup bolt: %s", err)
}

View File

@@ -29,7 +29,7 @@ func main() {
flag.Parse()
if *v {
fmt.Printf("vuls %s %s\n", config.Version, config.Revision)
fmt.Printf("vuls-%s-%s\n", config.Version, config.Revision)
os.Exit(int(subcommands.ExitSuccess))
}

30
config/awsconf.go Normal file
View File

@@ -0,0 +1,30 @@
package config
// AWSConf is aws config
type AWSConf struct {
// AWS profile to use
Profile string `json:"profile"`
// AWS region to use
Region string `json:"region"`
// S3 bucket name
S3Bucket string `json:"s3Bucket"`
// /bucket/path/to/results
S3ResultsDir string `json:"s3ResultsDir"`
// The Server-side encryption algorithm used when storing the reports in S3 (e.g., AES256, aws:kms).
S3ServerSideEncryption string `json:"s3ServerSideEncryption"`
Enabled bool `toml:"-" json:"-"`
}
// Validate configuration
func (c *AWSConf) Validate() (errs []error) {
// TODO
if !c.Enabled {
return
}
return
}

46
config/azureconf.go Normal file
View File

@@ -0,0 +1,46 @@
package config
import (
"os"
"golang.org/x/xerrors"
)
// AzureConf is azure config
type AzureConf struct {
// Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified
AccountName string `json:"accountName"`
// Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified
AccountKey string `json:"-"`
// Azure storage container name
ContainerName string `json:"containerName"`
Enabled bool `toml:"-" json:"-"`
}
const (
azureAccount = "AZURE_STORAGE_ACCOUNT"
azureKey = "AZURE_STORAGE_ACCESS_KEY"
)
// Validate configuration
func (c *AzureConf) Validate() (errs []error) {
if !c.Enabled {
return
}
// overwrite if env var is not empty
if os.Getenv(azureAccount) != "" {
c.AccountName = os.Getenv(azureAccount)
}
if os.Getenv(azureKey) != "" {
c.AccountKey = os.Getenv(azureKey)
}
if c.ContainerName == "" {
errs = append(errs, xerrors.Errorf("Azure storage container name is required"))
}
return
}

View File

@@ -9,11 +9,12 @@ import (
type ChatWorkConf struct {
APIToken string `json:"-"`
Room string `json:"-"`
Enabled bool `toml:"-" json:"-"`
}
// Validate validates configuration
func (c *ChatWorkConf) Validate() (errs []error) {
if !Conf.ToChatWork {
if !c.Enabled {
return
}
if len(c.Room) == 0 {

View File

@@ -3,12 +3,12 @@ package config
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"github.com/asaskevich/govalidator"
log "github.com/sirupsen/logrus"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"golang.org/x/xerrors"
)
@@ -23,29 +23,19 @@ var Conf Config
//Config is struct of Configuration
type Config struct {
Debug bool `json:"debug,omitempty"`
DebugSQL bool `json:"debugSQL,omitempty"`
Lang string `json:"lang,omitempty"`
logging.LogOpts
// scan, report
HTTPProxy string `valid:"url" json:"httpProxy,omitempty"`
LogDir string `json:"logDir,omitempty"`
ResultsDir string `json:"resultsDir,omitempty"`
Pipe bool `json:"pipe,omitempty"`
Quiet bool `json:"quiet,omitempty"`
NoProgress bool `json:"noProgress,omitempty"`
SSHNative bool `json:"sshNative,omitempty"`
Vvv bool `json:"vvv,omitempty"`
Default ServerInfo `json:"default,omitempty"`
Servers map[string]ServerInfo `json:"servers,omitempty"`
CvssScoreOver float64 `json:"cvssScoreOver,omitempty"`
Default ServerInfo `json:"default,omitempty"`
Servers map[string]ServerInfo `json:"servers,omitempty"`
IgnoreUnscoredCves bool `json:"ignoreUnscoredCves,omitempty"`
IgnoreUnfixed bool `json:"ignoreUnfixed,omitempty"`
IgnoreGitHubDismissed bool `json:"ignore_git_hub_dismissed,omitempty"`
CacheDBPath string `json:"cacheDBPath,omitempty"`
TrivyCacheDBDir string `json:"trivyCacheDBDir,omitempty"`
ScanOpts
// report
CveDict GoCveDictConf `json:"cveDict,omitempty"`
OvalDict GovalDictConf `json:"ovalDict,omitempty"`
Gost GostConf `json:"gost,omitempty"`
@@ -60,61 +50,55 @@ type Config struct {
Azure AzureConf `json:"-"`
ChatWork ChatWorkConf `json:"-"`
Telegram TelegramConf `json:"-"`
WpScan WpScanConf `json:"-"`
Saas SaasConf `json:"-"`
WpScan WpScanConf `json:"WpScan,omitempty"`
ReportOpts
}
Saas SaasConf `json:"-"`
DetectIPS bool `json:"detectIps,omitempty"`
// ReportConf is an interface to Validate Report Config
type ReportConf interface {
Validate() []error
}
RefreshCve bool `json:"refreshCve,omitempty"`
ToSlack bool `json:"toSlack,omitempty"`
ToChatWork bool `json:"toChatWork,omitempty"`
ToTelegram bool `json:"ToTelegram,omitempty"`
ToEmail bool `json:"toEmail,omitempty"`
ToSyslog bool `json:"toSyslog,omitempty"`
ToLocalFile bool `json:"toLocalFile,omitempty"`
ToS3 bool `json:"toS3,omitempty"`
ToAzureBlob bool `json:"toAzureBlob,omitempty"`
ToHTTP bool `json:"toHTTP,omitempty"`
FormatXML bool `json:"formatXML,omitempty"`
FormatJSON bool `json:"formatJSON,omitempty"`
FormatOneEMail bool `json:"formatOneEMail,omitempty"`
FormatOneLineText bool `json:"formatOneLineText,omitempty"`
FormatList bool `json:"formatList,omitempty"`
FormatFullText bool `json:"formatFullText,omitempty"`
FormatCsvList bool `json:"formatCsvList,omitempty"`
GZIP bool `json:"gzip,omitempty"`
Diff bool `json:"diff,omitempty"`
// ScanOpts is options for scan
type ScanOpts struct {
Vvv bool `json:"vvv,omitempty"`
}
// ReportOpts is options for report
type ReportOpts struct {
// refactored
CvssScoreOver float64 `json:"cvssScoreOver,omitempty"`
TrivyCacheDBDir string `json:"trivyCacheDBDir,omitempty"`
NoProgress bool `json:"noProgress,omitempty"`
RefreshCve bool `json:"refreshCve,omitempty"`
IgnoreUnfixed bool `json:"ignoreUnfixed,omitempty"`
IgnoreUnscoredCves bool `json:"ignoreUnscoredCves,omitempty"`
DiffPlus bool `json:"diffPlus,omitempty"`
DiffMinus bool `json:"diffMinus,omitempty"`
Diff bool `json:"diff,omitempty"`
Lang string `json:"lang,omitempty"`
//TODO move to GitHubConf
IgnoreGitHubDismissed bool `json:"ignore_git_hub_dismissed,omitempty"`
}
// ValidateOnConfigtest validates
func (c Config) ValidateOnConfigtest() bool {
errs := c.checkSSHKeyExist()
if runtime.GOOS == "windows" && !c.SSHNative {
errs = append(errs, xerrors.New("-ssh-native-insecure is needed on windows"))
}
_, err := govalidator.ValidateStruct(c)
if err != nil {
if _, err := govalidator.ValidateStruct(c); err != nil {
errs = append(errs, err)
}
for _, err := range errs {
log.Error(err)
logging.Log.Error(err)
}
return len(errs) == 0
}
// ValidateOnScan validates configuration
func (c Config) ValidateOnScan() bool {
errs := c.checkSSHKeyExist()
if runtime.GOOS == "windows" && !c.SSHNative {
errs = append(errs, xerrors.New("-ssh-native-insecure is needed on windows"))
}
if len(c.ResultsDir) != 0 {
if ok, _ := govalidator.IsFilePath(c.ResultsDir); !ok {
errs = append(errs, xerrors.Errorf(
@@ -122,29 +106,18 @@ func (c Config) ValidateOnScan() bool {
}
}
if len(c.CacheDBPath) != 0 {
if ok, _ := govalidator.IsFilePath(c.CacheDBPath); !ok {
errs = append(errs, xerrors.Errorf(
"Cache DB path must be a *Absolute* file path. -cache-dbpath: %s",
c.CacheDBPath))
}
}
_, err := govalidator.ValidateStruct(c)
if err != nil {
if _, err := govalidator.ValidateStruct(c); err != nil {
errs = append(errs, err)
}
for _, err := range errs {
log.Error(err)
logging.Log.Error(err)
}
return len(errs) == 0
}
func (c Config) checkSSHKeyExist() (errs []error) {
for serverName, v := range c.Servers {
if v.Type == ServerTypePseudo {
if v.Type == constant.ServerTypePseudo {
continue
}
if v.KeyPath != "" {
@@ -157,39 +130,8 @@ func (c Config) checkSSHKeyExist() (errs []error) {
return errs
}
// ValidateOnReportDB validates configuration
func (c Config) ValidateOnReportDB() bool {
errs := []error{}
if err := validateDB("cvedb", c.CveDict.Type, c.CveDict.SQLite3Path, c.CveDict.URL); err != nil {
errs = append(errs, err)
}
if err := validateDB("ovaldb", c.OvalDict.Type, c.OvalDict.SQLite3Path, c.OvalDict.URL); err != nil {
errs = append(errs, err)
}
if err := validateDB("gostdb", c.Gost.Type, c.Gost.SQLite3Path, c.Gost.URL); err != nil {
errs = append(errs, err)
}
if err := validateDB("exploitdb", c.Exploit.Type, c.Exploit.SQLite3Path, c.Exploit.URL); err != nil {
errs = append(errs, err)
}
if err := validateDB("msfdb", c.Metasploit.Type, c.Metasploit.SQLite3Path, c.Metasploit.URL); err != nil {
errs = append(errs, err)
}
for _, err := range errs {
log.Error(err)
}
return len(errs) == 0
}
// ValidateOnReport validates configuration
func (c Config) ValidateOnReport() bool {
func (c *Config) ValidateOnReport() bool {
errs := []error{}
if len(c.ResultsDir) != 0 {
@@ -204,54 +146,38 @@ func (c Config) ValidateOnReport() bool {
errs = append(errs, err)
}
if mailerrs := c.EMail.Validate(); 0 < len(mailerrs) {
errs = append(errs, mailerrs...)
}
if slackerrs := c.Slack.Validate(); 0 < len(slackerrs) {
errs = append(errs, slackerrs...)
}
if chatworkerrs := c.ChatWork.Validate(); 0 < len(chatworkerrs) {
errs = append(errs, chatworkerrs...)
}
if telegramerrs := c.Telegram.Validate(); 0 < len(telegramerrs) {
errs = append(errs, telegramerrs...)
}
if syslogerrs := c.Syslog.Validate(); 0 < len(syslogerrs) {
errs = append(errs, syslogerrs...)
}
if httperrs := c.HTTP.Validate(); 0 < len(httperrs) {
errs = append(errs, httperrs...)
}
for _, err := range errs {
log.Error(err)
}
return len(errs) == 0
}
// ValidateOnTui validates configuration
func (c Config) ValidateOnTui() bool {
errs := []error{}
if len(c.ResultsDir) != 0 {
if ok, _ := govalidator.IsFilePath(c.ResultsDir); !ok {
errs = append(errs, xerrors.Errorf(
"JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir))
for _, rc := range []ReportConf{
&c.EMail,
&c.Slack,
&c.ChatWork,
&c.Telegram,
&c.Syslog,
&c.HTTP,
&c.AWS,
&c.Azure,
} {
if es := rc.Validate(); 0 < len(es) {
errs = append(errs, es...)
}
}
if err := validateDB("cvedb", c.CveDict.Type, c.CveDict.SQLite3Path, c.CveDict.URL); err != nil {
errs = append(errs, err)
for _, cnf := range []VulnDictInterface{
&Conf.CveDict,
&Conf.OvalDict,
&Conf.Gost,
&Conf.Exploit,
&Conf.Metasploit,
} {
if err := cnf.Validate(); err != nil {
errs = append(errs, xerrors.Errorf("Failed to validate %s: %+v", cnf.GetName(), err))
}
if err := cnf.CheckHTTPHealth(); err != nil {
errs = append(errs, xerrors.Errorf("Run %s as server mode before reporting: %+v", cnf.GetName(), err))
}
}
for _, err := range errs {
log.Error(err)
logging.Log.Error(err)
}
return len(errs) == 0
@@ -261,86 +187,14 @@ func (c Config) ValidateOnTui() bool {
func (c Config) ValidateOnSaaS() bool {
saaserrs := c.Saas.Validate()
for _, err := range saaserrs {
log.Error("Failed to validate SaaS conf: %+w", err)
logging.Log.Error("Failed to validate SaaS conf: %+w", err)
}
return len(saaserrs) == 0
}
// validateDB validates configuration
func validateDB(dictionaryDBName, dbType, dbPath, dbURL string) error {
log.Infof("-%s-type: %s, -%s-url: %s, -%s-path: %s",
dictionaryDBName, dbType, dictionaryDBName, dbURL, dictionaryDBName, dbPath)
switch dbType {
case "sqlite3":
if dbURL != "" {
return xerrors.Errorf("To use SQLite3, specify -%s-type=sqlite3 and -%s-path. To use as http server mode, specify -%s-type=http and -%s-url",
dictionaryDBName, dictionaryDBName, dictionaryDBName, dictionaryDBName)
}
if ok, _ := govalidator.IsFilePath(dbPath); !ok {
return xerrors.Errorf("SQLite3 path must be a *Absolute* file path. -%s-path: %s",
dictionaryDBName, dbPath)
}
case "mysql":
if dbURL == "" {
return xerrors.Errorf(`MySQL connection string is needed. -%s-url="user:pass@tcp(localhost:3306)/dbname"`,
dictionaryDBName)
}
case "postgres":
if dbURL == "" {
return xerrors.Errorf(`PostgreSQL connection string is needed. -%s-url="host=myhost user=user dbname=dbname sslmode=disable password=password"`,
dictionaryDBName)
}
case "redis":
if dbURL == "" {
return xerrors.Errorf(`Redis connection string is needed. -%s-url="redis://localhost/0"`,
dictionaryDBName)
}
case "http":
if dbURL == "" {
return xerrors.Errorf(`URL is needed. -%s-url="http://localhost:1323"`,
dictionaryDBName)
}
default:
return xerrors.Errorf("%s type must be either 'sqlite3', 'mysql', 'postgres', 'redis' or 'http'. -%s-type: %s",
dictionaryDBName, dictionaryDBName, dbType)
}
return nil
}
// AWSConf is aws config
type AWSConf struct {
// AWS profile to use
Profile string `json:"profile"`
// AWS region to use
Region string `json:"region"`
// S3 bucket name
S3Bucket string `json:"s3Bucket"`
// /bucket/path/to/results
S3ResultsDir string `json:"s3ResultsDir"`
// The Server-side encryption algorithm used when storing the reports in S3 (e.g., AES256, aws:kms).
S3ServerSideEncryption string `json:"s3ServerSideEncryption"`
}
// AzureConf is azure config
type AzureConf struct {
// Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified
AccountName string `json:"accountName"`
// Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified
AccountKey string `json:"-"`
// Azure storage container name
ContainerName string `json:"containerName"`
}
// WpScanConf is wpscan.com config
type WpScanConf struct {
Token string `toml:"Token,omitempty" json:"-"`
Token string `toml:"token,omitempty" json:"-"`
DetectInactive bool `toml:"detectInactive,omitempty" json:"detectInactive,omitempty"`
}
@@ -353,7 +207,6 @@ type ServerInfo struct {
Port string `toml:"port,omitempty" json:"port,omitempty"`
SSHConfigPath string `toml:"sshConfigPath,omitempty" json:"sshConfigPath,omitempty"`
KeyPath string `toml:"keyPath,omitempty" json:"keyPath,omitempty"`
KeyPassword string `json:"-" toml:"-"`
CpeNames []string `toml:"cpeNames,omitempty" json:"cpeNames,omitempty"`
ScanMode []string `toml:"scanMode,omitempty" json:"scanMode,omitempty"`
ScanModules []string `toml:"scanModules,omitempty" json:"scanModules,omitempty"`
@@ -376,7 +229,7 @@ type ServerInfo struct {
IgnoredJSONKeys []string `toml:"ignoredJSONKeys,omitempty" json:"ignoredJSONKeys,omitempty"`
IPv4Addrs []string `toml:"-" json:"ipv4Addrs,omitempty"`
IPv6Addrs []string `toml:"-" json:"ipv6Addrs,omitempty"`
IPSIdentifiers map[IPS]string `toml:"-" json:"ipsIdentifiers,omitempty"`
IPSIdentifiers map[string]string `toml:"-" json:"ipsIdentifiers,omitempty"`
WordPress *WordPressConf `toml:"wordpress,omitempty" json:"wordpress,omitempty"`
// internal use
@@ -433,7 +286,7 @@ func (l Distro) String() string {
// MajorVersion returns Major version
func (l Distro) MajorVersion() (int, error) {
if l.Family == Amazon {
if l.Family == constant.Amazon {
if isAmazonLinux1(l.Release) {
return 1, nil
}

View File

@@ -2,6 +2,8 @@ package config
import (
"testing"
. "github.com/future-architect/vuls/constant"
)
func TestSyslogConfValidate(t *testing.T) {
@@ -55,7 +57,7 @@ func TestSyslogConfValidate(t *testing.T) {
}
for i, tt := range tests {
Conf.ToSyslog = true
tt.conf.Enabled = true
errs := tt.conf.Validate()
if len(errs) != tt.expectedErrLength {
t.Errorf("test: %d, expected %d, actual %d", i, tt.expectedErrLength, len(errs))

View File

@@ -1,53 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// ExploitConf is exploit config
type ExploitConf struct {
// DB type for exploit dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://exploit-dictionary.com:1324 or DB connection string
URL string `json:"-"`
// /path/to/exploit.sqlite3
SQLite3Path string `json:"-"`
}
func (cnf *ExploitConf) setDefault() {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, "go-exploitdb.sqlite3")
}
}
const exploitDBType = "EXPLOITDB_TYPE"
const exploitDBURL = "EXPLOITDB_URL"
const exploitDBPATH = "EXPLOITDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *ExploitConf) Init() {
if os.Getenv(exploitDBType) != "" {
cnf.Type = os.Getenv(exploitDBType)
}
if os.Getenv(exploitDBURL) != "" {
cnf.URL = os.Getenv(exploitDBURL)
}
if os.Getenv(exploitDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(exploitDBPATH)
}
cnf.setDefault()
}
// IsFetchViaHTTP returns wether fetch via http
func (cnf *ExploitConf) IsFetchViaHTTP() bool {
return Conf.Exploit.Type == "http"
}

View File

@@ -1,53 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// GoCveDictConf is go-cve-dictionary config
type GoCveDictConf struct {
// DB type of CVE dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://cve-dictionary.com:1323 or DB connection string
URL string `json:"-"`
// /path/to/cve.sqlite3
SQLite3Path string `json:"-"`
}
func (cnf *GoCveDictConf) setDefault() {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, "cve.sqlite3")
}
}
const cveDBType = "CVEDB_TYPE"
const cveDBURL = "CVEDB_URL"
const cveDBPATH = "CVEDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GoCveDictConf) Init() {
if os.Getenv(cveDBType) != "" {
cnf.Type = os.Getenv(cveDBType)
}
if os.Getenv(cveDBURL) != "" {
cnf.URL = os.Getenv(cveDBURL)
}
if os.Getenv(cveDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(cveDBPATH)
}
cnf.setDefault()
}
// IsFetchViaHTTP returns wether fetch via http
func (cnf *GoCveDictConf) IsFetchViaHTTP() bool {
return Conf.CveDict.Type == "http"
}

View File

@@ -1,53 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// GostConf is gost config
type GostConf struct {
// DB type for gost dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://gost-dictionary.com:1324 or DB connection string
URL string `json:"-"`
// /path/to/gost.sqlite3
SQLite3Path string `json:"-"`
}
func (cnf *GostConf) setDefault() {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, "gost.sqlite3")
}
}
const gostDBType = "GOSTDB_TYPE"
const gostDBURL = "GOSTDB_URL"
const gostDBPATH = "GOSTDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GostConf) Init() {
if os.Getenv(gostDBType) != "" {
cnf.Type = os.Getenv(gostDBType)
}
if os.Getenv(gostDBURL) != "" {
cnf.URL = os.Getenv(gostDBURL)
}
if os.Getenv(gostDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(gostDBPATH)
}
cnf.setDefault()
}
// IsFetchViaHTTP returns wether fetch via http
func (cnf *GostConf) IsFetchViaHTTP() bool {
return Conf.Gost.Type == "http"
}

View File

@@ -1,54 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// GovalDictConf is goval-dictionary config
type GovalDictConf struct {
// DB type of OVAL dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://goval-dictionary.com:1324 or DB connection string
URL string `json:"-"`
// /path/to/oval.sqlite3
SQLite3Path string `json:"-"`
}
func (cnf *GovalDictConf) setDefault() {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, "oval.sqlite3")
}
}
const govalType = "OVALDB_TYPE"
const govalURL = "OVALDB_URL"
const govalPATH = "OVALDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GovalDictConf) Init() {
if os.Getenv(govalType) != "" {
cnf.Type = os.Getenv(govalType)
}
if os.Getenv(govalURL) != "" {
cnf.URL = os.Getenv(govalURL)
}
if os.Getenv(govalPATH) != "" {
cnf.SQLite3Path = os.Getenv(govalPATH)
}
cnf.setDefault()
}
// IsFetchViaHTTP returns wether fetch via http
func (cnf *GovalDictConf) IsFetchViaHTTP() bool {
return Conf.OvalDict.Type == "http"
}

View File

@@ -8,31 +8,25 @@ import (
// HTTPConf is HTTP config
type HTTPConf struct {
URL string `valid:"url" json:"-"`
URL string `valid:"url" json:"-"`
Enabled bool `toml:"-" json:"-"`
}
const httpKey = "VULS_HTTP_URL"
// Validate validates configuration
func (c *HTTPConf) Validate() (errs []error) {
if !Conf.ToHTTP {
if !c.Enabled {
return nil
}
// overwrite if env var is not empty
if os.Getenv(httpKey) != "" {
c.URL = os.Getenv(httpKey)
}
if _, err := govalidator.ValidateStruct(c); err != nil {
errs = append(errs, err)
}
return errs
}
const httpKey = "VULS_HTTP_URL"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (c *HTTPConf) Init(toml HTTPConf) {
if os.Getenv(httpKey) != "" {
c.URL = os.Getenv(httpKey)
}
if toml.URL != "" {
c.URL = toml.URL
}
}

View File

@@ -1,9 +0,0 @@
package config
// IPS is
type IPS string
const (
// DeepSecurity is
DeepSecurity IPS = "deepsecurity"
)

View File

@@ -1,53 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// MetasploitConf is metasploit config
type MetasploitConf struct {
// DB type for metasploit dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://metasploit-dictionary.com:1324 or DB connection string
URL string `json:"-"`
// /path/to/metasploit.sqlite3
SQLite3Path string `json:"-"`
}
func (cnf *MetasploitConf) setDefault() {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, "go-msfdb.sqlite3")
}
}
const metasploitDBType = "METASPLOITDB_TYPE"
const metasploitDBURL = "METASPLOITDB_URL"
const metasploitDBPATH = "METASPLOITDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *MetasploitConf) Init() {
if os.Getenv(metasploitDBType) != "" {
cnf.Type = os.Getenv(metasploitDBType)
}
if os.Getenv(metasploitDBURL) != "" {
cnf.URL = os.Getenv(metasploitDBURL)
}
if os.Getenv(metasploitDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(metasploitDBPATH)
}
cnf.setDefault()
}
// IsFetchViaHTTP returns wether fetch via http
func (cnf *MetasploitConf) IsFetchViaHTTP() bool {
return Conf.Metasploit.Type == "http"
}

View File

@@ -4,59 +4,8 @@ import (
"fmt"
"strings"
"time"
)
const (
// RedHat is
RedHat = "redhat"
// Debian is
Debian = "debian"
// Ubuntu is
Ubuntu = "ubuntu"
// CentOS is
CentOS = "centos"
// Fedora is
// Fedora = "fedora"
// Amazon is
Amazon = "amazon"
// Oracle is
Oracle = "oracle"
// FreeBSD is
FreeBSD = "freebsd"
// Raspbian is
Raspbian = "raspbian"
// Windows is
Windows = "windows"
// OpenSUSE is
OpenSUSE = "opensuse"
// OpenSUSELeap is
OpenSUSELeap = "opensuse.leap"
// SUSEEnterpriseServer is
SUSEEnterpriseServer = "suse.linux.enterprise.server"
// SUSEEnterpriseDesktop is
SUSEEnterpriseDesktop = "suse.linux.enterprise.desktop"
// SUSEOpenstackCloud is
SUSEOpenstackCloud = "suse.openstack.cloud"
// Alpine is
Alpine = "alpine"
// ServerTypePseudo is used for ServerInfo.Type, r.Family
ServerTypePseudo = "pseudo"
"github.com/future-architect/vuls/constant"
)
// EOL has End-of-Life information
@@ -89,7 +38,7 @@ func (e EOL) IsExtendedSuppportEnded(now time.Time) bool {
// https://github.com/aquasecurity/trivy/blob/master/pkg/detector/ospkg/redhat/redhat.go#L20
func GetEOL(family, release string) (eol EOL, found bool) {
switch family {
case Amazon:
case constant.Amazon:
rel := "2"
if isAmazonLinux1(release) {
rel = "1"
@@ -98,7 +47,7 @@ func GetEOL(family, release string) (eol EOL, found bool) {
"1": {StandardSupportUntil: time.Date(2023, 6, 30, 23, 59, 59, 0, time.UTC)},
"2": {},
}[rel]
case RedHat:
case constant.RedHat:
// https://access.redhat.com/support/policy/updates/errata
eol, found = map[string]EOL{
"3": {Ended: true},
@@ -115,7 +64,7 @@ func GetEOL(family, release string) (eol EOL, found bool) {
StandardSupportUntil: time.Date(2029, 5, 31, 23, 59, 59, 0, time.UTC),
},
}[major(release)]
case CentOS:
case constant.CentOS:
// https://en.wikipedia.org/wiki/CentOS#End-of-support_schedule
// TODO Stream
eol, found = map[string]EOL{
@@ -126,7 +75,7 @@ func GetEOL(family, release string) (eol EOL, found bool) {
"7": {StandardSupportUntil: time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC)},
"8": {StandardSupportUntil: time.Date(2021, 12, 31, 23, 59, 59, 0, time.UTC)},
}[major(release)]
case Oracle:
case constant.Oracle:
eol, found = map[string]EOL{
// Source:
// https://www.oracle.com/a/ocom/docs/elsp-lifetime-069338.pdf
@@ -145,7 +94,7 @@ func GetEOL(family, release string) (eol EOL, found bool) {
StandardSupportUntil: time.Date(2029, 7, 1, 23, 59, 59, 0, time.UTC),
},
}[major(release)]
case Debian:
case constant.Debian:
eol, found = map[string]EOL{
// https://wiki.debian.org/LTS
"6": {Ended: true},
@@ -154,10 +103,10 @@ func GetEOL(family, release string) (eol EOL, found bool) {
"9": {StandardSupportUntil: time.Date(2022, 6, 30, 23, 59, 59, 0, time.UTC)},
"10": {StandardSupportUntil: time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC)},
}[major(release)]
case Raspbian:
case constant.Raspbian:
// Not found
eol, found = map[string]EOL{}[major(release)]
case Ubuntu:
case constant.Ubuntu:
// https://wiki.ubuntu.com/Releases
eol, found = map[string]EOL{
"14.10": {Ended: true},
@@ -189,9 +138,9 @@ func GetEOL(family, release string) (eol EOL, found bool) {
StandardSupportUntil: time.Date(2022, 7, 1, 23, 59, 59, 0, time.UTC),
},
}[release]
case SUSEEnterpriseServer:
case constant.SUSEEnterpriseServer:
//TODO
case Alpine:
case constant.Alpine:
// https://github.com/aquasecurity/trivy/blob/master/pkg/detector/ospkg/alpine/alpine.go#L19
// https://wiki.alpinelinux.org/wiki/Alpine_Linux:Releases
eol, found = map[string]EOL{
@@ -216,8 +165,9 @@ func GetEOL(family, release string) (eol EOL, found bool) {
"3.10": {StandardSupportUntil: time.Date(2021, 5, 1, 23, 59, 59, 0, time.UTC)},
"3.11": {StandardSupportUntil: time.Date(2021, 11, 1, 23, 59, 59, 0, time.UTC)},
"3.12": {StandardSupportUntil: time.Date(2022, 5, 1, 23, 59, 59, 0, time.UTC)},
"3.13": {StandardSupportUntil: time.Date(2022, 11, 1, 23, 59, 59, 0, time.UTC)},
}[majorDotMinor(release)]
case FreeBSD:
case constant.FreeBSD:
// https://www.freebsd.org/security/
eol, found = map[string]EOL{
"7": {Ended: true},

View File

@@ -3,6 +3,8 @@ package config
import (
"testing"
"time"
. "github.com/future-architect/vuls/constant"
)
func TestEOL_IsStandardSupportEnded(t *testing.T) {
@@ -258,7 +260,7 @@ func TestEOL_IsStandardSupportEnded(t *testing.T) {
found: true,
},
{
name: "Debian 3.9 eol",
name: "Alpine 3.9 eol",
fields: fields{family: Alpine, release: "3.9"},
now: time.Date(2021, 1, 6, 23, 59, 59, 0, time.UTC),
stdEnded: true,
@@ -266,8 +268,8 @@ func TestEOL_IsStandardSupportEnded(t *testing.T) {
found: true,
},
{
name: "Debian 3.13 not found",
fields: fields{family: Alpine, release: "3.13"},
name: "Alpine 3.14 not found",
fields: fields{family: Alpine, release: "3.14"},
now: time.Date(2021, 1, 6, 23, 59, 59, 0, time.UTC),
stdEnded: false,
extEnded: false,

View File

@@ -16,11 +16,12 @@ type SlackConf struct {
AuthUser string `json:"-" toml:"authUser,omitempty"`
NotifyUsers []string `toml:"notifyUsers,omitempty" json:"-"`
Text string `json:"-"`
Enabled bool `toml:"-" json:"-"`
}
// Validate validates configuration
func (c *SlackConf) Validate() (errs []error) {
if !Conf.ToSlack {
if !c.Enabled {
return
}

View File

@@ -15,6 +15,7 @@ type SMTPConf struct {
To []string `toml:"to,omitempty" json:"-"`
Cc []string `toml:"cc,omitempty" json:"-"`
SubjectPrefix string `toml:"subjectPrefix,omitempty" json:"-"`
Enabled bool `toml:"-" json:"-"`
}
func checkEmails(emails []string) (errs []error) {
@@ -31,10 +32,9 @@ func checkEmails(emails []string) (errs []error) {
// Validate SMTP configuration
func (c *SMTPConf) Validate() (errs []error) {
if !Conf.ToEmail {
if !c.Enabled {
return
}
// Check Emails fromat
emails := []string{}
emails = append(emails, c.From)
emails = append(emails, c.To...)
@@ -44,10 +44,10 @@ func (c *SMTPConf) Validate() (errs []error) {
errs = append(errs, emailErrs...)
}
if len(c.SMTPAddr) == 0 {
if c.SMTPAddr == "" {
errs = append(errs, xerrors.New("email.smtpAddr must not be empty"))
}
if len(c.SMTPPort) == 0 {
if c.SMTPPort == "" {
errs = append(errs, xerrors.New("email.smtpPort must not be empty"))
}
if len(c.To) == 0 {

View File

@@ -17,11 +17,12 @@ type SyslogConf struct {
Facility string `json:"-"`
Tag string `json:"-"`
Verbose bool `json:"-"`
Enabled bool `toml:"-" json:"-"`
}
// Validate validates configuration
func (c *SyslogConf) Validate() (errs []error) {
if !Conf.ToSyslog {
if !c.Enabled {
return nil
}
// If protocol is empty, it will connect to the local syslog server.

View File

@@ -7,13 +7,14 @@ import (
// TelegramConf is Telegram config
type TelegramConf struct {
Token string `json:"-"`
ChatID string `json:"-"`
Token string `json:"-"`
ChatID string `json:"-"`
Enabled bool `toml:"-" json:"-"`
}
// Validate validates configuration
func (c *TelegramConf) Validate() (errs []error) {
if !Conf.ToTelegram {
if !c.Enabled {
return
}
if len(c.ChatID) == 0 {

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/BurntSushi/toml"
"github.com/future-architect/vuls/constant"
"github.com/knqyf263/go-cpe/naming"
"golang.org/x/xerrors"
)
@@ -15,26 +16,24 @@ type TOMLLoader struct {
// Load load the configuration TOML file specified by path arg.
func (c TOMLLoader) Load(pathToToml, keyPass string) error {
// util.Log.Infof("Loading config: %s", pathToToml)
if _, err := toml.DecodeFile(pathToToml, &Conf); err != nil {
return err
}
if keyPass != "" {
Conf.Default.KeyPassword = keyPass
}
Conf.CveDict.Init()
Conf.OvalDict.Init()
Conf.Gost.Init()
Conf.Exploit.Init()
Conf.Metasploit.Init()
for _, cnf := range []VulnDictInterface{
&Conf.CveDict,
&Conf.OvalDict,
&Conf.Gost,
&Conf.Exploit,
&Conf.Metasploit,
} {
cnf.Init()
}
index := 0
for name, server := range Conf.Servers {
server.ServerName = name
if 0 < len(server.KeyPassword) {
return xerrors.Errorf("[Deprecated] KEYPASSWORD IN CONFIG FILE ARE UNSECURE. REMOVE THEM IMMEDIATELY FOR A SECURITY REASONS. THEY WILL BE REMOVED IN A FUTURE RELEASE: %s", name)
}
if err := setDefaultIfEmpty(&server, Conf.Default); err != nil {
return xerrors.Errorf("Failed to set default value to config. server: %s, err: %w", name, err)
}
@@ -135,7 +134,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
}
func setDefaultIfEmpty(server *ServerInfo, d ServerInfo) error {
if server.Type != ServerTypePseudo {
if server.Type != constant.ServerTypePseudo {
if len(server.Host) == 0 {
return xerrors.Errorf("server.host is empty")
}
@@ -153,10 +152,8 @@ func setDefaultIfEmpty(server *ServerInfo, d ServerInfo) error {
}
if server.User == "" {
if Conf.Default.User != "" {
server.User = Conf.Default.User
}
if server.Port != "local" {
server.User = Conf.Default.User
if server.User == "" && server.Port != "local" {
return xerrors.Errorf("server.user is empty")
}
}
@@ -168,10 +165,6 @@ func setDefaultIfEmpty(server *ServerInfo, d ServerInfo) error {
if server.KeyPath == "" {
server.KeyPath = Conf.Default.KeyPath
}
if server.KeyPassword == "" {
server.KeyPassword = Conf.Default.KeyPassword
}
}
if len(server.Lockfiles) == 0 {
@@ -203,11 +196,12 @@ func setDefaultIfEmpty(server *ServerInfo, d ServerInfo) error {
server.Memo = Conf.Default.Memo
}
// TODO set default WordPress
if server.WordPress == nil {
server.WordPress = &WordPressConf{}
server.WordPress = Conf.Default.WordPress
if server.WordPress == nil {
server.WordPress = &WordPressConf{}
}
}
//TODO set nil in config re-generate in saas subcmd
if len(server.IgnoredJSONKeys) == 0 {
server.IgnoredJSONKeys = Conf.Default.IgnoredJSONKeys

276
config/vulnDictConf.go Normal file
View File

@@ -0,0 +1,276 @@
package config
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/asaskevich/govalidator"
"github.com/future-architect/vuls/logging"
"github.com/parnurzeal/gorequest"
"golang.org/x/xerrors"
)
// VulnDictInterface is an interface of vulnsrc
type VulnDictInterface interface {
Init()
Validate() error
IsFetchViaHTTP() bool
CheckHTTPHealth() error
GetName() string
GetType() string
GetURL() string
GetSQLite3Path() string
GetDebugSQL() bool
}
// VulnDict is a base struct of vuln dicts
type VulnDict struct {
Name string
// DB type of CVE dictionary (sqlite3, mysql, postgres or redis)
Type string
// http://cve-dictionary.com:1323 or DB connection string
URL string `json:"-"`
// /path/to/cve.sqlite3
SQLite3Path string
DebugSQL bool
}
// GetType returns type
func (cnf VulnDict) GetType() string {
return cnf.Type
}
// GetName returns name
func (cnf VulnDict) GetName() string {
return cnf.Name
}
// GetURL returns url
func (cnf VulnDict) GetURL() string {
return cnf.URL
}
// GetSQLite3Path return the path of SQLite3
func (cnf VulnDict) GetSQLite3Path() string {
return cnf.SQLite3Path
}
// GetDebugSQL return debugSQL flag
func (cnf VulnDict) GetDebugSQL() bool {
return cnf.DebugSQL
}
// Validate settings
func (cnf VulnDict) Validate() error {
logging.Log.Infof("%s.type=%s, %s.url=%s, %s.SQLite3Path=%s",
cnf.Name, cnf.Type, cnf.Name, cnf.URL, cnf.Name, cnf.SQLite3Path)
switch cnf.Type {
case "sqlite3":
if cnf.URL != "" {
return xerrors.Errorf("To use SQLite3, specify %s.type=sqlite3 and %s.SQLite3Path. To use as HTTP server mode, specify %s.type=http and %s.url",
cnf.Name, cnf.Name, cnf.Name, cnf.Name)
}
if ok, _ := govalidator.IsFilePath(cnf.SQLite3Path); !ok {
return xerrors.Errorf("SQLite3 path must be a *Absolute* file path. %s.SQLite3Path: %s",
cnf.Name, cnf.SQLite3Path)
}
if _, err := os.Stat(cnf.SQLite3Path); os.IsNotExist(err) {
logging.Log.Warnf("%s.SQLite3Path=%s file not found", cnf.Name, cnf.SQLite3Path)
}
case "mysql":
if cnf.URL == "" {
return xerrors.Errorf(`MySQL connection string is needed. %s.url="user:pass@tcp(localhost:3306)/dbname"`, cnf.Name)
}
case "postgres":
if cnf.URL == "" {
return xerrors.Errorf(`PostgreSQL connection string is needed. %s.url="host=myhost user=user dbname=dbname sslmode=disable password=password"`, cnf.Name)
}
case "redis":
if cnf.URL == "" {
return xerrors.Errorf(`Redis connection string is needed. %s.url="redis://localhost/0"`, cnf.Name)
}
case "http":
if cnf.URL == "" {
return xerrors.Errorf(`URL is needed. -%s-url="http://localhost:1323"`, cnf.Name)
}
default:
return xerrors.Errorf("%s.type must be either 'sqlite3', 'mysql', 'postgres', 'redis' or 'http'. %s.type: %s", cnf.Name, cnf.Name, cnf.Type)
}
return nil
}
// Init the struct
func (cnf VulnDict) Init() {}
func (cnf *VulnDict) setDefault(sqlite3Name string) {
if cnf.Type == "" {
cnf.Type = "sqlite3"
}
if cnf.URL == "" && cnf.SQLite3Path == "" {
wd, _ := os.Getwd()
cnf.SQLite3Path = filepath.Join(wd, sqlite3Name)
}
}
// IsFetchViaHTTP returns if fetch via HTTP
func (cnf VulnDict) IsFetchViaHTTP() bool {
return cnf.Type == "http"
}
// CheckHTTPHealth checks http server status
func (cnf VulnDict) CheckHTTPHealth() error {
if !cnf.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", cnf.URL)
resp, _, errs := gorequest.New().Timeout(10 * time.Second).SetDebug(Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to request to CVE server. url: %s, errs: %s",
url, errs)
}
return nil
}
// GovalDictConf is goval-dictionary config
type GovalDictConf struct {
VulnDict
}
const govalType = "OVALDB_TYPE"
const govalURL = "OVALDB_URL"
const govalPATH = "OVALDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GovalDictConf) Init() {
cnf.Name = "ovalDict"
if os.Getenv(govalType) != "" {
cnf.Type = os.Getenv(govalType)
}
if os.Getenv(govalURL) != "" {
cnf.URL = os.Getenv(govalURL)
}
if os.Getenv(govalPATH) != "" {
cnf.SQLite3Path = os.Getenv(govalPATH)
}
cnf.setDefault("oval.sqlite3")
cnf.DebugSQL = Conf.DebugSQL
}
// ExploitConf is exploit config
type ExploitConf struct {
VulnDict
}
const exploitDBType = "EXPLOITDB_TYPE"
const exploitDBURL = "EXPLOITDB_URL"
const exploitDBPATH = "EXPLOITDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *ExploitConf) Init() {
cnf.Name = "exploit"
if os.Getenv(exploitDBType) != "" {
cnf.Type = os.Getenv(exploitDBType)
}
if os.Getenv(exploitDBURL) != "" {
cnf.URL = os.Getenv(exploitDBURL)
}
if os.Getenv(exploitDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(exploitDBPATH)
}
cnf.setDefault("go-exploitdb.sqlite3")
cnf.DebugSQL = Conf.DebugSQL
}
// GoCveDictConf is GoCveDict config
type GoCveDictConf struct {
VulnDict
}
const cveDBType = "CVEDB_TYPE"
const cveDBURL = "CVEDB_URL"
const cveDBPATH = "CVEDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GoCveDictConf) Init() {
cnf.Name = "cveDict"
if os.Getenv(cveDBType) != "" {
cnf.Type = os.Getenv(cveDBType)
}
if os.Getenv(cveDBURL) != "" {
cnf.URL = os.Getenv(cveDBURL)
}
if os.Getenv(cveDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(cveDBPATH)
}
cnf.setDefault("cve.sqlite3")
cnf.DebugSQL = Conf.DebugSQL
}
// GostConf is gost config
type GostConf struct {
VulnDict
}
const gostDBType = "GOSTDB_TYPE"
const gostDBURL = "GOSTDB_URL"
const gostDBPATH = "GOSTDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *GostConf) Init() {
cnf.Name = "gost"
if os.Getenv(gostDBType) != "" {
cnf.Type = os.Getenv(gostDBType)
}
if os.Getenv(gostDBURL) != "" {
cnf.URL = os.Getenv(gostDBURL)
}
if os.Getenv(gostDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(gostDBPATH)
}
cnf.setDefault("gost.sqlite3")
cnf.DebugSQL = Conf.DebugSQL
}
// MetasploitConf is gost go-metasploitdb
type MetasploitConf struct {
VulnDict
}
const metasploitDBType = "METASPLOITDB_TYPE"
const metasploitDBURL = "METASPLOITDB_URL"
const metasploitDBPATH = "METASPLOITDB_SQLITE3_PATH"
// Init set options with the following priority.
// 1. Environment variable
// 2. config.toml
func (cnf *MetasploitConf) Init() {
cnf.Name = "metasploit"
if os.Getenv(metasploitDBType) != "" {
cnf.Type = os.Getenv(metasploitDBType)
}
if os.Getenv(metasploitDBURL) != "" {
cnf.URL = os.Getenv(metasploitDBURL)
}
if os.Getenv(metasploitDBPATH) != "" {
cnf.SQLite3Path = os.Getenv(metasploitDBPATH)
}
cnf.setDefault("go-msfdb.sqlite3")
cnf.DebugSQL = Conf.DebugSQL
}

61
constant/constant.go Normal file
View File

@@ -0,0 +1,61 @@
package constant
// Global constant
// Pkg local constants should not be defined here.
// Define them in the each package.
const (
// RedHat is
RedHat = "redhat"
// Debian is
Debian = "debian"
// Ubuntu is
Ubuntu = "ubuntu"
// CentOS is
CentOS = "centos"
// Fedora is
// Fedora = "fedora"
// Amazon is
Amazon = "amazon"
// Oracle is
Oracle = "oracle"
// FreeBSD is
FreeBSD = "freebsd"
// Raspbian is
Raspbian = "raspbian"
// Windows is
Windows = "windows"
// OpenSUSE is
OpenSUSE = "opensuse"
// OpenSUSELeap is
OpenSUSELeap = "opensuse.leap"
// SUSEEnterpriseServer is
SUSEEnterpriseServer = "suse.linux.enterprise.server"
// SUSEEnterpriseDesktop is
SUSEEnterpriseDesktop = "suse.linux.enterprise.desktop"
// SUSEOpenstackCloud is
SUSEOpenstackCloud = "suse.openstack.cloud"
// Alpine is
Alpine = "alpine"
// ServerTypePseudo is used for ServerInfo.Type, r.Family
ServerTypePseudo = "pseudo"
// DeepSecurity is
DeepSecurity = "deepsecurity"
)

View File

@@ -22,6 +22,9 @@ func Parse(vulnJSON []byte, scanResult *models.ScanResult) (result *models.ScanR
vulnInfos := models.VulnInfos{}
uniqueLibraryScannerPaths := map[string]models.LibraryScanner{}
for _, trivyResult := range trivyResults {
if IsTrivySupportedOS(trivyResult.Type) {
overrideServerData(scanResult, &trivyResult)
}
for _, vuln := range trivyResult.Vulnerabilities {
if _, ok := vulnInfos[vuln.VulnerabilityID]; !ok {
vulnInfos[vuln.VulnerabilityID] = models.VulnInfo{
@@ -57,12 +60,24 @@ func Parse(vulnJSON []byte, scanResult *models.ScanResult) (result *models.ScanR
return references[i].Link < references[j].Link
})
var published time.Time
if vuln.PublishedDate != nil {
published = *vuln.PublishedDate
}
var lastModified time.Time
if vuln.LastModifiedDate != nil {
lastModified = *vuln.LastModifiedDate
}
vulnInfo.CveContents = models.CveContents{
models.Trivy: models.CveContent{
Cvss3Severity: vuln.Severity,
References: references,
Title: vuln.Title,
Summary: vuln.Description,
Published: published,
LastModified: lastModified,
},
}
// do only if image type is Vuln
@@ -77,16 +92,6 @@ func Parse(vulnJSON []byte, scanResult *models.ScanResult) (result *models.ScanR
FixState: fixState,
FixedIn: vuln.FixedVersion,
})
// overwrite every time if os package
scanResult.Family = trivyResult.Type
scanResult.ServerName = trivyResult.Target
scanResult.Optional = map[string]interface{}{
"trivy-target": trivyResult.Target,
}
scanResult.ScannedAt = time.Now()
scanResult.ScannedBy = "trivy"
scanResult.ScannedVia = "trivy"
} else {
// LibraryScanの結果
vulnInfo.LibraryFixedIns = append(vulnInfo.LibraryFixedIns, models.LibraryFixedIn{
@@ -162,3 +167,14 @@ func IsTrivySupportedOS(family string) bool {
}
return false
}
func overrideServerData(scanResult *models.ScanResult, trivyResult *report.Result) {
scanResult.Family = trivyResult.Type
scanResult.ServerName = trivyResult.Target
scanResult.Optional = map[string]interface{}{
"trivy-target": trivyResult.Target,
}
scanResult.ScannedAt = time.Now()
scanResult.ScannedBy = "trivy"
scanResult.ScannedVia = "trivy"
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/aquasecurity/trivy/pkg/types"
"github.com/d4l3k/messagediff"
"github.com/future-architect/vuls/models"
)
@@ -3205,6 +3206,33 @@ func TestParse(t *testing.T) {
Optional: map[string]interface{}{"trivy-target": "knqyf263/vuln-image:1.2.3 (alpine 3.7.1)"},
},
},
"found-no-vulns": {
vulnJSON: []byte(`[
{
"Target": "no-vuln-image:v1 (debian 9.13)",
"Type": "debian",
"Vulnerabilities": null
}
]
`),
scanResult: &models.ScanResult{
JSONVersion: 1,
ServerUUID: "uuid",
ScannedCves: models.VulnInfos{},
},
expected: &models.ScanResult{
JSONVersion: 1,
ServerUUID: "uuid",
ServerName: "no-vuln-image:v1 (debian 9.13)",
Family: "debian",
ScannedBy: "trivy",
ScannedVia: "trivy",
ScannedCves: models.VulnInfos{},
Packages: models.Packages{},
LibraryScanners: models.LibraryScanners{},
Optional: map[string]interface{}{"trivy-target": "no-vuln-image:v1 (debian 9.13)"},
},
},
}
for testcase, v := range cases {

View File

@@ -1,6 +1,6 @@
// +build !scanner
package report
package detector
import (
"encoding/json"
@@ -13,69 +13,61 @@ import (
"golang.org/x/xerrors"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/util"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
cvelog "github.com/kotakanbe/go-cve-dictionary/log"
cvemodels "github.com/kotakanbe/go-cve-dictionary/models"
)
// CveClient is api client of CVE disctionary service.
var CveClient cvedictClient
type cvedictClient struct {
// httpProxy string
baseURL string
type goCveDictClient struct {
cnf config.VulnDictInterface
driver cvedb.DB
}
func (api *cvedictClient) initialize() {
api.baseURL = config.Conf.CveDict.URL
func newGoCveDictClient(cnf config.VulnDictInterface, o logging.LogOpts) (*goCveDictClient, error) {
cvelog.SetLogger(o.Debug, o.Quiet, false, o.LogToFile, o.LogDir)
driver, locked, err := newCveDB(cnf)
if locked {
return nil, xerrors.Errorf("SQLite3 is locked: %s", cnf.GetSQLite3Path())
} else if err != nil {
return nil, err
}
return &goCveDictClient{cnf: cnf, driver: driver}, nil
}
func (api cvedictClient) CheckHealth() error {
if !config.Conf.CveDict.IsFetchViaHTTP() {
util.Log.Debugf("get cve-dictionary from %s", config.Conf.CveDict.Type)
func (api goCveDictClient) closeDB() error {
if api.driver == nil {
return nil
}
api.initialize()
url := fmt.Sprintf("%s/health", api.baseURL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to request to CVE server. url: %s, errs: %w",
url, errs)
if err := api.driver.CloseDB(); err != nil {
return xerrors.Errorf("Failed to close DB: %+v", err)
}
return nil
}
func (api goCveDictClient) fetchCveDetails(cveIDs []string) (cveDetails []cvemodels.CveDetail, err error) {
for _, cveID := range cveIDs {
cveDetail, err := api.driver.Get(cveID)
if err != nil {
return nil, xerrors.Errorf("Failed to fetch CVE. err: %w", err)
}
if len(cveDetail.CveID) == 0 {
cveDetails = append(cveDetails, cvemodels.CveDetail{CveID: cveID})
} else {
cveDetails = append(cveDetails, *cveDetail)
}
}
return
}
type response struct {
Key string
CveDetail cvemodels.CveDetail
}
func (api cvedictClient) FetchCveDetails(driver cvedb.DB, cveIDs []string) (cveDetails []cvemodels.CveDetail, err error) {
if !config.Conf.CveDict.IsFetchViaHTTP() {
if driver == nil {
return
}
for _, cveID := range cveIDs {
cveDetail, err := driver.Get(cveID)
if err != nil {
return nil, xerrors.Errorf("Failed to fetch CVE. err: %w", err)
}
if len(cveDetail.CveID) == 0 {
cveDetails = append(cveDetails, cvemodels.CveDetail{
CveID: cveID,
})
} else {
cveDetails = append(cveDetails, *cveDetail)
}
}
return
}
api.baseURL = config.Conf.CveDict.URL
func (api goCveDictClient) fetchCveDetailsViaHTTP(cveIDs []string) (cveDetails []cvemodels.CveDetail, err error) {
reqChan := make(chan string, len(cveIDs))
resChan := make(chan response, len(cveIDs))
errChan := make(chan error, len(cveIDs))
@@ -95,11 +87,11 @@ func (api cvedictClient) FetchCveDetails(driver cvedb.DB, cveIDs []string) (cveD
tasks <- func() {
select {
case cveID := <-reqChan:
url, err := util.URLPathJoin(api.baseURL, "cves", cveID)
url, err := util.URLPathJoin(api.cnf.GetURL(), "cves", cveID)
if err != nil {
errChan <- err
} else {
util.Log.Debugf("HTTP Request to %s", url)
logging.Log.Debugf("HTTP Request to %s", url)
api.httpGet(cveID, url, resChan, errChan)
}
}
@@ -131,22 +123,20 @@ func (api cvedictClient) FetchCveDetails(driver cvedb.DB, cveIDs []string) (cveD
return
}
func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errChan chan<- error) {
func (api goCveDictClient) httpGet(key, url string, resChan chan<- response, errChan chan<- error) {
var body string
var errs []error
var resp *http.Response
f := func() (err error) {
// resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
resp, body, errs = gorequest.New().Get(url).End()
resp, body, errs = gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("HTTP GET Error, url: %s, resp: %v, err: %w",
return xerrors.Errorf("HTTP GET Error, url: %s, resp: %v, err: %+v",
url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s",
t, err)
logging.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %+v", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
@@ -155,7 +145,7 @@ func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errCh
}
cveDetail := cvemodels.CveDetail{}
if err := json.Unmarshal([]byte(body), &cveDetail); err != nil {
errChan <- xerrors.Errorf("Failed to Unmarshall. body: %s, err: %w", body, err)
errChan <- xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err)
return
}
resChan <- response{
@@ -164,39 +154,37 @@ func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errCh
}
}
func (api cvedictClient) FetchCveDetailsByCpeName(driver cvedb.DB, cpeName string) ([]cvemodels.CveDetail, error) {
if config.Conf.CveDict.IsFetchViaHTTP() {
api.baseURL = config.Conf.CveDict.URL
url, err := util.URLPathJoin(api.baseURL, "cpes")
func (api goCveDictClient) fetchCveDetailsByCpeName(cpeName string) ([]cvemodels.CveDetail, error) {
if api.cnf.IsFetchViaHTTP() {
url, err := util.URLPathJoin(api.cnf.GetURL(), "cpes")
if err != nil {
return nil, err
}
query := map[string]string{"name": cpeName}
util.Log.Debugf("HTTP Request to %s, query: %#v", url, query)
logging.Log.Debugf("HTTP Request to %s, query: %#v", url, query)
return api.httpPost(cpeName, url, query)
}
return driver.GetByCpeURI(cpeName)
return api.driver.GetByCpeURI(cpeName)
}
func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cvemodels.CveDetail, error) {
func (api goCveDictClient) httpPost(key, url string, query map[string]string) ([]cvemodels.CveDetail, error) {
var body string
var errs []error
var resp *http.Response
f := func() (err error) {
// req := gorequest.New().SetDebug(config.Conf.Debug).Post(url)
req := gorequest.New().Post(url)
req := gorequest.New().Timeout(10 * time.Second).Post(url)
for key := range query {
req = req.Send(fmt.Sprintf("%s=%s", key, query[key])).Type("json")
}
resp, body, errs = req.End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %w", url, resp, errs)
return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %+v", url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
util.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %s", t, err)
logging.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %+v", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
@@ -206,7 +194,23 @@ func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]c
cveDetails := []cvemodels.CveDetail{}
if err := json.Unmarshal([]byte(body), &cveDetails); err != nil {
return nil,
xerrors.Errorf("Failed to Unmarshall. body: %s, err: %w", body, err)
xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err)
}
return cveDetails, nil
}
func newCveDB(cnf config.VulnDictInterface) (driver cvedb.DB, locked bool, err error) {
if cnf.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.GetURL()
if cnf.GetType() == "sqlite3" {
path = cnf.GetSQLite3Path()
}
driver, locked, err = cvedb.NewDB(cnf.GetType(), path, cnf.GetDebugSQL())
if err != nil {
err = xerrors.Errorf("Failed to init CVE DB. err: %w, path: %s", err, path)
return nil, locked, err
}
return driver, false, nil
}

475
detector/detector.go Normal file
View File

@@ -0,0 +1,475 @@
// +build !scanner
package detector
import (
"os"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/contrib/owasp-dependency-check/parser"
"github.com/future-architect/vuls/cwe"
"github.com/future-architect/vuls/gost"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/oval"
"github.com/future-architect/vuls/reporter"
"github.com/future-architect/vuls/util"
cvemodels "github.com/kotakanbe/go-cve-dictionary/models"
"golang.org/x/xerrors"
)
// Detect vulns and fill CVE detailed information
func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
// Use the same reportedAt for all rs
reportedAt := time.Now()
for i, r := range rs {
if !config.Conf.RefreshCve && !needToRefreshCve(r) {
logging.Log.Info("No need to refresh")
continue
}
if !reuseScannedCves(&r) {
r.ScannedCves = models.VulnInfos{}
}
cpeURIs, owaspDCXMLPath := []string{}, ""
if len(r.Container.ContainerID) == 0 {
cpeURIs = config.Conf.Servers[r.ServerName].CpeNames
owaspDCXMLPath = config.Conf.Servers[r.ServerName].OwaspDCXMLPath
} else {
if s, ok := config.Conf.Servers[r.ServerName]; ok {
if con, ok := s.Containers[r.Container.Name]; ok {
cpeURIs = con.Cpes
owaspDCXMLPath = con.OwaspDCXMLPath
}
}
}
if owaspDCXMLPath != "" {
cpes, err := parser.Parse(owaspDCXMLPath)
if err != nil {
return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
r.ServerInfo(), owaspDCXMLPath, err)
}
cpeURIs = append(cpeURIs, cpes...)
}
if err := DetectLibsCves(&r, config.Conf.TrivyCacheDBDir, config.Conf.NoProgress); err != nil {
return nil, xerrors.Errorf("Failed to fill with Library dependency: %w", err)
}
if err := DetectPkgCves(&r, config.Conf.OvalDict, config.Conf.Gost); err != nil {
return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err)
}
if err := DetectCpeURIsCves(&r, cpeURIs, config.Conf.CveDict, config.Conf.LogOpts); err != nil {
return nil, xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err)
}
repos := config.Conf.Servers[r.ServerName].GitHubRepos
if err := DetectGitHubCves(&r, repos, config.Conf.IgnoreGitHubDismissed); err != nil {
return nil, xerrors.Errorf("Failed to detect GitHub Cves: %w", err)
}
if err := DetectWordPressCves(&r, config.Conf.WpScan); err != nil {
return nil, xerrors.Errorf("Failed to detect WordPress Cves: %w", err)
}
logging.Log.Infof("Fill CVE detailed with gost")
if err := gost.FillCVEsWithRedHat(&r, config.Conf.Gost); err != nil {
return nil, xerrors.Errorf("Failed to fill with gost: %w", err)
}
logging.Log.Infof("Fill CVE detailed with go-cve-dictionary")
if err := FillCvesWithNvdJvn(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil {
return nil, xerrors.Errorf("Failed to fill with CVE: %w", err)
}
nExploitCve, err := FillWithExploit(&r, config.Conf.Exploit)
if err != nil {
return nil, xerrors.Errorf("Failed to fill with exploit: %w", err)
}
logging.Log.Infof("%s: %d exploits are detected", r.FormatServerName(), nExploitCve)
nMetasploitCve, err := FillWithMetasploit(&r, config.Conf.Metasploit)
if err != nil {
return nil, xerrors.Errorf("Failed to fill with metasploit: %w", err)
}
logging.Log.Infof("%s: %d modules are detected", r.FormatServerName(), nMetasploitCve)
FillCweDict(&r)
r.ReportedBy, _ = os.Hostname()
r.Lang = config.Conf.Lang
r.ReportedAt = reportedAt
r.ReportedVersion = config.Version
r.ReportedRevision = config.Revision
r.Config.Report = config.Conf
r.Config.Report.Servers = map[string]config.ServerInfo{
r.ServerName: config.Conf.Servers[r.ServerName],
}
rs[i] = r
}
// Overwrite the json file every time to clear the fields specified in config.IgnoredJSONKeys
for _, r := range rs {
if s, ok := config.Conf.Servers[r.ServerName]; ok {
r = r.ClearFields(s.IgnoredJSONKeys)
}
//TODO don't call here
if err := reporter.OverwriteJSONFile(dir, r); err != nil {
return nil, xerrors.Errorf("Failed to write JSON: %w", err)
}
}
if config.Conf.DiffPlus || config.Conf.DiffMinus {
prevs, err := loadPrevious(rs, config.Conf.ResultsDir)
if err != nil {
return nil, err
}
rs = diff(rs, prevs, config.Conf.DiffPlus, config.Conf.DiffMinus)
}
for i, r := range rs {
r.ScannedCves = r.ScannedCves.FilterByCvssOver(config.Conf.CvssScoreOver)
r.ScannedCves = r.ScannedCves.FilterUnfixed(config.Conf.IgnoreUnfixed)
// IgnoreCves
ignoreCves := []string{}
if r.Container.Name == "" {
ignoreCves = config.Conf.Servers[r.ServerName].IgnoreCves
} else if con, ok := config.Conf.Servers[r.ServerName].Containers[r.Container.Name]; ok {
ignoreCves = con.IgnoreCves
}
r.ScannedCves = r.ScannedCves.FilterIgnoreCves(ignoreCves)
// ignorePkgs
ignorePkgsRegexps := []string{}
if r.Container.Name == "" {
ignorePkgsRegexps = config.Conf.Servers[r.ServerName].IgnorePkgsRegexp
} else if s, ok := config.Conf.Servers[r.ServerName].Containers[r.Container.Name]; ok {
ignorePkgsRegexps = s.IgnorePkgsRegexp
}
r.ScannedCves = r.ScannedCves.FilterIgnorePkgs(ignorePkgsRegexps)
// IgnoreUnscored
if config.Conf.IgnoreUnscoredCves {
r.ScannedCves = r.ScannedCves.FindScoredVulns()
}
r.FilterInactiveWordPressLibs(config.Conf.WpScan.DetectInactive)
rs[i] = r
}
return rs, nil
}
// DetectPkgCves detects OS pkg cves
// pass 2 configs
func DetectPkgCves(r *models.ScanResult, ovalCnf config.GovalDictConf, gostCnf config.GostConf) error {
// Pkg Scan
if r.Release != "" {
// OVAL
if err := detectPkgsCvesWithOval(ovalCnf, r); err != nil {
return xerrors.Errorf("Failed to detect CVE with OVAL: %w", err)
}
// gost
if err := detectPkgsCvesWithGost(gostCnf, r); err != nil {
return xerrors.Errorf("Failed to detect CVE with gost: %w", err)
}
} else if reuseScannedCves(r) {
logging.Log.Infof("r.Release is empty. Use CVEs as it as.")
} else if r.Family == constant.ServerTypePseudo {
logging.Log.Infof("pseudo type. Skip OVAL and gost detection")
} else {
return xerrors.Errorf("Failed to fill CVEs. r.Release is empty")
}
for i, v := range r.ScannedCves {
for j, p := range v.AffectedPackages {
if p.NotFixedYet && p.FixState == "" {
p.FixState = "Not fixed yet"
r.ScannedCves[i].AffectedPackages[j] = p
}
}
}
// To keep backward compatibility
// Newer versions use ListenPortStats,
// but older versions of Vuls are set to ListenPorts.
// Set ListenPorts to ListenPortStats to allow newer Vuls to report old results.
for i, pkg := range r.Packages {
for j, proc := range pkg.AffectedProcs {
for _, ipPort := range proc.ListenPorts {
ps, err := models.NewPortStat(ipPort)
if err != nil {
logging.Log.Warnf("Failed to parse ip:port: %s, err:%+v", ipPort, err)
continue
}
r.Packages[i].AffectedProcs[j].ListenPortStats = append(
r.Packages[i].AffectedProcs[j].ListenPortStats, *ps)
}
}
}
return nil
}
// DetectGitHubCves fetches CVEs from GitHub Security Alerts
func DetectGitHubCves(r *models.ScanResult, githubConfs map[string]config.GitHubConf, ignoreDismissed bool) error {
if len(githubConfs) == 0 {
return nil
}
for ownerRepo, setting := range githubConfs {
ss := strings.Split(ownerRepo, "/")
if len(ss) != 2 {
return xerrors.Errorf("Failed to parse GitHub owner/repo: %s", ownerRepo)
}
owner, repo := ss[0], ss[1]
n, err := DetectGitHubSecurityAlerts(r, owner, repo, setting.Token, ignoreDismissed)
if err != nil {
return xerrors.Errorf("Failed to access GitHub Security Alerts: %w", err)
}
logging.Log.Infof("%s: %d CVEs detected with GHSA %s/%s",
r.FormatServerName(), n, owner, repo)
}
return nil
}
// DetectWordPressCves detects CVEs of WordPress
func DetectWordPressCves(r *models.ScanResult, wpCnf config.WpScanConf) error {
if len(r.WordPressPackages) == 0 {
return nil
}
logging.Log.Infof("Detect WordPress CVE. pkgs: %d ", len(r.WordPressPackages))
n, err := detectWordPressCves(r, wpCnf)
if err != nil {
return xerrors.Errorf("Failed to detect WordPress CVE: %w", err)
}
logging.Log.Infof("%s: found %d WordPress CVEs", r.FormatServerName(), n)
return nil
}
// FillCvesWithNvdJvn fills CVE detail with NVD, JVN
func FillCvesWithNvdJvn(r *models.ScanResult, cnf config.GoCveDictConf, logOpts logging.LogOpts) (err error) {
cveIDs := []string{}
for _, v := range r.ScannedCves {
cveIDs = append(cveIDs, v.CveID)
}
client, err := newGoCveDictClient(&cnf, logOpts)
if err != nil {
return err
}
defer func() {
if err := client.closeDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
var ds []cvemodels.CveDetail
if cnf.IsFetchViaHTTP() {
ds, err = client.fetchCveDetailsViaHTTP(cveIDs)
} else {
ds, err = client.fetchCveDetails(cveIDs)
}
if err != nil {
return err
}
for _, d := range ds {
nvd, exploits, mitigations := models.ConvertNvdJSONToModel(d.CveID, d.NvdJSON)
jvn := models.ConvertJvnToModel(d.CveID, d.Jvn)
alerts := fillCertAlerts(&d)
for cveID, vinfo := range r.ScannedCves {
if vinfo.CveID == d.CveID {
if vinfo.CveContents == nil {
vinfo.CveContents = models.CveContents{}
}
for _, con := range []*models.CveContent{nvd, jvn} {
if con != nil && !con.Empty() {
vinfo.CveContents[con.Type] = *con
}
}
vinfo.AlertDict = alerts
vinfo.Exploits = append(vinfo.Exploits, exploits...)
vinfo.Mitigations = append(vinfo.Mitigations, mitigations...)
r.ScannedCves[cveID] = vinfo
break
}
}
}
return nil
}
func fillCertAlerts(cvedetail *cvemodels.CveDetail) (dict models.AlertDict) {
if cvedetail.NvdJSON != nil {
for _, cert := range cvedetail.NvdJSON.Certs {
dict.En = append(dict.En, models.Alert{
URL: cert.Link,
Title: cert.Title,
Team: "us",
})
}
}
if cvedetail.Jvn != nil {
for _, cert := range cvedetail.Jvn.Certs {
dict.Ja = append(dict.Ja, models.Alert{
URL: cert.Link,
Title: cert.Title,
Team: "jp",
})
}
}
return dict
}
// detectPkgsCvesWithOval fetches OVAL database
func detectPkgsCvesWithOval(cnf config.GovalDictConf, r *models.ScanResult) error {
ovalClient, err := oval.NewOVALClient(r.Family, cnf)
if err != nil {
return err
}
if ovalClient == nil {
return nil
}
logging.Log.Debugf("Check if oval fetched: %s %s", r.Family, r.Release)
ok, err := ovalClient.CheckIfOvalFetched(r.Family, r.Release)
if err != nil {
return err
}
if !ok {
return xerrors.Errorf("OVAL entries of %s %s are not found. Fetch OVAL before reporting. For details, see `https://github.com/kotakanbe/goval-dictionary#usage`", r.Family, r.Release)
}
logging.Log.Debugf("Check if oval fresh: %s %s", r.Family, r.Release)
_, err = ovalClient.CheckIfOvalFresh(r.Family, r.Release)
if err != nil {
return err
}
logging.Log.Debugf("Fill with oval: %s %s", r.Family, r.Release)
nCVEs, err := ovalClient.FillWithOval(r)
if err != nil {
return err
}
logging.Log.Infof("%s: %d CVEs are detected with OVAL", r.FormatServerName(), nCVEs)
return nil
}
func detectPkgsCvesWithGost(cnf config.GostConf, r *models.ScanResult) error {
client, err := gost.NewClient(cnf, r.Family)
if err != nil {
return xerrors.Errorf("Failed to new a gost client: %w", err)
}
nCVEs, err := client.DetectUnfixed(r, true)
if err != nil {
return xerrors.Errorf("Failed to detect unfixed CVEs with gost: %w", err)
}
logging.Log.Infof("%s: %d unfixed CVEs are detected with gost", r.FormatServerName(), nCVEs)
return nil
}
// DetectCpeURIsCves detects CVEs of given CPE-URIs
func DetectCpeURIsCves(r *models.ScanResult, cpeURIs []string, cnf config.GoCveDictConf, logOpts logging.LogOpts) error {
client, err := newGoCveDictClient(&cnf, logOpts)
if err != nil {
return err
}
defer func() {
if err := client.closeDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
nCVEs := 0
for _, name := range cpeURIs {
details, err := client.fetchCveDetailsByCpeName(name)
if err != nil {
return err
}
for _, detail := range details {
if val, ok := r.ScannedCves[detail.CveID]; ok {
names := val.CpeURIs
names = util.AppendIfMissing(names, name)
val.CpeURIs = names
val.Confidences.AppendIfMissing(models.CpeNameMatch)
r.ScannedCves[detail.CveID] = val
} else {
v := models.VulnInfo{
CveID: detail.CveID,
CpeURIs: []string{name},
Confidences: models.Confidences{models.CpeNameMatch},
}
r.ScannedCves[detail.CveID] = v
nCVEs++
}
}
}
logging.Log.Infof("%s: %d CVEs are detected with CPE", r.FormatServerName(), nCVEs)
return nil
}
// FillCweDict fills CWE
func FillCweDict(r *models.ScanResult) {
uniqCweIDMap := map[string]bool{}
for _, vinfo := range r.ScannedCves {
for _, cont := range vinfo.CveContents {
for _, id := range cont.CweIDs {
if strings.HasPrefix(id, "CWE-") {
id = strings.TrimPrefix(id, "CWE-")
uniqCweIDMap[id] = true
}
}
}
}
dict := map[string]models.CweDictEntry{}
for id := range uniqCweIDMap {
entry := models.CweDictEntry{}
if e, ok := cwe.CweDictEn[id]; ok {
if rank, ok := cwe.OwaspTopTen2017[id]; ok {
entry.OwaspTopTen2017 = rank
}
if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
entry.CweTopTwentyfive2019 = rank
}
if rank, ok := cwe.SansTopTwentyfive[id]; ok {
entry.SansTopTwentyfive = rank
}
entry.En = &e
} else {
logging.Log.Debugf("CWE-ID %s is not found in English CWE Dict", id)
entry.En = &cwe.Cwe{CweID: id}
}
if r.Lang == "ja" {
if e, ok := cwe.CweDictJa[id]; ok {
if rank, ok := cwe.OwaspTopTen2017[id]; ok {
entry.OwaspTopTen2017 = rank
}
if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
entry.CweTopTwentyfive2019 = rank
}
if rank, ok := cwe.SansTopTwentyfive[id]; ok {
entry.SansTopTwentyfive = rank
}
entry.Ja = &e
} else {
logging.Log.Debugf("CWE-ID %s is not found in Japanese CWE Dict", id)
entry.Ja = &cwe.Cwe{CweID: id}
}
}
dict[id] = entry
}
r.CweDict = dict
return
}

223
detector/exploitdb.go Normal file
View File

@@ -0,0 +1,223 @@
// +build !scanner
package detector
import (
"encoding/json"
"net/http"
"time"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/parnurzeal/gorequest"
exploitdb "github.com/vulsio/go-exploitdb/db"
exploitmodels "github.com/vulsio/go-exploitdb/models"
"golang.org/x/xerrors"
)
// FillWithExploit fills exploit information that has in Exploit
func FillWithExploit(r *models.ScanResult, cnf config.ExploitConf) (nExploitCve int, err error) {
if cnf.IsFetchViaHTTP() {
var cveIDs []string
for cveID := range r.ScannedCves {
cveIDs = append(cveIDs, cveID)
}
prefix, _ := util.URLPathJoin(cnf.GetURL(), "cves")
responses, err := getCvesViaHTTP(cveIDs, prefix)
if err != nil {
return 0, err
}
for _, res := range responses {
exps := []*exploitmodels.Exploit{}
if err := json.Unmarshal([]byte(res.json), &exps); err != nil {
return 0, err
}
exploits := ConvertToModels(exps)
v, ok := r.ScannedCves[res.request.cveID]
if ok {
v.Exploits = exploits
}
r.ScannedCves[res.request.cveID] = v
nExploitCve++
}
} else {
driver, locked, err := newExploitDB(&cnf)
if locked {
return 0, xerrors.Errorf("SQLite3 is locked: %s", cnf.GetSQLite3Path())
} else if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
for cveID, vuln := range r.ScannedCves {
if cveID == "" {
continue
}
es := driver.GetExploitByCveID(cveID)
if len(es) == 0 {
continue
}
exploits := ConvertToModels(es)
vuln.Exploits = exploits
r.ScannedCves[cveID] = vuln
nExploitCve++
}
}
return nExploitCve, nil
}
// ConvertToModels converts gost model to vuls model
func ConvertToModels(es []*exploitmodels.Exploit) (exploits []models.Exploit) {
for _, e := range es {
var documentURL, shellURL *string
if e.OffensiveSecurity != nil {
os := e.OffensiveSecurity
if os.Document != nil {
documentURL = &os.Document.DocumentURL
}
if os.ShellCode != nil {
shellURL = &os.ShellCode.ShellCodeURL
}
}
exploit := models.Exploit{
ExploitType: e.ExploitType,
ID: e.ExploitUniqueID,
URL: e.URL,
Description: e.Description,
DocumentURL: documentURL,
ShellCodeURL: shellURL,
}
exploits = append(exploits, exploit)
}
return exploits
}
type exploitResponse struct {
request request
json string
}
func getCvesViaHTTP(cveIDs []string, urlPrefix string) (
responses []exploitResponse, err error) {
nReq := len(cveIDs)
reqChan := make(chan request, nReq)
resChan := make(chan exploitResponse, nReq)
errChan := make(chan error, nReq)
defer close(reqChan)
defer close(resChan)
defer close(errChan)
go func() {
for _, cveID := range cveIDs {
reqChan <- request{
cveID: cveID,
}
}
}()
concurrency := 10
tasks := util.GenWorkers(concurrency)
for i := 0; i < nReq; i++ {
tasks <- func() {
select {
case req := <-reqChan:
url, err := util.URLPathJoin(
urlPrefix,
req.cveID,
)
if err != nil {
errChan <- err
} else {
logging.Log.Debugf("HTTP Request to %s", url)
httpGet(url, req, resChan, errChan)
}
}
}
}
timeout := time.After(2 * 60 * time.Second)
var errs []error
for i := 0; i < nReq; i++ {
select {
case res := <-resChan:
responses = append(responses, res)
case err := <-errChan:
errs = append(errs, err)
case <-timeout:
return nil, xerrors.New("Timeout Fetching OVAL")
}
}
if len(errs) != 0 {
return nil, xerrors.Errorf("Failed to fetch OVAL. err: %w", errs)
}
return
}
type request struct {
osMajorVersion string
packName string
isSrcPack bool
cveID string
}
func httpGet(url string, req request, resChan chan<- exploitResponse, errChan chan<- error) {
var body string
var errs []error
var resp *http.Response
count, retryMax := 0, 3
f := func() (err error) {
// resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
resp, body, errs = gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
count++
if count == retryMax {
return nil
}
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %+v", url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
logging.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %+v", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
errChan <- xerrors.Errorf("HTTP Error %w", err)
return
}
if count == retryMax {
errChan <- xerrors.New("Retry count exceeded")
return
}
resChan <- exploitResponse{
request: req,
json: body,
}
}
func newExploitDB(cnf config.VulnDictInterface) (driver exploitdb.DB, locked bool, err error) {
if cnf.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.GetURL()
if cnf.GetType() == "sqlite3" {
path = cnf.GetSQLite3Path()
}
if driver, locked, err = exploitdb.NewDB(cnf.GetType(), path, cnf.GetDebugSQL()); err != nil {
if locked {
return nil, true, xerrors.Errorf("exploitDB is locked. err: %w", err)
}
return nil, false, err
}
return driver, false, nil
}

View File

@@ -1,4 +1,4 @@
package github
package detector
import (
"bytes"
@@ -9,7 +9,6 @@ import (
"net/http"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/errof"
"github.com/future-architect/vuls/models"
"golang.org/x/oauth2"
@@ -17,11 +16,11 @@ import (
// DetectGitHubSecurityAlerts access to owner/repo on GitHub and fetch security alerts of the repository via GitHub API v4 GraphQL and then set to the given ScanResult.
// https://help.github.com/articles/about-security-alerts-for-vulnerable-dependencies/
//TODO move to report
func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) (nCVEs int, err error) {
func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string, ignoreDismissed bool) (nCVEs int, err error) {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
//TODO Proxy
httpClient := oauth2.NewClient(context.Background(), src)
// TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint
@@ -32,10 +31,12 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string)
for {
jsonStr := fmt.Sprintf(jsonfmt, owner, repo, 100, after)
req, err := http.NewRequest("POST",
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.github.com/graphql",
bytes.NewBuffer([]byte(jsonStr)),
)
defer cancel()
if err != nil {
return 0, err
}
@@ -71,7 +72,7 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string)
}
for _, v := range alerts.Data.Repository.VulnerabilityAlerts.Edges {
if config.Conf.IgnoreGitHubDismissed && v.Node.DismissReason != "" {
if ignoreDismissed && v.Node.DismissReason != "" {
continue
}
@@ -101,20 +102,39 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string)
cveIDs = other
}
refs := []models.Reference{}
for _, r := range v.Node.SecurityAdvisory.References {
refs = append(refs, models.Reference{Link: r.URL})
}
for _, cveID := range cveIDs {
cveContent := models.CveContent{
Type: models.GitHub,
CveID: cveID,
Title: v.Node.SecurityAdvisory.Summary,
Summary: v.Node.SecurityAdvisory.Description,
Cvss2Severity: v.Node.SecurityVulnerability.Severity,
Cvss3Severity: v.Node.SecurityVulnerability.Severity,
SourceLink: v.Node.SecurityAdvisory.Permalink,
References: refs,
Published: v.Node.SecurityAdvisory.PublishedAt,
LastModified: v.Node.SecurityAdvisory.UpdatedAt,
}
if val, ok := r.ScannedCves[cveID]; ok {
val.GitHubSecurityAlerts = val.GitHubSecurityAlerts.Add(m)
val.CveContents[models.GitHub] = cveContent
r.ScannedCves[cveID] = val
nCVEs++
} else {
v := models.VulnInfo{
CveID: cveID,
Confidences: models.Confidences{models.GitHubMatch},
GitHubSecurityAlerts: models.GitHubSecurityAlerts{m},
CveContents: models.NewCveContents(cveContent),
}
r.ScannedCves[cveID] = v
nCVEs++
}
nCVEs++
}
}
if !alerts.Data.Repository.VulnerabilityAlerts.PageInfo.HasNextPage {

View File

@@ -1,4 +1,4 @@
package libmanager
package detector
import (
"context"
@@ -12,13 +12,12 @@ import (
"golang.org/x/xerrors"
"k8s.io/utils/clock"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// DetectLibsCves fills LibraryScanner information
func DetectLibsCves(r *models.ScanResult) (err error) {
func DetectLibsCves(r *models.ScanResult, cacheDir string, noProgress bool) (err error) {
totalCnt := 0
if len(r.LibraryScanners) == 0 {
return
@@ -30,12 +29,12 @@ func DetectLibsCves(r *models.ScanResult) (err error) {
return err
}
util.Log.Info("Updating library db...")
if err := downloadDB(config.Version, config.Conf.TrivyCacheDBDir, config.Conf.NoProgress, false, false); err != nil {
logging.Log.Info("Updating library db...")
if err := downloadDB("", cacheDir, noProgress, false, false); err != nil {
return err
}
if err := db2.Init(config.Conf.TrivyCacheDBDir); err != nil {
if err := db2.Init(cacheDir); err != nil {
return err
}
defer db2.Close()
@@ -57,7 +56,7 @@ func DetectLibsCves(r *models.ScanResult) (err error) {
totalCnt += len(vinfos)
}
util.Log.Infof("%s: %d CVEs are detected with Library",
logging.Log.Infof("%s: %d CVEs are detected with Library",
r.FormatServerName(), totalCnt)
return nil
@@ -72,8 +71,8 @@ func downloadDB(appVersion, cacheDir string, quiet, light, skipUpdate bool) erro
}
if needsUpdate {
util.Log.Info("Need to update DB")
util.Log.Info("Downloading DB...")
logging.Log.Info("Need to update DB")
logging.Log.Info("Downloading DB...")
if err := client.Download(ctx, cacheDir, light); err != nil {
return xerrors.Errorf("failed to download vulnerability DB: %w", err)
}
@@ -106,7 +105,7 @@ func showDBInfo(cacheDir string) error {
if err != nil {
return xerrors.Errorf("something wrong with DB: %w", err)
}
util.Log.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s",
logging.Log.Debugf("DB Schema: %d, Type: %d, UpdatedAt: %s, NextUpdate: %s",
metadata.Version, metadata.Type, metadata.UpdatedAt, metadata.NextUpdate)
return nil
}

81
detector/msf.go Normal file
View File

@@ -0,0 +1,81 @@
// +build !scanner
package detector
import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
metasploitdb "github.com/takuzoo3868/go-msfdb/db"
metasploitmodels "github.com/takuzoo3868/go-msfdb/models"
"golang.org/x/xerrors"
)
// FillWithMetasploit fills metasploit module information that has in module
func FillWithMetasploit(r *models.ScanResult, cnf config.MetasploitConf) (nMetasploitCve int, err error) {
driver, locked, err := newMetasploitDB(&cnf)
if locked {
return 0, xerrors.Errorf("SQLite3 is locked: %s", cnf.GetSQLite3Path())
} else if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v")
}
}()
for cveID, vuln := range r.ScannedCves {
if cveID == "" {
continue
}
ms := driver.GetModuleByCveID(cveID)
if len(ms) == 0 {
continue
}
modules := ConvertToModelsMsf(ms)
vuln.Metasploits = modules
r.ScannedCves[cveID] = vuln
nMetasploitCve++
}
return nMetasploitCve, nil
}
// ConvertToModelsMsf converts gost model to vuls model
func ConvertToModelsMsf(ms []*metasploitmodels.Metasploit) (modules []models.Metasploit) {
for _, m := range ms {
var links []string
if 0 < len(m.References) {
for _, u := range m.References {
links = append(links, u.Link)
}
}
module := models.Metasploit{
Name: m.Name,
Title: m.Title,
Description: m.Description,
URLs: links,
}
modules = append(modules, module)
}
return modules
}
func newMetasploitDB(cnf config.VulnDictInterface) (driver metasploitdb.DB, locked bool, err error) {
if cnf.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.GetURL()
if cnf.GetType() == "sqlite3" {
path = cnf.GetSQLite3Path()
}
if driver, locked, err = metasploitdb.NewDB(cnf.GetType(), path, cnf.GetDebugSQL(), false); err != nil {
if locked {
return nil, true, xerrors.Errorf("metasploitDB is locked. err: %w", err)
}
return nil, false, err
}
return driver, false, nil
}

270
detector/util.go Normal file
View File

@@ -0,0 +1,270 @@
package detector
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"golang.org/x/xerrors"
)
func reuseScannedCves(r *models.ScanResult) bool {
switch r.Family {
case constant.FreeBSD, constant.Raspbian:
return true
}
if isTrivyResult(r) {
return true
}
return false
}
func isTrivyResult(r *models.ScanResult) bool {
_, ok := r.Optional["trivy-target"]
return ok
}
func needToRefreshCve(r models.ScanResult) bool {
for _, cve := range r.ScannedCves {
if 0 < len(cve.CveContents) {
return false
}
}
return true
}
func loadPrevious(currs models.ScanResults, resultsDir string) (prevs models.ScanResults, err error) {
dirs, err := ListValidJSONDirs(resultsDir)
if err != nil {
return
}
for _, result := range currs {
filename := result.ServerName + ".json"
if result.Container.Name != "" {
filename = fmt.Sprintf("%s@%s.json", result.Container.Name, result.ServerName)
}
for _, dir := range dirs[1:] {
path := filepath.Join(dir, filename)
r, err := loadOneServerScanResult(path)
if err != nil {
logging.Log.Debugf("%+v", err)
continue
}
if r.Family == result.Family && r.Release == result.Release {
prevs = append(prevs, *r)
logging.Log.Infof("Previous json found: %s", path)
break
} else {
logging.Log.Infof("Previous json is different family.Release: %s, pre: %s.%s cur: %s.%s",
path, r.Family, r.Release, result.Family, result.Release)
}
}
}
return prevs, nil
}
func diff(curResults, preResults models.ScanResults, isPlus, isMinus bool) (diffed models.ScanResults) {
for _, current := range curResults {
found := false
var previous models.ScanResult
for _, r := range preResults {
if current.ServerName == r.ServerName && current.Container.Name == r.Container.Name {
found = true
previous = r
break
}
}
if !found {
diffed = append(diffed, current)
continue
}
cves := models.VulnInfos{}
if isPlus {
cves = getPlusDiffCves(previous, current)
}
if isMinus {
minus := getMinusDiffCves(previous, current)
if len(cves) == 0 {
cves = minus
} else {
for k, v := range minus {
cves[k] = v
}
}
}
packages := models.Packages{}
for _, s := range cves {
for _, affected := range s.AffectedPackages {
var p models.Package
if s.DiffStatus == models.DiffPlus {
p = current.Packages[affected.Name]
} else {
p = previous.Packages[affected.Name]
}
packages[affected.Name] = p
}
}
current.ScannedCves = cves
current.Packages = packages
diffed = append(diffed, current)
}
return
}
func getPlusDiffCves(previous, current models.ScanResult) models.VulnInfos {
previousCveIDsSet := map[string]bool{}
for _, previousVulnInfo := range previous.ScannedCves {
previousCveIDsSet[previousVulnInfo.CveID] = true
}
new := models.VulnInfos{}
updated := models.VulnInfos{}
for _, v := range current.ScannedCves {
if previousCveIDsSet[v.CveID] {
if isCveInfoUpdated(v.CveID, previous, current) {
v.DiffStatus = models.DiffPlus
updated[v.CveID] = v
logging.Log.Debugf("updated: %s", v.CveID)
// TODO commented out because a bug of diff logic when multiple oval defs found for a certain CVE-ID and same updated_at
// if these OVAL defs have different affected packages, this logic detects as updated.
// This logic will be uncomented after integration with gost https://github.com/knqyf263/gost
// } else if isCveFixed(v, previous) {
// updated[v.CveID] = v
// logging.Log.Debugf("fixed: %s", v.CveID)
} else {
logging.Log.Debugf("same: %s", v.CveID)
}
} else {
logging.Log.Debugf("new: %s", v.CveID)
v.DiffStatus = models.DiffPlus
new[v.CveID] = v
}
}
if len(updated) == 0 && len(new) == 0 {
logging.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
}
for cveID, vuln := range new {
updated[cveID] = vuln
}
return updated
}
func getMinusDiffCves(previous, current models.ScanResult) models.VulnInfos {
currentCveIDsSet := map[string]bool{}
for _, currentVulnInfo := range current.ScannedCves {
currentCveIDsSet[currentVulnInfo.CveID] = true
}
clear := models.VulnInfos{}
for _, v := range previous.ScannedCves {
if !currentCveIDsSet[v.CveID] {
v.DiffStatus = models.DiffMinus
clear[v.CveID] = v
logging.Log.Debugf("clear: %s", v.CveID)
}
}
if len(clear) == 0 {
logging.Log.Infof("%s: There are %d vulnerabilities, but no difference between current result and previous one.", current.FormatServerName(), len(current.ScannedCves))
}
return clear
}
func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool {
cTypes := []models.CveContentType{
models.Nvd,
models.Jvn,
models.NewCveContentType(current.Family),
}
prevLastModified := map[models.CveContentType]time.Time{}
preVinfo, ok := previous.ScannedCves[cveID]
if !ok {
return true
}
for _, cType := range cTypes {
if content, ok := preVinfo.CveContents[cType]; ok {
prevLastModified[cType] = content.LastModified
}
}
curLastModified := map[models.CveContentType]time.Time{}
curVinfo, ok := current.ScannedCves[cveID]
if !ok {
return true
}
for _, cType := range cTypes {
if content, ok := curVinfo.CveContents[cType]; ok {
curLastModified[cType] = content.LastModified
}
}
for _, t := range cTypes {
if !curLastModified[t].Equal(prevLastModified[t]) {
logging.Log.Debugf("%s LastModified not equal: \n%s\n%s",
cveID, curLastModified[t], prevLastModified[t])
return true
}
}
return false
}
// jsonDirPattern is file name pattern of JSON directory
// 2016-11-16T10:43:28+09:00
// 2016-11-16T10:43:28Z
var jsonDirPattern = regexp.MustCompile(
`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`)
// ListValidJSONDirs returns valid json directory as array
// Returned array is sorted so that recent directories are at the head
func ListValidJSONDirs(resultsDir string) (dirs []string, err error) {
var dirInfo []os.FileInfo
if dirInfo, err = ioutil.ReadDir(resultsDir); err != nil {
err = xerrors.Errorf("Failed to read %s: %w",
config.Conf.ResultsDir, err)
return
}
for _, d := range dirInfo {
if d.IsDir() && jsonDirPattern.MatchString(d.Name()) {
jsonDir := filepath.Join(resultsDir, d.Name())
dirs = append(dirs, jsonDir)
}
}
sort.Slice(dirs, func(i, j int) bool {
return dirs[j] < dirs[i]
})
return
}
// loadOneServerScanResult read JSON data of one server
func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) {
var (
data []byte
err error
)
if data, err = ioutil.ReadFile(jsonFile); err != nil {
return nil, xerrors.Errorf("Failed to read %s: %w", jsonFile, err)
}
result := &models.ScanResult{}
if err := json.Unmarshal(data, result); err != nil {
return nil, xerrors.Errorf("Failed to parse %s: %w", jsonFile, err)
}
return result, nil
}

View File

@@ -1,6 +1,7 @@
package wordpress
package detector
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -8,8 +9,9 @@ import (
"strings"
"time"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/errof"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
version "github.com/hashicorp/go-version"
@@ -30,11 +32,10 @@ type WpCveInfos struct {
//WpCveInfo is for wpscan json
type WpCveInfo struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
// PublishedDate string `json:"published_date"`
ID string `json:"id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
VulnType string `json:"vuln_type"`
References References `json:"references"`
FixedIn string `json:"fixed_in"`
@@ -49,18 +50,18 @@ type References struct {
// DetectWordPressCves access to wpscan and fetch scurity alerts and then set to the given ScanResult.
// https://wpscan.com/
// TODO move to report
func DetectWordPressCves(r *models.ScanResult, cnf *c.WpScanConf) (int, error) {
func detectWordPressCves(r *models.ScanResult, cnf config.WpScanConf) (int, error) {
if len(r.WordPressPackages) == 0 {
return 0, nil
}
// Core
ver := strings.Replace(r.WordPressPackages.CoreVersion(), ".", "", -1)
if ver == "" {
return 0, xerrors.New("Failed to get WordPress core version")
return 0, errof.New(errof.ErrFailedToAccessWpScan,
fmt.Sprintf("Failed to get WordPress core version."))
}
url := fmt.Sprintf("https://wpscan.com/api/v3/wordpresses/%s", ver)
wpVinfos, err := wpscan(url, ver, cnf.Token)
wpVinfos, err := wpscan(url, ver, cnf.Token, true)
if err != nil {
return 0, err
}
@@ -72,7 +73,7 @@ func DetectWordPressCves(r *models.ScanResult, cnf *c.WpScanConf) (int, error) {
}
for _, p := range themes {
url := fmt.Sprintf("https://wpscan.com/api/v3/themes/%s", p.Name)
candidates, err := wpscan(url, p.Name, cnf.Token)
candidates, err := wpscan(url, p.Name, cnf.Token, false)
if err != nil {
return 0, err
}
@@ -87,7 +88,7 @@ func DetectWordPressCves(r *models.ScanResult, cnf *c.WpScanConf) (int, error) {
}
for _, p := range plugins {
url := fmt.Sprintf("https://wpscan.com/api/v3/plugins/%s", p.Name)
candidates, err := wpscan(url, p.Name, cnf.Token)
candidates, err := wpscan(url, p.Name, cnf.Token, false)
if err != nil {
return 0, err
}
@@ -109,14 +110,16 @@ func DetectWordPressCves(r *models.ScanResult, cnf *c.WpScanConf) (int, error) {
return len(wpVinfos), nil
}
func wpscan(url, name, token string) (vinfos []models.VulnInfo, err error) {
func wpscan(url, name, token string, isCore bool) (vinfos []models.VulnInfo, err error) {
body, err := httpRequest(url, token)
if err != nil {
return nil, errof.New(errof.ErrFailedToAccessWpScan,
fmt.Sprintf("Failed to access to wpscan.comm. body: %s, err: %s", string(body), err))
return nil, err
}
if body == "" {
util.Log.Debugf("wpscan.com response body is empty. URL: %s", url)
logging.Log.Debugf("wpscan.com response body is empty. URL: %s", url)
}
if isCore {
name = "core"
}
return convertToVinfos(name, body)
}
@@ -126,17 +129,17 @@ func detect(installed models.WpPackage, candidates []models.VulnInfo) (vulns []m
for _, fixstat := range v.WpPackageFixStats {
ok, err := match(installed.Version, fixstat.FixedIn)
if err != nil {
util.Log.Errorf("Failed to compare versions %s installed: %s, fixedIn: %s, v: %+v",
logging.Log.Warnf("Failed to compare versions %s installed: %s, fixedIn: %s, v: %+v",
installed.Name, installed.Version, fixstat.FixedIn, v)
// continue scanning
continue
}
if ok {
vulns = append(vulns, v)
util.Log.Debugf("Affected: %s installed: %s, fixedIn: %s",
logging.Log.Debugf("Affected: %s installed: %s, fixedIn: %s",
installed.Name, installed.Version, fixstat.FixedIn)
} else {
util.Log.Debugf("Not affected: %s : %s, fixedIn: %s",
logging.Log.Debugf("Not affected: %s : %s, fixedIn: %s",
installed.Name, installed.Version, fixstat.FixedIn)
}
}
@@ -196,10 +199,12 @@ func extractToVulnInfos(pkgName string, cves []WpCveInfo) (vinfos []models.VulnI
CveID: cveID,
CveContents: models.NewCveContents(
models.CveContent{
Type: models.WpScan,
CveID: cveID,
Title: vulnerability.Title,
References: refs,
Type: models.WpScan,
CveID: cveID,
Title: vulnerability.Title,
References: refs,
Published: vulnerability.CreatedAt,
LastModified: vulnerability.UpdatedAt,
},
),
VulnType: vulnerability.VulnType,
@@ -217,21 +222,27 @@ func extractToVulnInfos(pkgName string, cves []WpCveInfo) (vinfos []models.VulnI
}
func httpRequest(url, token string) (string, error) {
retry := 1
util.Log.Debugf("%s", url)
req, err := http.NewRequest("GET", url, nil)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
defer cancel()
if err != nil {
return "", errof.New(errof.ErrFailedToAccessWpScan,
fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
}
req.Header.Set("Authorization", fmt.Sprintf("Token token=%s", token))
client, err := util.GetHTTPClient(config.Conf.HTTPProxy)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Token token=%s", token))
loop:
resp, err := new(http.Client).Do(req)
resp, err := client.Do(req)
if err != nil {
return "", err
return "", errof.New(errof.ErrFailedToAccessWpScan,
fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
return "", errof.New(errof.ErrFailedToAccessWpScan,
fmt.Sprintf("Failed to access to wpscan.com. err: %s", err))
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
@@ -239,14 +250,13 @@ loop:
} else if resp.StatusCode == 404 {
// This package is not in wpscan
return "", nil
} else if resp.StatusCode == 429 && retry <= 3 {
// 429 Too Many Requests
util.Log.Debugf("sleep %d min(s): %s", retry, resp.Status)
time.Sleep(time.Duration(retry) * time.Minute)
retry++
goto loop
} else if resp.StatusCode == 429 {
return "", errof.New(errof.ErrWpScanAPILimitExceeded,
fmt.Sprintf("wpscan.com API limit exceeded: %+v", resp.Status))
} else {
logging.Log.Warnf("wpscan.com unknown status code: %+v", resp.Status)
return "", nil
}
return "", err
}
func removeInactives(pkgs models.WordPressPackages) (removed models.WordPressPackages) {

View File

@@ -1,4 +1,4 @@
package wordpress
package detector
import (
"reflect"

View File

@@ -19,6 +19,9 @@ var (
// ErrFailedToAccessWpScan is error of wpscan.com api access
ErrFailedToAccessWpScan ErrorCode = "ErrFailedToAccessWpScan"
// ErrWpScanAPILimitExceeded is error of wpscan.com api limit exceeded
ErrWpScanAPILimitExceeded ErrorCode = "ErrWpScanAPILimitExceeded"
)
// New :

View File

@@ -1,119 +0,0 @@
// +build !scanner
package exploit
import (
"encoding/json"
"fmt"
"net/http"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/mozqnet/go-exploitdb/db"
exploitmodels "github.com/mozqnet/go-exploitdb/models"
"github.com/parnurzeal/gorequest"
"golang.org/x/xerrors"
)
// FillWithExploit fills exploit information that has in Exploit
func FillWithExploit(driver db.DB, r *models.ScanResult) (nExploitCve int, err error) {
if cnf.Conf.Exploit.IsFetchViaHTTP() {
var cveIDs []string
for cveID := range r.ScannedCves {
cveIDs = append(cveIDs, cveID)
}
prefix, _ := util.URLPathJoin(cnf.Conf.Exploit.URL, "cves")
responses, err := getCvesViaHTTP(cveIDs, prefix)
if err != nil {
return 0, err
}
for _, res := range responses {
exps := []*exploitmodels.Exploit{}
if err := json.Unmarshal([]byte(res.json), &exps); err != nil {
return 0, err
}
exploits := ConvertToModels(exps)
v, ok := r.ScannedCves[res.request.cveID]
if ok {
v.Exploits = exploits
}
r.ScannedCves[res.request.cveID] = v
nExploitCve++
}
} else {
if driver == nil {
return 0, nil
}
for cveID, vuln := range r.ScannedCves {
if cveID == "" {
continue
}
es := driver.GetExploitByCveID(cveID)
if len(es) == 0 {
continue
}
exploits := ConvertToModels(es)
vuln.Exploits = exploits
r.ScannedCves[cveID] = vuln
nExploitCve++
}
}
return nExploitCve, nil
}
// ConvertToModels converts gost model to vuls model
func ConvertToModels(es []*exploitmodels.Exploit) (exploits []models.Exploit) {
for _, e := range es {
var documentURL, shellURL *string
if e.OffensiveSecurity != nil {
os := e.OffensiveSecurity
if os.Document != nil {
documentURL = &os.Document.DocumentURL
}
if os.ShellCode != nil {
shellURL = &os.ShellCode.ShellCodeURL
}
}
exploit := models.Exploit{
ExploitType: e.ExploitType,
ID: e.ExploitUniqueID,
URL: e.URL,
Description: e.Description,
DocumentURL: documentURL,
ShellCodeURL: shellURL,
}
exploits = append(exploits, exploit)
}
return exploits
}
// CheckHTTPHealth do health check
func CheckHTTPHealth() error {
if !cnf.Conf.Exploit.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", cnf.Conf.Exploit.URL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().Get(url).End()
// resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to connect to exploit server. url: %s, errs: %w", url, errs)
}
return nil
}
// CheckIfExploitFetched checks if oval entries are in DB by family, release.
func CheckIfExploitFetched(driver db.DB, osFamily string) (fetched bool, err error) {
//TODO
return true, nil
}
// CheckIfExploitFresh checks if oval entries are fresh enough
func CheckIfExploitFresh(driver db.DB, osFamily string) (ok bool, err error) {
//TODO
return true, nil
}

View File

@@ -1,115 +0,0 @@
package exploit
import (
"net/http"
"time"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/util"
"github.com/parnurzeal/gorequest"
"golang.org/x/xerrors"
)
type response struct {
request request
json string
}
func getCvesViaHTTP(cveIDs []string, urlPrefix string) (
responses []response, err error) {
nReq := len(cveIDs)
reqChan := make(chan request, nReq)
resChan := make(chan response, nReq)
errChan := make(chan error, nReq)
defer close(reqChan)
defer close(resChan)
defer close(errChan)
go func() {
for _, cveID := range cveIDs {
reqChan <- request{
cveID: cveID,
}
}
}()
concurrency := 10
tasks := util.GenWorkers(concurrency)
for i := 0; i < nReq; i++ {
tasks <- func() {
select {
case req := <-reqChan:
url, err := util.URLPathJoin(
urlPrefix,
req.cveID,
)
if err != nil {
errChan <- err
} else {
util.Log.Debugf("HTTP Request to %s", url)
httpGet(url, req, resChan, errChan)
}
}
}
}
timeout := time.After(2 * 60 * time.Second)
var errs []error
for i := 0; i < nReq; i++ {
select {
case res := <-resChan:
responses = append(responses, res)
case err := <-errChan:
errs = append(errs, err)
case <-timeout:
return nil, xerrors.New("Timeout Fetching OVAL")
}
}
if len(errs) != 0 {
return nil, xerrors.Errorf("Failed to fetch OVAL. err: %w", errs)
}
return
}
type request struct {
osMajorVersion string
packName string
isSrcPack bool
cveID string
}
func httpGet(url string, req request, resChan chan<- response, errChan chan<- error) {
var body string
var errs []error
var resp *http.Response
count, retryMax := 0, 3
f := func() (err error) {
// resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
resp, body, errs = gorequest.New().Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
count++
if count == retryMax {
return nil
}
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %w", url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
errChan <- xerrors.Errorf("HTTP Error %w", err)
return
}
if count == retryMax {
errChan <- xerrors.New("Retry count exceeded")
return
}
resChan <- response{
request: req,
json: body,
}
}

71
go.mod
View File

@@ -1,38 +1,25 @@
module github.com/future-architect/vuls
go 1.15
replace (
gopkg.in/mattn/go-colorable.v0 => github.com/mattn/go-colorable v0.1.0
gopkg.in/mattn/go-isatty.v0 => github.com/mattn/go-isatty v0.0.6
)
go 1.16
require (
github.com/Azure/azure-sdk-for-go v50.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.16 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.10 // indirect
github.com/Azure/azure-sdk-for-go v50.2.0+incompatible
github.com/BurntSushi/toml v0.3.1
github.com/aquasecurity/fanal v0.0.0-20210111044704-9cb28297c870
github.com/aquasecurity/go-dep-parser v0.0.0-20210110062711-bd24476c042d // indirect
github.com/aquasecurity/trivy v0.15.0
github.com/aquasecurity/trivy-db v0.0.0-20210111152553-7d4d1aa5f0d4
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/aws/aws-sdk-go v1.36.25
github.com/aquasecurity/fanal v0.0.0-20210119051230-28c249da7cfd
github.com/aquasecurity/trivy v0.16.0
github.com/aquasecurity/trivy-db v0.0.0-20210121143430-2a5c54036a86
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/aws/aws-sdk-go v1.36.31
github.com/boltdb/bolt v1.3.1
github.com/caarlos0/env/v6 v6.4.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.14.0
github.com/go-redis/redis/v8 v8.4.8 // indirect
github.com/goccy/go-yaml v1.8.4 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/google/subcommands v1.2.0
github.com/google/wire v0.4.0 // indirect
github.com/gosuri/uitable v0.0.4
github.com/grokify/html-strip-tags-go v0.0.1 // indirect
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/go-version v1.3.0
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
github.com/jesseduffield/gocui v0.3.0
github.com/k0kubun/pp v3.0.1+incompatible
@@ -40,45 +27,37 @@ require (
github.com/knqyf263/go-cpe v0.0.0-20201213041631-54f6ab28673f
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936
github.com/knqyf263/gost v0.1.7
github.com/kotakanbe/go-cve-dictionary v0.5.6
github.com/knqyf263/gost v0.1.10
github.com/kotakanbe/go-cve-dictionary v0.5.12
github.com/kotakanbe/go-pingscanner v0.1.0
github.com/kotakanbe/goval-dictionary v0.3.0
github.com/kotakanbe/goval-dictionary v0.3.3
github.com/kotakanbe/logrus-prefixed-formatter v0.0.0-20180123152602-928f7356cb96
github.com/lib/pq v1.10.0 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mozqnet/go-exploitdb v0.1.2
github.com/nlopes/slack v0.6.0
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/parnurzeal/gorequest v0.2.16
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sirupsen/logrus v1.7.0
github.com/spf13/afero v1.5.1
github.com/spf13/afero v1.6.0
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/cobra v1.1.3
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1 // indirect
github.com/takuzoo3868/go-msfdb v0.1.3
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
github.com/takuzoo3868/go-msfdb v0.1.5
github.com/vulsio/go-exploitdb v0.1.7
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
golang.org/x/oauth2 v0.0.0-20210112200429-01de73cf58bd
golang.org/x/sys v0.0.0-20210113000019-eaf3bda374d2 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 // indirect
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
golang.org/x/oauth2 v0.0.0-20210125201302-af13f521f196
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
honnef.co/go/tools v0.1.0 // indirect
k8s.io/utils v0.0.0-20210111153108-fddb29f9d009
)

416
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +0,0 @@
// +build !scanner
package gost
import (
"fmt"
"net/http"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/knqyf263/gost/db"
"github.com/parnurzeal/gorequest"
"golang.org/x/xerrors"
)
// Base is a base struct
type Base struct {
}
// FillCVEsWithRedHat fills cve information that has in Gost
func (b Base) FillCVEsWithRedHat(driver db.DB, r *models.ScanResult) error {
return RedHat{}.fillCvesWithRedHatAPI(driver, r)
}
// CheckHTTPHealth do health check
func (b Base) CheckHTTPHealth() error {
if !cnf.Conf.Gost.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", cnf.Conf.Gost.URL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().Get(url).End()
// resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to connect to gost server. url: %s, errs: %w", url, errs)
}
return nil
}
// CheckIfGostFetched checks if oval entries are in DB by family, release.
func (b Base) CheckIfGostFetched(driver db.DB, osFamily string) (fetched bool, err error) {
//TODO
return true, nil
}
// CheckIfGostFresh checks if oval entries are fresh enough
func (b Base) CheckIfGostFresh(driver db.DB, osFamily string) (ok bool, err error) {
//TODO
return true, nil
}

View File

@@ -5,10 +5,10 @@ package gost
import (
"encoding/json"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/knqyf263/gost/db"
gostmodels "github.com/knqyf263/gost/models"
)
@@ -33,10 +33,10 @@ func (deb Debian) supported(major string) bool {
}
// DetectUnfixed fills cve information that has in Gost
func (deb Debian) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (nCVEs int, err error) {
func (deb Debian) DetectUnfixed(r *models.ScanResult, _ bool) (nCVEs int, err error) {
if !deb.supported(major(r.Release)) {
// only logging
util.Log.Warnf("Debian %s is not supported yet", r.Release)
logging.Log.Warnf("Debian %s is not supported yet", r.Release)
return 0, nil
}
@@ -56,15 +56,15 @@ func (deb Debian) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (nCV
// Debian Security Tracker does not support Package for Raspbian, so skip it.
var scanResult models.ScanResult
if r.Family != config.Raspbian {
if r.Family != constant.Raspbian {
scanResult = *r
} else {
scanResult = r.RemoveRaspbianPackFromResult()
}
packCvesList := []packCves{}
if config.Conf.Gost.IsFetchViaHTTP() {
url, _ := util.URLPathJoin(config.Conf.Gost.URL, "debian", major(scanResult.Release), "pkgs")
if deb.DBDriver.Cnf.IsFetchViaHTTP() {
url, _ := util.URLPathJoin(deb.DBDriver.Cnf.GetURL(), "debian", major(scanResult.Release), "pkgs")
responses, err := getAllUnfixedCvesViaHTTP(r, url)
if err != nil {
return 0, err
@@ -86,11 +86,11 @@ func (deb Debian) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (nCV
})
}
} else {
if driver == nil {
if deb.DBDriver.DB == nil {
return 0, nil
}
for _, pack := range scanResult.Packages {
cveDebs := driver.GetUnfixedCvesDebian(major(scanResult.Release), pack.Name)
cveDebs := deb.DBDriver.DB.GetUnfixedCvesDebian(major(scanResult.Release), pack.Name)
cves := []models.CveContent{}
for _, cveDeb := range cveDebs {
cves = append(cves, *deb.ConvertToModel(&cveDeb))
@@ -104,7 +104,7 @@ func (deb Debian) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (nCV
// SrcPack
for _, pack := range scanResult.SrcPackages {
cveDebs := driver.GetUnfixedCvesDebian(major(scanResult.Release), pack.Name)
cveDebs := deb.DBDriver.DB.GetUnfixedCvesDebian(major(scanResult.Release), pack.Name)
cves := []models.CveContent{}
for _, cveDeb := range cveDebs {
cves = append(cves, *deb.ConvertToModel(&cveDeb))

View File

@@ -3,33 +3,84 @@
package gost
import (
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/knqyf263/gost/db"
"golang.org/x/xerrors"
"github.com/future-architect/vuls/constant"
)
// DBDriver is a DB Driver
type DBDriver struct {
DB db.DB
Cnf config.VulnDictInterface
}
// Client is the interface of OVAL client.
type Client interface {
DetectUnfixed(db.DB, *models.ScanResult, bool) (int, error)
FillCVEsWithRedHat(db.DB, *models.ScanResult) error
DetectUnfixed(*models.ScanResult, bool) (int, error)
}
//TODO implement
// CheckHTTPHealth() error
// CheckIfGostFetched checks if Gost entries are fetched
// CheckIfGostFetched(db.DB, string, string) (bool, error)
// CheckIfGostFresh(db.DB, string, string) (bool, error)
// Base is a base struct
type Base struct {
DBDriver DBDriver
}
// FillCVEsWithRedHat fills CVE detailed with Red Hat Security
func FillCVEsWithRedHat(r *models.ScanResult, cnf config.GostConf) error {
db, locked, err := newGostDB(cnf)
if locked {
return xerrors.Errorf("SQLite3 is locked: %s", cnf.GetSQLite3Path())
} else if err != nil {
return err
}
defer func() {
if err := db.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
return RedHat{Base{DBDriver{DB: db, Cnf: &cnf}}}.fillCvesWithRedHatAPI(r)
}
// NewClient make Client by family
func NewClient(family string) Client {
func NewClient(cnf config.GostConf, family string) (Client, error) {
db, locked, err := newGostDB(cnf)
if locked {
return nil, xerrors.Errorf("SQLite3 is locked: %s", cnf.GetSQLite3Path())
} else if err != nil {
return nil, err
}
driver := DBDriver{DB: db, Cnf: &cnf}
switch family {
case cnf.RedHat, cnf.CentOS:
return RedHat{}
case cnf.Debian, cnf.Raspbian:
return Debian{}
case cnf.Windows:
return Microsoft{}
case constant.RedHat, constant.CentOS:
return RedHat{Base{DBDriver: driver}}, nil
case constant.Debian, constant.Raspbian:
return Debian{Base{DBDriver: driver}}, nil
case constant.Windows:
return Microsoft{Base{DBDriver: driver}}, nil
default:
return Pseudo{}
return Pseudo{}, nil
}
}
// NewGostDB returns db client for Gost
func newGostDB(cnf config.GostConf) (driver db.DB, locked bool, err error) {
if cnf.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.GetURL()
if cnf.GetType() == "sqlite3" {
path = cnf.GetSQLite3Path()
}
if driver, locked, err = db.NewDB(cnf.GetType(), path, cnf.GetDebugSQL()); err != nil {
if locked {
return nil, true, xerrors.Errorf("gostDB is locked. err: %w", err)
}
return nil, false, err
}
return driver, false, nil
}

View File

@@ -6,7 +6,6 @@ import (
"strings"
"github.com/future-architect/vuls/models"
"github.com/knqyf263/gost/db"
gostmodels "github.com/knqyf263/gost/models"
)
@@ -16,15 +15,15 @@ type Microsoft struct {
}
// DetectUnfixed fills cve information that has in Gost
func (ms Microsoft) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (nCVEs int, err error) {
if driver == nil {
func (ms Microsoft) DetectUnfixed(r *models.ScanResult, _ bool) (nCVEs int, err error) {
if ms.DBDriver.DB == nil {
return 0, nil
}
cveIDs := []string{}
for cveID := range r.ScannedCves {
cveIDs = append(cveIDs, cveID)
}
for cveID, msCve := range driver.GetMicrosoftMulti(cveIDs) {
for cveID, msCve := range ms.DBDriver.DB.GetMicrosoftMulti(cveIDs) {
if _, ok := r.ScannedCves[cveID]; !ok {
continue
}
@@ -70,11 +69,10 @@ func (ms Microsoft) ConvertToModel(cve *gostmodels.MicrosoftCVE) (*models.CveCon
option := map[string]string{}
if 0 < len(cve.ExploitStatus) {
// TODO: CVE-2020-0739
// "exploit_status": "Publicly Disclosed:No;Exploited:No;Latest Software Release:Exploitation Less Likely;Older Software Release:Exploitation Less Likely;DOS:N/A",
option["exploit"] = cve.ExploitStatus
}
if 0 < len(cve.Workaround) {
option["workaround"] = cve.Workaround
}
kbids := []string{}
for _, kbid := range cve.KBIDs {
kbids = append(kbids, kbid.KBID)
@@ -86,13 +84,18 @@ func (ms Microsoft) ConvertToModel(cve *gostmodels.MicrosoftCVE) (*models.CveCon
vendorURL := "https://msrc.microsoft.com/update-guide/vulnerability/" + cve.CveID
mitigations := []models.Mitigation{}
if cve.Mitigation != "" {
mitigations = []models.Mitigation{
{
CveContentType: models.Microsoft,
Mitigation: cve.Mitigation,
URL: vendorURL,
},
}
mitigations = append(mitigations, models.Mitigation{
CveContentType: models.Microsoft,
Mitigation: cve.Mitigation,
URL: vendorURL,
})
}
if cve.Workaround != "" {
mitigations = append(mitigations, models.Mitigation{
CveContentType: models.Microsoft,
Mitigation: cve.Workaround,
URL: vendorURL,
})
}
return &models.CveContent{

View File

@@ -4,7 +4,6 @@ package gost
import (
"github.com/future-architect/vuls/models"
"github.com/knqyf263/gost/db"
)
// Pseudo is Gost client except for RedHat family and Debian
@@ -13,6 +12,6 @@ type Pseudo struct {
}
// DetectUnfixed fills cve information that has in Gost
func (pse Pseudo) DetectUnfixed(driver db.DB, r *models.ScanResult, _ bool) (int, error) {
func (pse Pseudo) DetectUnfixed(r *models.ScanResult, _ bool) (int, error) {
return 0, nil
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/knqyf263/gost/db"
gostmodels "github.com/knqyf263/gost/models"
)
@@ -20,14 +19,9 @@ type RedHat struct {
}
// DetectUnfixed fills cve information that has in Gost
func (red RedHat) DetectUnfixed(driver db.DB, r *models.ScanResult, ignoreWillNotFix bool) (nCVEs int, err error) {
return red.detectUnfixed(driver, r, ignoreWillNotFix)
}
func (red RedHat) detectUnfixed(driver db.DB, r *models.ScanResult, ignoreWillNotFix bool) (nCVEs int, err error) {
if config.Conf.Gost.IsFetchViaHTTP() {
prefix, _ := util.URLPathJoin(config.Conf.Gost.URL,
"redhat", major(r.Release), "pkgs")
func (red RedHat) DetectUnfixed(r *models.ScanResult, ignoreWillNotFix bool) (nCVEs int, err error) {
if red.DBDriver.Cnf.IsFetchViaHTTP() {
prefix, _ := util.URLPathJoin(red.DBDriver.Cnf.GetURL(), "redhat", major(r.Release), "pkgs")
responses, err := getAllUnfixedCvesViaHTTP(r, prefix)
if err != nil {
return 0, err
@@ -45,12 +39,12 @@ func (red RedHat) detectUnfixed(driver db.DB, r *models.ScanResult, ignoreWillNo
}
}
} else {
if driver == nil {
if red.DBDriver.DB == nil {
return 0, nil
}
for _, pack := range r.Packages {
// CVE-ID: RedhatCVE
cves := driver.GetUnfixedCvesRedhat(major(r.Release), pack.Name, ignoreWillNotFix)
cves := red.DBDriver.DB.GetUnfixedCvesRedhat(major(r.Release), pack.Name, ignoreWillNotFix)
for _, cve := range cves {
if newly := red.setUnfixedCveToScanResult(&cve, r); newly {
nCVEs++
@@ -61,7 +55,7 @@ func (red RedHat) detectUnfixed(driver db.DB, r *models.ScanResult, ignoreWillNo
return nCVEs, nil
}
func (red RedHat) fillCvesWithRedHatAPI(driver db.DB, r *models.ScanResult) error {
func (red RedHat) fillCvesWithRedHatAPI(r *models.ScanResult) error {
cveIDs := []string{}
for cveID, vuln := range r.ScannedCves {
if _, ok := vuln.CveContents[models.RedHatAPI]; ok {
@@ -70,9 +64,8 @@ func (red RedHat) fillCvesWithRedHatAPI(driver db.DB, r *models.ScanResult) erro
cveIDs = append(cveIDs, cveID)
}
if config.Conf.Gost.IsFetchViaHTTP() {
prefix, _ := util.URLPathJoin(config.Conf.Gost.URL,
"redhat", "cves")
if red.DBDriver.Cnf.IsFetchViaHTTP() {
prefix, _ := util.URLPathJoin(config.Conf.Gost.URL, "redhat", "cves")
responses, err := getCvesViaHTTP(cveIDs, prefix)
if err != nil {
return err
@@ -88,10 +81,10 @@ func (red RedHat) fillCvesWithRedHatAPI(driver db.DB, r *models.ScanResult) erro
red.setFixedCveToScanResult(&redCve, r)
}
} else {
if driver == nil {
if red.DBDriver.DB == nil {
return nil
}
for _, redCve := range driver.GetRedhatMulti(cveIDs) {
for _, redCve := range red.DBDriver.DB.GetRedhatMulti(cveIDs) {
if len(redCve.Name) == 0 {
continue
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/parnurzeal/gorequest"
@@ -48,7 +49,7 @@ func getCvesViaHTTP(cveIDs []string, urlPrefix string) (
if err != nil {
errChan <- err
} else {
util.Log.Debugf("HTTP Request to %s", url)
logging.Log.Debugf("HTTP Request to %s", url)
httpGet(url, req, resChan, errChan)
}
}
@@ -122,7 +123,7 @@ func getAllUnfixedCvesViaHTTP(r *models.ScanResult, urlPrefix string) (
if err != nil {
errChan <- err
} else {
util.Log.Debugf("HTTP Request to %s", url)
logging.Log.Debugf("HTTP Request to %s", url)
httpGet(url, req, resChan, errChan)
}
}
@@ -154,18 +155,18 @@ func httpGet(url string, req request, resChan chan<- response, errChan chan<- er
count, retryMax := 0, 3
f := func() (err error) {
// resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
resp, body, errs = gorequest.New().Get(url).End()
resp, body, errs = gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
count++
if count == retryMax {
return nil
}
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %w", url, resp, errs)
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %+v", url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err)
logging.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %+v", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {

6534
integration/data/amazon_2.json Executable file

File diff suppressed because it is too large Load Diff

5634
integration/data/centos_7.json Executable file

File diff suppressed because it is too large Load Diff

5631
integration/data/debian_10.json Executable file

File diff suppressed because it is too large Load Diff

131
integration/data/rails.json Normal file
View File

@@ -0,0 +1,131 @@
{
"jsonVersion": 4,
"lang": "",
"serverUUID": "",
"serverName": "rails",
"family": "pseudo",
"release": "",
"container": {
"containerID": "",
"name": "",
"image": "",
"type": "",
"uuid": ""
},
"platform": {
"name": "other",
"instanceID": ""
},
"scannedAt": "2021-03-31T12:22:26.428630183+09:00",
"scanMode": "fast mode",
"scannedVersion": "v0.15.9",
"scannedRevision": "build-20210331_121257_1a58c94",
"scannedBy": "dev",
"scannedVia": "pseudo",
"scannedIpv4Addrs": [
"172.19.0.1",
"172.17.0.1",
"172.27.0.1"
],
"reportedAt": "0001-01-01T00:00:00Z",
"reportedVersion": "",
"reportedRevision": "",
"reportedBy": "",
"errors": [],
"warnings": [],
"scannedCves": {},
"runningKernel": {
"release": "",
"version": "",
"rebootRequired": false
},
"packages": {},
"config": {
"scan": {
"logDir": "/var/log/vuls",
"resultsDir": "/home/ubuntu/go/src/github.com/future-architect/vuls/results",
"default": {
"port": "22",
"scanMode": [
"fast"
]
},
"servers": {
"rails": {
"serverName": "rails",
"cpeNames": [
"cpe:/a:rubyonrails:ruby_on_rails:3.0.1"
],
"scanMode": [
"fast"
],
"type": "pseudo",
"wordpress": {}
}
},
"cveDict": {
"Name": "cveDict",
"Type": "sqlite3",
"SQLite3Path": "/home/ubuntu/go/src/github.com/kotakanbe/go-cve-dictionary/cve.sqlite3",
"DebugSQL": false
},
"ovalDict": {
"Name": "ovalDict",
"Type": "sqlite3",
"SQLite3Path": "/home/ubuntu/go/src/github.com/kotakanbe/goval-dictionary/oval.sqlite3",
"DebugSQL": false
},
"gost": {
"Name": "gost",
"Type": "sqlite3",
"SQLite3Path": "/home/ubuntu/go/src/github.com/future-architect/vuls/gost.sqlite3",
"DebugSQL": false
},
"exploit": {
"Name": "exploit",
"Type": "sqlite3",
"SQLite3Path": "/home/ubuntu/go/src/github.com/vulsio/go-exploitdb/go-exploitdb.sqlite3",
"DebugSQL": false
},
"metasploit": {
"Name": "metasploit",
"Type": "sqlite3",
"SQLite3Path": "/home/ubuntu/go/src/github.com/takuzoo3868/go-msfdb/go-msfdb.sqlite3",
"DebugSQL": false
}
},
"report": {
"default": {},
"cveDict": {
"Name": "",
"Type": "",
"SQLite3Path": "",
"DebugSQL": false
},
"ovalDict": {
"Name": "",
"Type": "",
"SQLite3Path": "",
"DebugSQL": false
},
"gost": {
"Name": "",
"Type": "",
"SQLite3Path": "",
"DebugSQL": false
},
"exploit": {
"Name": "",
"Type": "",
"SQLite3Path": "",
"DebugSQL": false
},
"metasploit": {
"Name": "",
"Type": "",
"SQLite3Path": "",
"DebugSQL": false
}
}
}
}

5158
integration/data/rhel_71.json Executable file

File diff suppressed because it is too large Load Diff

6926
integration/data/rhel_8.json Executable file

File diff suppressed because it is too large Load Diff

8609
integration/data/ubuntu_1804.json Executable file

File diff suppressed because it is too large Load Diff

8559
integration/data/ubuntu_2004.json Executable file

File diff suppressed because it is too large Load Diff

27
integration/int-config.toml Executable file
View File

@@ -0,0 +1,27 @@
[cveDict]
Type = "sqlite3"
SQLite3Path = "/home/ubuntu/vulsctl/docker/cve.sqlite3"
[ovalDict]
Type = "sqlite3"
SQLite3Path = "/home/ubuntu/vulsctl/docker/oval.sqlite3"
[gost]
Type = "sqlite3"
SQLite3Path = "/home/ubuntu/vulsctl/docker/gost.sqlite3"
[exploit]
Type = "sqlite3"
SQLite3Path = "/home/ubuntu/vulsctl/docker/go-exploitdb.sqlite3"
[metasploit]
type = "sqlite3"
SQLite3Path = "/home/ubuntu/vulsctl/docker/go-msfdb.sqlite3"
[default]
[servers]
[servers.rails]
type = "pseudo"
cpeNames = [ "cpe:/a:rubyonrails:ruby_on_rails:3.0.1" ]

View File

@@ -0,0 +1,27 @@
[cveDict]
Type = "redis"
Url = "redis://127.0.0.1/3"
[ovalDict]
Type = "redis"
Url = "redis://127.0.0.1/1"
[gost]
Type = "redis"
Url = "redis://127.0.0.1/2"
[exploit]
Type = "redis"
Url = "redis://127.0.0.1/4"
[metasploit]
Type = "redis"
Url = "redis://127.0.0.1/5"
[default]
[servers]
[servers.rails]
type = "pseudo"
cpeNames = [ "cpe:/a:rubyonrails:ruby_on_rails:3.0.1" ]

119
logging/logutil.go Normal file
View File

@@ -0,0 +1,119 @@
package logging
import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/k0kubun/pp"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
formatter "github.com/kotakanbe/logrus-prefixed-formatter"
)
//LogOpts has options for logging
type LogOpts struct {
Debug bool `json:"debug,omitempty"`
DebugSQL bool `json:"debugSQL,omitempty"`
LogToFile bool `json:"logToFile,omitempty"`
LogDir string `json:"logDir,omitempty"`
Quiet bool `json:"quiet,omitempty"`
}
// Log for localhost
var Log Logger
// Logger has logrus entry
type Logger struct {
logrus.Entry
}
func init() {
log := logrus.New()
log.Out = ioutil.Discard
fields := logrus.Fields{"prefix": ""}
Log = Logger{Entry: *log.WithFields(fields)}
}
// NewNormalLogger creates normal logger
func NewNormalLogger() Logger {
return Logger{Entry: logrus.Entry{Logger: logrus.New()}}
}
// NewCustomLogger creates logrus
func NewCustomLogger(debug, quiet, logToFile bool, logDir, logMsgAnsiColor, serverName string) Logger {
log := logrus.New()
log.Formatter = &formatter.TextFormatter{MsgAnsiColor: logMsgAnsiColor}
log.Level = logrus.InfoLevel
if debug {
log.Level = logrus.DebugLevel
pp.ColoringEnabled = false
}
if flag.Lookup("test.v") != nil {
return Logger{Entry: *logrus.NewEntry(log)}
}
whereami := "localhost"
if serverName != "" {
whereami = serverName
}
if logToFile {
dir := GetDefaultLogDir()
if logDir != "" {
dir = logDir
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.Mkdir(dir, 0700); err != nil {
log.Errorf("Failed to create log directory. path: %s, err: %+v", dir, err)
}
}
logFile := dir + "/vuls.log"
if file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
log.Out = io.MultiWriter(os.Stderr, file)
} else {
log.Out = os.Stderr
log.Errorf("Failed to create log file. path: %s, err: %+v", logFile, err)
}
if _, err := os.Stat(dir); err == nil {
path := filepath.Join(dir, fmt.Sprintf("%s.log", whereami))
if _, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
log.Hooks.Add(lfshook.NewHook(lfshook.PathMap{
logrus.DebugLevel: path,
logrus.InfoLevel: path,
logrus.WarnLevel: path,
logrus.ErrorLevel: path,
logrus.FatalLevel: path,
logrus.PanicLevel: path,
}, nil))
} else {
log.Errorf("Failed to create log file. path: %s, err: %+v", path, err)
}
}
} else if quiet {
log.Out = io.Discard
} else {
log.Out = os.Stderr
}
entry := log.WithFields(logrus.Fields{"prefix": whereami})
return Logger{Entry: *entry}
}
// GetDefaultLogDir returns default log directory
func GetDefaultLogDir() string {
defaultLogDir := "/var/log/vuls"
if runtime.GOOS == "windows" {
defaultLogDir = filepath.Join(os.Getenv("APPDATA"), "vuls")
}
return defaultLogDir
}

View File

@@ -1,6 +1,7 @@
package models
import (
"strings"
"time"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
@@ -58,7 +59,7 @@ func (v CveContents) PrimarySrcURLs(lang, myFamily, cveID string) (values []CveC
}
}
order := CveContentTypes{Nvd, NewCveContentType(myFamily)}
order := CveContentTypes{Nvd, NewCveContentType(myFamily), GitHub}
for _, ctype := range order {
if cont, found := v[ctype]; found {
if cont.SourceLink == "" {
@@ -74,7 +75,7 @@ func (v CveContents) PrimarySrcURLs(lang, myFamily, cveID string) (values []CveC
}
}
if len(values) == 0 {
if len(values) == 0 && strings.HasPrefix(cveID, "CVE") {
return []CveContentStr{{
Type: Nvd,
Value: "https://nvd.nist.gov/vuln/detail/" + cveID,
@@ -252,6 +253,8 @@ func NewCveContentType(name string) CveContentType {
return Amazon
case "trivy":
return Trivy
case "GitHub":
return Trivy
default:
return Unknown
}
@@ -297,6 +300,9 @@ const (
// Trivy is Trivy
Trivy CveContentType = "trivy"
// GitHub is GitHub Security Alerts
GitHub CveContentType = "github"
// Unknown is Unknown
Unknown CveContentType = "unknown"
)
@@ -317,6 +323,7 @@ var AllCveContetTypes = CveContentTypes{
DebianSecurityTracker,
WpScan,
Trivy,
GitHub,
}
// Except returns CveContentTypes except for given args

View File

@@ -6,9 +6,9 @@ import (
"github.com/aquasecurity/trivy-db/pkg/db"
trivyDBTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/detector/library"
"github.com/future-architect/vuls/logging"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/future-architect/vuls/util"
"golang.org/x/xerrors"
// "github.com/aquasecurity/go-dep-parser/pkg/types"
)
@@ -71,7 +71,7 @@ func (s LibraryScanner) convertFanalToVuln(tvulns []types.DetectedVulnerability)
for _, tvuln := range tvulns {
vinfo, err := s.getVulnDetail(tvuln)
if err != nil {
util.Log.Debugf("failed to getVulnDetail. err: %s, tvuln: %#v", err, tvuln)
logging.Log.Debugf("failed to getVulnDetail. err: %+v, tvuln: %#v", err, tvuln)
continue
}
vulns = append(vulns, vinfo)

View File

@@ -63,13 +63,13 @@ func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) {
}
// FindByFQPN search a package by Fully-Qualified-Package-Name
func (ps Packages) FindByFQPN(nameVerRelArc string) (*Package, error) {
func (ps Packages) FindByFQPN(nameVerRel string) (*Package, error) {
for _, p := range ps {
if nameVerRelArc == p.FQPN() {
if nameVerRel == p.FQPN() {
return &p, nil
}
}
return nil, xerrors.Errorf("Failed to find the package: %s", nameVerRelArc)
return nil, xerrors.Errorf("Failed to find the package: %s", nameVerRel)
}
// Package has installed binary packages.
@@ -81,7 +81,7 @@ type Package struct {
NewRelease string `json:"newRelease"`
Arch string `json:"arch"`
Repository string `json:"repository"`
Changelog Changelog `json:"changelog"`
Changelog *Changelog `json:"changelog,omitempty"`
AffectedProcs []AffectedProcess `json:",omitempty"`
NeedRestartProcs []NeedRestartProcess `json:",omitempty"`
}
@@ -96,9 +96,6 @@ func (p Package) FQPN() string {
if p.Release != "" {
fqpn += fmt.Sprintf("-%s", p.Release)
}
if p.Arch != "" {
fqpn += fmt.Sprintf(".%s", p.Arch)
}
return fqpn
}

View File

@@ -287,7 +287,7 @@ func TestPackage_FormatVersionFromTo(t *testing.T) {
NewRelease: tt.fields.NewRelease,
Arch: tt.fields.Arch,
Repository: tt.fields.Repository,
Changelog: tt.fields.Changelog,
Changelog: &tt.fields.Changelog,
AffectedProcs: tt.fields.AffectedProcs,
NeedRestartProcs: tt.fields.NeedRestartProcs,
}

View File

@@ -4,13 +4,14 @@ import (
"bytes"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/cwe"
"github.com/future-architect/vuls/util"
"github.com/future-architect/vuls/logging"
)
// ScanResults is a slide of ScanResult
@@ -18,31 +19,31 @@ type ScanResults []ScanResult
// ScanResult has the result of scanned CVE information.
type ScanResult struct {
JSONVersion int `json:"jsonVersion"`
Lang string `json:"lang"`
ServerUUID string `json:"serverUUID"`
ServerName string `json:"serverName"` // TOML Section key
Family string `json:"family"`
Release string `json:"release"`
Container Container `json:"container"`
Platform Platform `json:"platform"`
IPv4Addrs []string `json:"ipv4Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
IPv6Addrs []string `json:"ipv6Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
IPSIdentifiers map[config.IPS]string `json:"ipsIdentifiers,omitempty"`
ScannedAt time.Time `json:"scannedAt"`
ScanMode string `json:"scanMode"`
ScannedVersion string `json:"scannedVersion"`
ScannedRevision string `json:"scannedRevision"`
ScannedBy string `json:"scannedBy"`
ScannedVia string `json:"scannedVia"`
ScannedIPv4Addrs []string `json:"scannedIpv4Addrs,omitempty"`
ScannedIPv6Addrs []string `json:"scannedIpv6Addrs,omitempty"`
ReportedAt time.Time `json:"reportedAt"`
ReportedVersion string `json:"reportedVersion"`
ReportedRevision string `json:"reportedRevision"`
ReportedBy string `json:"reportedBy"`
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
JSONVersion int `json:"jsonVersion"`
Lang string `json:"lang"`
ServerUUID string `json:"serverUUID"`
ServerName string `json:"serverName"` // TOML Section key
Family string `json:"family"`
Release string `json:"release"`
Container Container `json:"container"`
Platform Platform `json:"platform"`
IPv4Addrs []string `json:"ipv4Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
IPv6Addrs []string `json:"ipv6Addrs,omitempty"` // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
IPSIdentifiers map[string]string `json:"ipsIdentifiers,omitempty"`
ScannedAt time.Time `json:"scannedAt"`
ScanMode string `json:"scanMode"`
ScannedVersion string `json:"scannedVersion"`
ScannedRevision string `json:"scannedRevision"`
ScannedBy string `json:"scannedBy"`
ScannedVia string `json:"scannedVia"`
ScannedIPv4Addrs []string `json:"scannedIpv4Addrs,omitempty"`
ScannedIPv6Addrs []string `json:"scannedIpv6Addrs,omitempty"`
ReportedAt time.Time `json:"reportedAt"`
ReportedVersion string `json:"reportedVersion"`
ReportedRevision string `json:"reportedRevision"`
ReportedBy string `json:"reportedBy"`
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
ScannedCves VulnInfos `json:"scannedCves"`
RunningKernel Kernel `json:"runningKernel"`
@@ -59,63 +60,19 @@ type ScanResult struct {
} `json:"config"`
}
// CweDict is a dictionary for CWE
type CweDict map[string]CweDictEntry
// Get the name, url, top10URL for the specified cweID, lang
func (c CweDict) Get(cweID, lang string) (name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL string) {
cweNum := strings.TrimPrefix(cweID, "CWE-")
switch config.Conf.Lang {
case "ja":
if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
top10Rank = dict.OwaspTopTen2017
top10URL = cwe.OwaspTopTen2017GitHubURLJa[dict.OwaspTopTen2017]
}
if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
cweTop25Rank = dict.CweTopTwentyfive2019
cweTop25URL = cwe.CweTopTwentyfive2019URL
}
if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
sansTop25Rank = dict.SansTopTwentyfive
sansTop25URL = cwe.SansTopTwentyfiveURL
}
if dict, ok := cwe.CweDictJa[cweNum]; ok {
name = dict.Name
url = fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
} else {
if dict, ok := cwe.CweDictEn[cweNum]; ok {
name = dict.Name
}
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
}
default:
if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
top10Rank = dict.OwaspTopTen2017
top10URL = cwe.OwaspTopTen2017GitHubURLEn[dict.OwaspTopTen2017]
}
if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
cweTop25Rank = dict.CweTopTwentyfive2019
cweTop25URL = cwe.CweTopTwentyfive2019URL
}
if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
sansTop25Rank = dict.SansTopTwentyfive
sansTop25URL = cwe.SansTopTwentyfiveURL
}
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
if dict, ok := cwe.CweDictEn[cweNum]; ok {
name = dict.Name
}
}
return
// Container has Container information
type Container struct {
ContainerID string `json:"containerID"`
Name string `json:"name"`
Image string `json:"image"`
Type string `json:"type"`
UUID string `json:"uuid"`
}
// CweDictEntry is a entry of CWE
type CweDictEntry struct {
En *cwe.Cwe `json:"en,omitempty"`
Ja *cwe.Cwe `json:"ja,omitempty"`
OwaspTopTen2017 string `json:"owaspTopTen2017"`
CweTopTwentyfive2019 string `json:"cweTopTwentyfive2019"`
SansTopTwentyfive string `json:"sansTopTwentyfive"`
// Platform has platform information
type Platform struct {
Name string `json:"name"` // aws or azure or gcp or other...
InstanceID string `json:"instanceID"`
}
// Kernel has the Release, version and whether need restart
@@ -125,137 +82,10 @@ type Kernel struct {
RebootRequired bool `json:"rebootRequired"`
}
// FilterByCvssOver is filter function.
func (r ScanResult) FilterByCvssOver(over float64) ScanResult {
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
v2Max := v.MaxCvss2Score()
v3Max := v.MaxCvss3Score()
max := v2Max.Value.Score
if max < v3Max.Value.Score {
max = v3Max.Value.Score
}
if over <= max {
return true
}
return false
})
r.ScannedCves = filtered
return r
}
// FilterIgnoreCves is filter function.
func (r ScanResult) FilterIgnoreCves() ScanResult {
ignoreCves := []string{}
if len(r.Container.Name) == 0 {
//TODO pass by args
ignoreCves = config.Conf.Servers[r.ServerName].IgnoreCves
} else {
//TODO pass by args
if s, ok := config.Conf.Servers[r.ServerName]; ok {
if con, ok := s.Containers[r.Container.Name]; ok {
ignoreCves = con.IgnoreCves
} else {
return r
}
} else {
util.Log.Errorf("%s is not found in config.toml",
r.ServerName)
return r
}
}
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
for _, c := range ignoreCves {
if v.CveID == c {
return false
}
}
return true
})
r.ScannedCves = filtered
return r
}
// FilterUnfixed is filter function.
func (r ScanResult) FilterUnfixed(ignoreUnfixed bool) ScanResult {
if !ignoreUnfixed {
return r
}
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
// Report cves detected by CPE because Vuls can't know 'fixed' or 'unfixed'
if len(v.CpeURIs) != 0 {
return true
}
NotFixedAll := true
for _, p := range v.AffectedPackages {
NotFixedAll = NotFixedAll && p.NotFixedYet
}
return !NotFixedAll
})
r.ScannedCves = filtered
return r
}
// FilterIgnorePkgs is filter function.
func (r ScanResult) FilterIgnorePkgs() ScanResult {
var ignorePkgsRegexps []string
if len(r.Container.Name) == 0 {
//TODO pass by args
ignorePkgsRegexps = config.Conf.Servers[r.ServerName].IgnorePkgsRegexp
} else {
if s, ok := config.Conf.Servers[r.ServerName]; ok {
if con, ok := s.Containers[r.Container.Name]; ok {
ignorePkgsRegexps = con.IgnorePkgsRegexp
} else {
return r
}
} else {
util.Log.Errorf("%s is not found in config.toml",
r.ServerName)
return r
}
}
regexps := []*regexp.Regexp{}
for _, pkgRegexp := range ignorePkgsRegexps {
re, err := regexp.Compile(pkgRegexp)
if err != nil {
util.Log.Errorf("Failed to parse %s. err: %+v", pkgRegexp, err)
continue
} else {
regexps = append(regexps, re)
}
}
if len(regexps) == 0 {
return r
}
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
if len(v.AffectedPackages) == 0 {
return true
}
for _, p := range v.AffectedPackages {
match := false
for _, re := range regexps {
if re.MatchString(p.Name) {
match = true
}
}
if !match {
return true
}
}
return false
})
r.ScannedCves = filtered
return r
}
// FilterInactiveWordPressLibs is filter function.
func (r ScanResult) FilterInactiveWordPressLibs(detectInactive bool) ScanResult {
func (r *ScanResult) FilterInactiveWordPressLibs(detectInactive bool) {
if detectInactive {
return r
return
}
filtered := r.ScannedCves.Find(func(v VulnInfo) bool {
@@ -268,17 +98,19 @@ func (r ScanResult) FilterInactiveWordPressLibs(detectInactive bool) ScanResult
if p.Status != Inactive {
return true
}
} else {
logging.Log.Warnf("Failed to find the WordPress pkg: %+s", wp.Name)
}
}
return false
})
r.ScannedCves = filtered
return r
return
}
// ReportFileName returns the filename on localhost without extension
func (r ScanResult) ReportFileName() (name string) {
if len(r.Container.ContainerID) == 0 {
if r.Container.ContainerID == "" {
return fmt.Sprintf("%s", r.ServerName)
}
return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
@@ -287,7 +119,7 @@ func (r ScanResult) ReportFileName() (name string) {
// ReportKeyName returns the name of key on S3, Azure-Blob without extension
func (r ScanResult) ReportKeyName() (name string) {
timestr := r.ScannedAt.Format(time.RFC3339)
if len(r.Container.ContainerID) == 0 {
if r.Container.ContainerID == "" {
return fmt.Sprintf("%s/%s", timestr, r.ServerName)
}
return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName)
@@ -295,7 +127,7 @@ func (r ScanResult) ReportKeyName() (name string) {
// ServerInfo returns server name one line
func (r ScanResult) ServerInfo() string {
if len(r.Container.ContainerID) == 0 {
if r.Container.ContainerID == "" {
return fmt.Sprintf("%s (%s%s)",
r.FormatServerName(), r.Family, r.Release)
}
@@ -310,7 +142,7 @@ func (r ScanResult) ServerInfo() string {
// ServerInfoTui returns server information for TUI sidebar
func (r ScanResult) ServerInfoTui() string {
if len(r.Container.ContainerID) == 0 {
if r.Container.ContainerID == "" {
line := fmt.Sprintf("%s (%s%s)",
r.ServerName, r.Family, r.Release)
if len(r.Warnings) != 0 {
@@ -331,7 +163,7 @@ func (r ScanResult) ServerInfoTui() string {
// FormatServerName returns server and container name
func (r ScanResult) FormatServerName() (name string) {
if len(r.Container.ContainerID) == 0 {
if r.Container.ContainerID == "" {
name = r.ServerName
} else {
name = fmt.Sprintf("%s@%s",
@@ -350,7 +182,7 @@ func (r ScanResult) FormatTextReportHeader() string {
buf.WriteString("=")
}
pkgs := r.FormatUpdatablePacksSummary()
pkgs := r.FormatUpdatablePkgsSummary()
if 0 < len(r.WordPressPackages) {
pkgs = fmt.Sprintf("%s, %d WordPress pkgs", pkgs, len(r.WordPressPackages))
}
@@ -358,7 +190,7 @@ func (r ScanResult) FormatTextReportHeader() string {
pkgs = fmt.Sprintf("%s, %d libs", pkgs, r.LibraryScanners.Total())
}
return fmt.Sprintf("%s\n%s\n%s, %s, %s, %s, %s\n%s\n",
return fmt.Sprintf("%s\n%s\n%s\n%s, %s, %s, %s\n%s\n",
r.ServerInfo(),
buf.String(),
r.ScannedCves.FormatCveSummary(),
@@ -369,9 +201,10 @@ func (r ScanResult) FormatTextReportHeader() string {
pkgs)
}
// FormatUpdatablePacksSummary returns a summary of updatable packages
func (r ScanResult) FormatUpdatablePacksSummary() string {
if !r.isDisplayUpdatableNum() {
// FormatUpdatablePkgsSummary returns a summary of updatable packages
func (r ScanResult) FormatUpdatablePkgsSummary() string {
mode := r.Config.Scan.Servers[r.ServerName].Mode
if !r.isDisplayUpdatableNum(mode) {
return fmt.Sprintf("%d installed", len(r.Packages))
}
@@ -426,16 +259,11 @@ func (r ScanResult) FormatAlertSummary() string {
return fmt.Sprintf("en: %d, ja: %d alerts", enCnt, jaCnt)
}
func (r ScanResult) isDisplayUpdatableNum() bool {
if r.Family == config.FreeBSD {
func (r ScanResult) isDisplayUpdatableNum(mode config.ScanMode) bool {
if r.Family == constant.FreeBSD {
return false
}
var mode config.ScanMode
//TODO pass by args
s, _ := config.Conf.Servers[r.ServerName]
mode = s.Mode
if mode.IsOffline() {
return false
}
@@ -444,11 +272,11 @@ func (r ScanResult) isDisplayUpdatableNum() bool {
}
if mode.IsFast() {
switch r.Family {
case config.RedHat,
config.Oracle,
config.Debian,
config.Ubuntu,
config.Raspbian:
case constant.RedHat,
constant.Oracle,
constant.Debian,
constant.Ubuntu,
constant.Raspbian:
return false
default:
return true
@@ -462,34 +290,9 @@ func (r ScanResult) IsContainer() bool {
return 0 < len(r.Container.ContainerID)
}
// IsDeepScanMode checks if the scan mode is deep scan mode.
func (r ScanResult) IsDeepScanMode() bool {
for _, s := range r.Config.Scan.Servers {
if ok := s.Mode.IsDeep(); ok {
return true
}
}
return false
}
// Container has Container information
type Container struct {
ContainerID string `json:"containerID"`
Name string `json:"name"`
Image string `json:"image"`
Type string `json:"type"`
UUID string `json:"uuid"`
}
// Platform has platform information
type Platform struct {
Name string `json:"name"` // aws or azure or gcp or other...
InstanceID string `json:"instanceID"`
}
// RemoveRaspbianPackFromResult is for Raspberry Pi and removes the Raspberry Pi dedicated package from ScanResult.
func (r ScanResult) RemoveRaspbianPackFromResult() ScanResult {
if r.Family != config.Raspbian {
if r.Family != constant.Raspbian {
return r
}
@@ -534,3 +337,142 @@ func (r ScanResult) ClearFields(targetTagNames []string) ScanResult {
}
return r
}
// CheckEOL checks the EndOfLife of the OS
func (r *ScanResult) CheckEOL() {
switch r.Family {
case constant.ServerTypePseudo, constant.Raspbian:
return
}
eol, found := config.GetEOL(r.Family, r.Release)
if !found {
r.Warnings = append(r.Warnings,
fmt.Sprintf("Failed to check EOL. Register the issue to https://github.com/future-architect/vuls/issues with the information in `Family: %s Release: %s`",
r.Family, r.Release))
return
}
now := time.Now()
if eol.IsStandardSupportEnded(now) {
r.Warnings = append(r.Warnings, "Standard OS support is EOL(End-of-Life). Purchase extended support if available or Upgrading your OS is strongly recommended.")
if eol.ExtendedSupportUntil.IsZero() {
return
}
if !eol.IsExtendedSuppportEnded(now) {
r.Warnings = append(r.Warnings,
fmt.Sprintf("Extended support available until %s. Check the vendor site.",
eol.ExtendedSupportUntil.Format("2006-01-02")))
} else {
r.Warnings = append(r.Warnings,
"Extended support is also EOL. There are many Vulnerabilities that are not detected, Upgrading your OS strongly recommended.")
}
} else if !eol.StandardSupportUntil.IsZero() &&
now.AddDate(0, 3, 0).After(eol.StandardSupportUntil) {
r.Warnings = append(r.Warnings,
fmt.Sprintf("Standard OS support will be end in 3 months. EOL date: %s",
eol.StandardSupportUntil.Format("2006-01-02")))
}
}
// SortForJSONOutput sort list elements in the ScanResult to diff in integration-test
func (r *ScanResult) SortForJSONOutput() {
for k, v := range r.Packages {
sort.SliceStable(v.AffectedProcs, func(i, j int) bool {
return v.AffectedProcs[i].PID < v.AffectedProcs[j].PID
})
sort.SliceStable(v.NeedRestartProcs, func(i, j int) bool {
return v.NeedRestartProcs[i].PID < v.NeedRestartProcs[j].PID
})
r.Packages[k] = v
}
for k, v := range r.ScannedCves {
sort.SliceStable(v.AffectedPackages, func(i, j int) bool {
return v.AffectedPackages[i].Name < v.AffectedPackages[j].Name
})
sort.SliceStable(v.DistroAdvisories, func(i, j int) bool {
return v.DistroAdvisories[i].AdvisoryID < v.DistroAdvisories[j].AdvisoryID
})
sort.SliceStable(v.Exploits, func(i, j int) bool {
return v.Exploits[i].ID < v.Exploits[j].ID
})
sort.SliceStable(v.Metasploits, func(i, j int) bool {
return v.Metasploits[i].Name < v.Metasploits[j].Name
})
for kk, vv := range v.CveContents {
sort.SliceStable(vv.References, func(i, j int) bool {
return vv.References[i].Link < vv.References[j].Link
})
v.CveContents[kk] = vv
}
sort.SliceStable(v.AlertDict.En, func(i, j int) bool {
return v.AlertDict.En[i].Title < v.AlertDict.En[j].Title
})
sort.SliceStable(v.AlertDict.Ja, func(i, j int) bool {
return v.AlertDict.Ja[i].Title < v.AlertDict.Ja[j].Title
})
r.ScannedCves[k] = v
}
}
// CweDict is a dictionary for CWE
type CweDict map[string]CweDictEntry
// Get the name, url, top10URL for the specified cweID, lang
func (c CweDict) Get(cweID, lang string) (name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL string) {
cweNum := strings.TrimPrefix(cweID, "CWE-")
switch lang {
case "ja":
if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
top10Rank = dict.OwaspTopTen2017
top10URL = cwe.OwaspTopTen2017GitHubURLJa[dict.OwaspTopTen2017]
}
if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
cweTop25Rank = dict.CweTopTwentyfive2019
cweTop25URL = cwe.CweTopTwentyfive2019URL
}
if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
sansTop25Rank = dict.SansTopTwentyfive
sansTop25URL = cwe.SansTopTwentyfiveURL
}
if dict, ok := cwe.CweDictJa[cweNum]; ok {
name = dict.Name
url = fmt.Sprintf("http://jvndb.jvn.jp/ja/cwe/%s.html", cweID)
} else {
if dict, ok := cwe.CweDictEn[cweNum]; ok {
name = dict.Name
}
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
}
default:
if dict, ok := c[cweNum]; ok && dict.OwaspTopTen2017 != "" {
top10Rank = dict.OwaspTopTen2017
top10URL = cwe.OwaspTopTen2017GitHubURLEn[dict.OwaspTopTen2017]
}
if dict, ok := c[cweNum]; ok && dict.CweTopTwentyfive2019 != "" {
cweTop25Rank = dict.CweTopTwentyfive2019
cweTop25URL = cwe.CweTopTwentyfive2019URL
}
if dict, ok := c[cweNum]; ok && dict.SansTopTwentyfive != "" {
sansTop25Rank = dict.SansTopTwentyfive
sansTop25URL = cwe.SansTopTwentyfiveURL
}
url = fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", cweID)
if dict, ok := cwe.CweDictEn[cweNum]; ok {
name = dict.Name
}
}
return
}
// CweDictEntry is a entry of CWE
type CweDictEntry struct {
En *cwe.Cwe `json:"en,omitempty"`
Ja *cwe.Cwe `json:"ja,omitempty"`
OwaspTopTen2017 string `json:"owaspTopTen2017"`
CweTopTwentyfive2019 string `json:"cweTopTwentyfive2019"`
SansTopTwentyfive string `json:"sansTopTwentyfive"`
}

View File

@@ -3,634 +3,11 @@ package models
import (
"reflect"
"testing"
"time"
"github.com/future-architect/vuls/config"
"github.com/k0kubun/pp"
"github.com/future-architect/vuls/constant"
)
func TestFilterByCvssOver(t *testing.T) {
type in struct {
over float64
rs ScanResult
}
var tests = []struct {
in in
out ScanResult
}{
{
in: in{
over: 7.0,
rs: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0002",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
},
),
},
},
},
},
out: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
},
),
},
},
},
},
// OVAL Severity
{
in: in{
over: 7.0,
rs: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss2Severity: "HIGH",
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: RedHat,
CveID: "CVE-2017-0002",
Cvss2Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Oracle,
CveID: "CVE-2017-0003",
Cvss2Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
},
},
},
out: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss2Severity: "HIGH",
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: RedHat,
CveID: "CVE-2017-0002",
Cvss2Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Oracle,
CveID: "CVE-2017-0003",
Cvss2Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
},
},
},
}
for _, tt := range tests {
actual := tt.in.rs.FilterByCvssOver(tt.in.over)
for k := range tt.out.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
}
}
func TestFilterIgnoreCveIDs(t *testing.T) {
type in struct {
cves []string
rs ScanResult
}
var tests = []struct {
in in
out ScanResult
}{
{
in: in{
cves: []string{"CVE-2017-0002"},
rs: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
},
},
}
for _, tt := range tests {
config.Conf.Servers = map[string]config.ServerInfo{
"name": {IgnoreCves: tt.in.cves},
}
actual := tt.in.rs.FilterIgnoreCves()
for k := range tt.out.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
for k := range actual.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
}
}
func TestFilterIgnoreCveIDsContainer(t *testing.T) {
type in struct {
cves []string
rs ScanResult
}
var tests = []struct {
in in
out ScanResult
}{
{
in: in{
cves: []string{"CVE-2017-0002"},
rs: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
},
},
out: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
},
},
}
for _, tt := range tests {
config.Conf.Servers = map[string]config.ServerInfo{
"name": {
Containers: map[string]config.ContainerSetting{
"dockerA": {
IgnoreCves: tt.in.cves,
},
},
},
}
actual := tt.in.rs.FilterIgnoreCves()
for k := range tt.out.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
for k := range actual.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
}
}
func TestFilterUnfixed(t *testing.T) {
var tests = []struct {
in ScanResult
out ScanResult
}{
{
in: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{
Name: "a",
NotFixedYet: true,
},
},
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
AffectedPackages: PackageFixStatuses{
{
Name: "b",
NotFixedYet: false,
},
},
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
AffectedPackages: PackageFixStatuses{
{
Name: "c",
NotFixedYet: true,
},
{
Name: "d",
NotFixedYet: false,
},
},
},
},
},
out: ScanResult{
ScannedCves: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
AffectedPackages: PackageFixStatuses{
{
Name: "b",
NotFixedYet: false,
},
},
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
AffectedPackages: PackageFixStatuses{
{
Name: "c",
NotFixedYet: true,
},
{
Name: "d",
NotFixedYet: false,
},
},
},
},
},
},
}
for i, tt := range tests {
actual := tt.in.FilterUnfixed(true)
if !reflect.DeepEqual(tt.out.ScannedCves, actual.ScannedCves) {
o := pp.Sprintf("%v", tt.out.ScannedCves)
a := pp.Sprintf("%v", actual.ScannedCves)
t.Errorf("[%d] expected: %v\n actual: %v\n", i, o, a)
}
}
}
func TestFilterIgnorePkgs(t *testing.T) {
type in struct {
ignorePkgsRegexp []string
rs ScanResult
}
var tests = []struct {
in in
out ScanResult
}{
{
in: in{
ignorePkgsRegexp: []string{"^kernel"},
rs: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
},
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
},
},
{
in: in{
ignorePkgsRegexp: []string{"^kernel"},
rs: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
{
in: in{
ignorePkgsRegexp: []string{"^kernel", "^vim", "^bind"},
rs: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{},
},
},
}
for _, tt := range tests {
config.Conf.Servers = map[string]config.ServerInfo{
"name": {IgnorePkgsRegexp: tt.in.ignorePkgsRegexp},
}
actual := tt.in.rs.FilterIgnorePkgs()
for k := range tt.out.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
for k := range actual.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
}
}
func TestFilterIgnorePkgsContainer(t *testing.T) {
type in struct {
ignorePkgsRegexp []string
rs ScanResult
}
var tests = []struct {
in in
out ScanResult
}{
{
in: in{
ignorePkgsRegexp: []string{"^kernel"},
rs: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
},
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
},
},
out: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
},
},
{
in: in{
ignorePkgsRegexp: []string{"^kernel"},
rs: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
{
in: in{
ignorePkgsRegexp: []string{"^kernel", "^vim", "^bind"},
rs: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{},
},
},
}
for _, tt := range tests {
config.Conf.Servers = map[string]config.ServerInfo{
"name": {
Containers: map[string]config.ContainerSetting{
"dockerA": {
IgnorePkgsRegexp: tt.in.ignorePkgsRegexp,
},
},
},
}
actual := tt.in.rs.FilterIgnorePkgs()
for k := range tt.out.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
for k := range actual.ScannedCves {
if !reflect.DeepEqual(tt.out.ScannedCves[k], actual.ScannedCves[k]) {
o := pp.Sprintf("%v", tt.out.ScannedCves[k])
a := pp.Sprintf("%v", actual.ScannedCves[k])
t.Errorf("[%s] expected: %v\n actual: %v\n", k, o, a)
}
}
}
}
func TestIsDisplayUpdatableNum(t *testing.T) {
var tests = []struct {
mode []byte
@@ -651,52 +28,52 @@ func TestIsDisplayUpdatableNum(t *testing.T) {
},
{
mode: []byte{config.Fast},
family: config.RedHat,
family: constant.RedHat,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Oracle,
family: constant.Oracle,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Debian,
family: constant.Debian,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Ubuntu,
family: constant.Ubuntu,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Raspbian,
family: constant.Raspbian,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.CentOS,
family: constant.CentOS,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.Amazon,
family: constant.Amazon,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.FreeBSD,
family: constant.FreeBSD,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.OpenSUSE,
family: constant.OpenSUSE,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.Alpine,
family: constant.Alpine,
expected: true,
},
}
@@ -706,16 +83,284 @@ func TestIsDisplayUpdatableNum(t *testing.T) {
for _, m := range tt.mode {
mode.Set(m)
}
config.Conf.Servers = map[string]config.ServerInfo{
"name": {Mode: mode},
}
r := ScanResult{
ServerName: "name",
Family: tt.family,
}
act := r.isDisplayUpdatableNum()
act := r.isDisplayUpdatableNum(mode)
if tt.expected != act {
t.Errorf("[%d] expected %#v, actual %#v", i, tt.expected, act)
}
}
}
func TestScanResult_Sort(t *testing.T) {
type fields struct {
Packages Packages
ScannedCves VulnInfos
}
tests := []struct {
name string
fields fields
expected fields
}{
{
name: "already asc",
fields: fields{
Packages: map[string]Package{
"pkgA": {
Name: "pkgA",
AffectedProcs: []AffectedProcess{
{PID: "1", Name: "procB"},
{PID: "2", Name: "procA"},
},
NeedRestartProcs: []NeedRestartProcess{
{PID: "1"},
{PID: "2"},
},
},
},
ScannedCves: VulnInfos{
"CVE-2014-3591": VulnInfo{
AffectedPackages: PackageFixStatuses{
PackageFixStatus{Name: "pkgA"},
PackageFixStatus{Name: "pkgB"},
},
DistroAdvisories: []DistroAdvisory{
{AdvisoryID: "adv-1"},
{AdvisoryID: "adv-2"},
},
Exploits: []Exploit{
{ID: "a"},
{ID: "b"},
},
Metasploits: []Metasploit{
{Name: "a"},
{Name: "b"},
},
CveContents: CveContents{
"nvd": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
"jvn": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
},
AlertDict: AlertDict{
En: []Alert{
{Title: "a"},
{Title: "b"},
},
Ja: []Alert{
{Title: "a"},
{Title: "b"},
},
},
},
},
},
expected: fields{
Packages: map[string]Package{
"pkgA": {
Name: "pkgA",
AffectedProcs: []AffectedProcess{
{PID: "1", Name: "procB"},
{PID: "2", Name: "procA"},
},
NeedRestartProcs: []NeedRestartProcess{
{PID: "1"},
{PID: "2"},
},
},
},
ScannedCves: VulnInfos{
"CVE-2014-3591": VulnInfo{
AffectedPackages: PackageFixStatuses{
PackageFixStatus{Name: "pkgA"},
PackageFixStatus{Name: "pkgB"},
},
DistroAdvisories: []DistroAdvisory{
{AdvisoryID: "adv-1"},
{AdvisoryID: "adv-2"},
},
Exploits: []Exploit{
{ID: "a"},
{ID: "b"},
},
Metasploits: []Metasploit{
{Name: "a"},
{Name: "b"},
},
CveContents: CveContents{
"nvd": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
"jvn": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
},
AlertDict: AlertDict{
En: []Alert{
{Title: "a"},
{Title: "b"},
},
Ja: []Alert{
{Title: "a"},
{Title: "b"},
},
},
},
},
},
},
{
name: "sort",
fields: fields{
Packages: map[string]Package{
"pkgA": {
Name: "pkgA",
AffectedProcs: []AffectedProcess{
{PID: "2", Name: "procA"},
{PID: "1", Name: "procB"},
},
NeedRestartProcs: []NeedRestartProcess{
{PID: "91"},
{PID: "90"},
},
},
},
ScannedCves: VulnInfos{
"CVE-2014-3591": VulnInfo{
AffectedPackages: PackageFixStatuses{
PackageFixStatus{Name: "pkgB"},
PackageFixStatus{Name: "pkgA"},
},
DistroAdvisories: []DistroAdvisory{
{AdvisoryID: "adv-2"},
{AdvisoryID: "adv-1"},
},
Exploits: []Exploit{
{ID: "b"},
{ID: "a"},
},
Metasploits: []Metasploit{
{Name: "b"},
{Name: "a"},
},
CveContents: CveContents{
"nvd": CveContent{
References: References{
Reference{Link: "b"},
Reference{Link: "a"},
},
},
"jvn": CveContent{
References: References{
Reference{Link: "b"},
Reference{Link: "a"},
},
},
},
AlertDict: AlertDict{
En: []Alert{
{Title: "b"},
{Title: "a"},
},
Ja: []Alert{
{Title: "b"},
{Title: "a"},
},
},
},
},
},
expected: fields{
Packages: map[string]Package{
"pkgA": {
Name: "pkgA",
AffectedProcs: []AffectedProcess{
{PID: "1", Name: "procB"},
{PID: "2", Name: "procA"},
},
NeedRestartProcs: []NeedRestartProcess{
{PID: "90"},
{PID: "91"},
},
},
},
ScannedCves: VulnInfos{
"CVE-2014-3591": VulnInfo{
AffectedPackages: PackageFixStatuses{
PackageFixStatus{Name: "pkgA"},
PackageFixStatus{Name: "pkgB"},
},
DistroAdvisories: []DistroAdvisory{
{AdvisoryID: "adv-1"},
{AdvisoryID: "adv-2"},
},
Exploits: []Exploit{
{ID: "a"},
{ID: "b"},
},
Metasploits: []Metasploit{
{Name: "a"},
{Name: "b"},
},
CveContents: CveContents{
"nvd": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
"jvn": CveContent{
References: References{
Reference{Link: "a"},
Reference{Link: "b"},
},
},
},
AlertDict: AlertDict{
En: []Alert{
{Title: "a"},
{Title: "b"},
},
Ja: []Alert{
{Title: "a"},
{Title: "b"},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ScanResult{
Packages: tt.fields.Packages,
ScannedCves: tt.fields.ScannedCves,
}
r.SortForJSONOutput()
if !reflect.DeepEqual(r.Packages, tt.expected.Packages) {
t.Errorf("act %+v, want %+v", r.Packages, tt.expected.Packages)
}
if !reflect.DeepEqual(r.ScannedCves, tt.expected.ScannedCves) {
t.Errorf("act %+v, want %+v", r.ScannedCves, tt.expected.ScannedCves)
}
})
}
}

View File

@@ -3,12 +3,13 @@ package models
import (
"bytes"
"fmt"
"regexp"
"sort"
"strings"
"time"
"github.com/future-architect/vuls/config"
exploitmodels "github.com/mozqnet/go-exploitdb/models"
"github.com/future-architect/vuls/logging"
exploitmodels "github.com/vulsio/go-exploitdb/models"
)
// VulnInfos has a map of VulnInfo
@@ -26,6 +27,81 @@ func (v VulnInfos) Find(f func(VulnInfo) bool) VulnInfos {
return filtered
}
// FilterByCvssOver return scored vulnerabilities
func (v VulnInfos) FilterByCvssOver(over float64) VulnInfos {
return v.Find(func(v VulnInfo) bool {
if over <= v.MaxCvssScore().Value.Score {
return true
}
return false
})
}
// FilterIgnoreCves filter function.
func (v VulnInfos) FilterIgnoreCves(ignoreCveIDs []string) VulnInfos {
return v.Find(func(v VulnInfo) bool {
for _, c := range ignoreCveIDs {
if v.CveID == c {
return false
}
}
return true
})
}
// FilterUnfixed filter unfixed CVE-IDs
func (v VulnInfos) FilterUnfixed(ignoreUnfixed bool) VulnInfos {
if !ignoreUnfixed {
return v
}
return v.Find(func(v VulnInfo) bool {
// Report cves detected by CPE because Vuls can't know 'fixed' or 'unfixed'
if len(v.CpeURIs) != 0 {
return true
}
NotFixedAll := true
for _, p := range v.AffectedPackages {
NotFixedAll = NotFixedAll && p.NotFixedYet
}
return !NotFixedAll
})
}
// FilterIgnorePkgs is filter function.
func (v VulnInfos) FilterIgnorePkgs(ignorePkgsRegexps []string) VulnInfos {
regexps := []*regexp.Regexp{}
for _, pkgRegexp := range ignorePkgsRegexps {
re, err := regexp.Compile(pkgRegexp)
if err != nil {
logging.Log.Warnf("Failed to parse %s. err: %+v", pkgRegexp, err)
continue
} else {
regexps = append(regexps, re)
}
}
if len(regexps) == 0 {
return v
}
return v.Find(func(v VulnInfo) bool {
if len(v.AffectedPackages) == 0 {
return true
}
for _, p := range v.AffectedPackages {
match := false
for _, re := range regexps {
if re.MatchString(p.Name) {
match = true
}
}
if !match {
return true
}
}
return false
})
}
// FindScoredVulns return scored vulnerabilities
func (v VulnInfos) FindScoredVulns() VulnInfos {
return v.Find(func(vv VulnInfo) bool {
@@ -57,11 +133,13 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) {
func (v VulnInfos) CountGroupBySeverity() map[string]int {
m := map[string]int{}
for _, vInfo := range v {
score := vInfo.MaxCvss2Score().Value.Score
score := vInfo.MaxCvss3Score().Value.Score
if score < 0.1 {
score = vInfo.MaxCvss3Score().Value.Score
score = vInfo.MaxCvss2Score().Value.Score
}
switch {
case 9 <= score:
m["Critical"]++
case 7.0 <= score:
m["High"]++
case 4.0 <= score:
@@ -78,14 +156,15 @@ func (v VulnInfos) CountGroupBySeverity() map[string]int {
// FormatCveSummary summarize the number of CVEs group by CVSSv2 Severity
func (v VulnInfos) FormatCveSummary() string {
m := v.CountGroupBySeverity()
if config.Conf.IgnoreUnscoredCves {
return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
m["High"]+m["Medium"]+m["Low"], m["High"], m["Medium"], m["Low"])
}
return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
line := fmt.Sprintf("Total: %d (Critical:%d High:%d Medium:%d Low:%d ?:%d)",
m["High"]+m["Medium"]+m["Low"]+m["Unknown"],
m["High"], m["Medium"], m["Low"], m["Unknown"])
m["Critical"], m["High"], m["Medium"], m["Low"], m["Unknown"])
nPlus, nMinus := v.CountDiff()
if 0 < nPlus || 0 < nMinus {
line = fmt.Sprintf("%s +%d -%d", line, nPlus, nMinus)
}
return line
}
// FormatFixedStatus summarize the number of cves are fixed.
@@ -103,6 +182,18 @@ func (v VulnInfos) FormatFixedStatus(packs Packages) string {
return fmt.Sprintf("%d/%d Fixed", fixed, total)
}
// CountDiff counts the number of added/removed CVE-ID
func (v VulnInfos) CountDiff() (nPlus int, nMinus int) {
for _, vInfo := range v {
if vInfo.DiffStatus == DiffPlus {
nPlus++
} else if vInfo.DiffStatus == DiffMinus {
nMinus++
}
}
return
}
// PackageFixStatuses is a list of PackageStatus
type PackageFixStatuses []PackageFixStatus
@@ -157,8 +248,8 @@ type VulnInfo struct {
GitHubSecurityAlerts GitHubSecurityAlerts `json:"gitHubSecurityAlerts,omitempty"`
WpPackageFixStats WpPackageFixStats `json:"wpPackageFixStats,omitempty"`
LibraryFixedIns LibraryFixedIns `json:"libraryFixedIns,omitempty"`
VulnType string `json:"vulnType,omitempty"`
VulnType string `json:"vulnType,omitempty"`
DiffStatus DiffStatus `json:"diffStatus,omitempty"`
}
// Alert has CERT alert information
@@ -234,24 +325,47 @@ func (g WpPackages) Add(pkg WpPackage) WpPackages {
return append(g, pkg)
}
// DiffStatus keeps a comparison with the previous detection results for this CVE
type DiffStatus string
const (
// DiffPlus is newly detected CVE
DiffPlus = DiffStatus("+")
// DiffMinus is resolved CVE
DiffMinus = DiffStatus("-")
)
// CveIDDiffFormat format CVE-ID for diff mode
func (v VulnInfo) CveIDDiffFormat() string {
if v.DiffStatus != "" {
return fmt.Sprintf("%s %s", v.DiffStatus, v.CveID)
}
return fmt.Sprintf("%s", v.CveID)
}
// Titles returns title (TUI)
func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) {
if lang == "ja" {
if cont, found := v.CveContents[Jvn]; found && 0 < len(cont.Title) {
if cont, found := v.CveContents[Jvn]; found && cont.Title != "" {
values = append(values, CveContentStr{Jvn, cont.Title})
}
}
// RedHat API has one line title.
if cont, found := v.CveContents[RedHatAPI]; found && 0 < len(cont.Title) {
if cont, found := v.CveContents[RedHatAPI]; found && cont.Title != "" {
values = append(values, CveContentStr{RedHatAPI, cont.Title})
}
// GitHub security alerts has a title.
if cont, found := v.CveContents[GitHub]; found && cont.Title != "" {
values = append(values, CveContentStr{GitHub, cont.Title})
}
order := CveContentTypes{Trivy, Nvd, NewCveContentType(myFamily)}
order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...)
for _, ctype := range order {
// Only JVN has meaningful title. so return first 100 char of summary
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Summary) {
if cont, found := v.CveContents[ctype]; found && cont.Summary != "" {
summary := strings.Replace(cont.Summary, "\n", " ", -1)
values = append(values, CveContentStr{
Type: ctype,
@@ -279,7 +393,7 @@ func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) {
// Summaries returns summaries
func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
if lang == "ja" {
if cont, found := v.CveContents[Jvn]; found && 0 < len(cont.Summary) {
if cont, found := v.CveContents[Jvn]; found && cont.Summary != "" {
summary := cont.Title
summary += "\n" + strings.Replace(
strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1)
@@ -287,10 +401,10 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
}
}
order := CveContentTypes{Trivy, NewCveContentType(myFamily), Nvd}
order := CveContentTypes{Trivy, NewCveContentType(myFamily), Nvd, GitHub}
order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...)
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Summary) {
if cont, found := v.CveContents[ctype]; found && cont.Summary != "" {
summary := strings.Replace(cont.Summary, "\n", " ", -1)
values = append(values, CveContentStr{
Type: ctype,
@@ -308,7 +422,7 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
if v, ok := v.CveContents[WpScan]; ok {
values = append(values, CveContentStr{
Type: "WPVDB",
Type: WpScan,
Value: v.Title,
})
}
@@ -324,14 +438,11 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
}
// Cvss2Scores returns CVSS V2 Scores
func (v VulnInfo) Cvss2Scores(myFamily string) (values []CveContentCvss) {
order := []CveContentType{Nvd, RedHatAPI, RedHat, Jvn}
if myFamily != config.RedHat && myFamily != config.CentOS {
order = append(order, NewCveContentType(myFamily))
}
func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) {
order := []CveContentType{RedHatAPI, RedHat, Nvd, Jvn}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found {
if cont.Cvss2Score == 0 || cont.Cvss2Severity == "" {
if cont.Cvss2Score == 0 && cont.Cvss2Severity == "" {
continue
}
// https://nvd.nist.gov/vuln-metrics/cvss
@@ -346,52 +457,17 @@ func (v VulnInfo) Cvss2Scores(myFamily string) (values []CveContentCvss) {
})
}
}
for _, adv := range v.DistroAdvisories {
if adv.Severity != "" {
values = append(values, CveContentCvss{
Type: "Advisory",
Value: Cvss{
Type: CVSS2,
Score: severityToV2ScoreRoughly(adv.Severity),
CalculatedBySeverity: true,
Vector: "-",
Severity: strings.ToUpper(adv.Severity),
},
})
}
}
// An OVAL entry in Ubuntu and Debian has only severity (CVSS score isn't included).
// Show severity and dummy score calculated roughly.
order = append(order, AllCveContetTypes.Except(order...)...)
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found &&
cont.Cvss2Score == 0 &&
cont.Cvss3Score == 0 &&
cont.Cvss2Severity != "" {
values = append(values, CveContentCvss{
Type: cont.Type,
Value: Cvss{
Type: CVSS2,
Score: severityToV2ScoreRoughly(cont.Cvss2Severity),
CalculatedBySeverity: true,
Vector: "-",
Severity: strings.ToUpper(cont.Cvss2Severity),
},
})
}
}
return
}
// Cvss3Scores returns CVSS V3 Score
func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) {
order := []CveContentType{Nvd, RedHatAPI, RedHat, Jvn}
order := []CveContentType{RedHatAPI, RedHat, Nvd, Jvn}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found {
if cont.Cvss3Score == 0 && cont.Cvss3Severity == "" {
continue
}
// https://nvd.nist.gov/vuln-metrics/cvss
values = append(values, CveContentCvss{
Type: ctype,
@@ -405,131 +481,74 @@ func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) {
}
}
if cont, found := v.CveContents[Trivy]; found && cont.Cvss3Severity != "" {
values = append(values, CveContentCvss{
Type: Trivy,
Value: Cvss{
Type: CVSS3,
Score: severityToV2ScoreRoughly(cont.Cvss3Severity),
Severity: strings.ToUpper(cont.Cvss3Severity),
},
})
}
return
}
// MaxCvss3Score returns Max CVSS V3 Score
func (v VulnInfo) MaxCvss3Score() CveContentCvss {
order := []CveContentType{Nvd, RedHat, RedHatAPI, Jvn}
max := 0.0
value := CveContentCvss{
Type: Unknown,
Value: Cvss{Type: CVSS3},
}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && max < cont.Cvss3Score {
// https://nvd.nist.gov/vuln-metrics/cvss
value = CveContentCvss{
for _, ctype := range []CveContentType{Debian, DebianSecurityTracker, Ubuntu, Amazon, Trivy, GitHub, WpScan} {
if cont, found := v.CveContents[ctype]; found && cont.Cvss3Severity != "" {
values = append(values, CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS3,
Score: cont.Cvss3Score,
Vector: cont.Cvss3Vector,
Severity: strings.ToUpper(cont.Cvss3Severity),
Type: CVSS3,
Score: severityToCvssScoreRoughly(cont.Cvss3Severity),
CalculatedBySeverity: true,
Severity: strings.ToUpper(cont.Cvss3Severity),
},
}
max = cont.Cvss3Score
})
}
}
return value
// Memo: Only RedHat, Oracle and Amazon has severity data in advisory.
for _, adv := range v.DistroAdvisories {
if adv.Severity != "" {
score := severityToCvssScoreRoughly(adv.Severity)
values = append(values, CveContentCvss{
Type: "Vendor",
Value: Cvss{
Type: CVSS3,
Score: score,
CalculatedBySeverity: true,
Severity: strings.ToUpper(adv.Severity),
},
})
}
}
return
}
// MaxCvssScore returns max CVSS Score
// If there is no CVSS Score, return Severity as a numerical value.
func (v VulnInfo) MaxCvssScore() CveContentCvss {
v3Max := v.MaxCvss3Score()
v2Max := v.MaxCvss2Score()
max := v3Max
if max.Type == Unknown {
return v2Max
if v3Max.Type != Unknown {
return v3Max
}
return v.MaxCvss2Score()
}
if max.Value.Score < v2Max.Value.Score && !v2Max.Value.CalculatedBySeverity {
max = v2Max
// MaxCvss3Score returns Max CVSS V3 Score
func (v VulnInfo) MaxCvss3Score() CveContentCvss {
max := CveContentCvss{
Type: Unknown,
Value: Cvss{Type: CVSS3},
}
for _, cvss := range v.Cvss3Scores() {
if max.Value.Score < cvss.Value.Score {
max = cvss
}
}
return max
}
// MaxCvss2Score returns Max CVSS V2 Score
func (v VulnInfo) MaxCvss2Score() CveContentCvss {
order := []CveContentType{Nvd, RedHat, RedHatAPI, Jvn}
max := 0.0
value := CveContentCvss{
max := CveContentCvss{
Type: Unknown,
Value: Cvss{Type: CVSS2},
}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && max < cont.Cvss2Score {
// https://nvd.nist.gov/vuln-metrics/cvss
value = CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS2,
Score: cont.Cvss2Score,
Vector: cont.Cvss2Vector,
Severity: strings.ToUpper(cont.Cvss2Severity),
},
}
max = cont.Cvss2Score
for _, cvss := range v.Cvss2Scores() {
if max.Value.Score < cvss.Value.Score {
max = cvss
}
}
if 0 < max {
return value
}
// If CVSS score isn't on NVD, RedHat and JVN, use OVAL and advisory Severity.
// Convert severity to cvss score roughly, then returns max severity.
// Only Ubuntu, RedHat and Oracle have severity data in OVAL.
order = []CveContentType{Ubuntu, RedHat, Oracle}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Cvss2Severity) {
score := severityToV2ScoreRoughly(cont.Cvss2Severity)
if max < score {
value = CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS2,
Score: score,
CalculatedBySeverity: true,
Vector: cont.Cvss2Vector,
Severity: strings.ToUpper(cont.Cvss2Severity),
},
}
}
max = score
}
}
// Only RedHat, Oracle and Amazon has severity data in advisory.
for _, adv := range v.DistroAdvisories {
if adv.Severity != "" {
score := severityToV2ScoreRoughly(adv.Severity)
if max < score {
value = CveContentCvss{
Type: "Vendor",
Value: Cvss{
Type: CVSS2,
Score: score,
CalculatedBySeverity: true,
Vector: "-",
Severity: adv.Severity,
},
}
}
}
}
return value
return max
}
// AttackVector returns attack vector string
@@ -613,16 +632,29 @@ type Cvss struct {
// Format CVSS Score and Vector
func (c Cvss) Format() string {
if c.Score == 0 || c.Vector == "" {
return c.Severity
if c.Vector == "" {
return fmt.Sprintf("%s %s", c.SeverityToCvssScoreRange(), c.Severity)
}
switch c.Type {
case CVSS2:
return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
case CVSS3:
return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
}
// SeverityToCvssScoreRange returns CVSS score range
func (c Cvss) SeverityToCvssScoreRange() string {
return severityToCvssScoreRange(c.Severity)
}
func severityToCvssScoreRange(severity string) string {
switch strings.ToUpper(severity) {
case "CRITICAL":
return "9.0-10.0"
case "IMPORTANT", "HIGH":
return "7.0-8.9"
case "MODERATE", "MEDIUM":
return "4.0-6.9"
case "LOW":
return "0.1-3.9"
}
return ""
return "None"
}
// Amazon Linux Security Advisory
@@ -637,7 +669,7 @@ func (c Cvss) Format() string {
// Critical, High, Medium, Low
// https://wiki.ubuntu.com/Bugs/Importance
// https://people.canonical.com/~ubuntu-security/cve/priority.html
func severityToV2ScoreRoughly(severity string) float64 {
func severityToCvssScoreRoughly(severity string) float64 {
switch strings.ToUpper(severity) {
case "CRITICAL":
return 10.0
@@ -731,14 +763,10 @@ type AlertDict struct {
// FormatSource returns which source has this alert
func (a AlertDict) FormatSource() string {
s := []string{}
if len(a.En) != 0 {
s = append(s, "USCERT")
if len(a.En) != 0 || len(a.Ja) != 0 {
return "CERT"
}
if len(a.Ja) != 0 {
s = append(s, "JPCERT")
}
return strings.Join(s, "/")
return ""
}
// Confidences is a list of Confidence

View File

@@ -3,6 +3,7 @@ package models
import (
"reflect"
"testing"
"time"
)
func TestTitles(t *testing.T) {
@@ -98,10 +99,10 @@ func TestTitles(t *testing.T) {
},
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.cont.Titles(tt.in.lang, "redhat")
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual)
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -221,11 +222,65 @@ func TestCountGroupBySeverity(t *testing.T) {
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss2Score: 6.0,
Cvss3Score: 6.0,
},
RedHat: {
Type: RedHat,
Cvss2Score: 7.0,
Cvss3Score: 7.0,
},
},
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss3Score: 2.0,
},
},
},
"CVE-2017-0004": {
CveID: "CVE-2017-0004",
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss3Score: 5.0,
},
},
},
"CVE-2017-0005": {
CveID: "CVE-2017-0005",
},
"CVE-2017-0006": {
CveID: "CVE-2017-0005",
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss3Score: 10.0,
},
},
},
},
out: map[string]int{
"Critical": 1,
"High": 1,
"Medium": 1,
"Low": 1,
"Unknown": 1,
},
},
{
in: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss2Score: 1.0,
},
RedHat: {
Type: RedHat,
Cvss3Score: 7.0,
},
},
},
@@ -250,21 +305,31 @@ func TestCountGroupBySeverity(t *testing.T) {
"CVE-2017-0005": {
CveID: "CVE-2017-0005",
},
"CVE-2017-0006": {
CveID: "CVE-2017-0005",
CveContents: CveContents{
Nvd: {
Type: Nvd,
Cvss2Score: 10.0,
},
},
},
},
out: map[string]int{
"High": 1,
"Medium": 1,
"Low": 1,
"Unknown": 1,
"Critical": 1,
"High": 1,
"Medium": 1,
"Low": 1,
"Unknown": 1,
},
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.CountGroupBySeverity()
for k := range tt.out {
if tt.out[k] != actual[k] {
t.Errorf("\nexpected %s: %d\n actual %d\n",
k, tt.out[k], actual[k])
t.Errorf("[%d]\nexpected %s: %d\n actual %d\n",
i, k, tt.out[k], actual[k])
}
}
}
@@ -275,6 +340,7 @@ func TestToSortedSlice(t *testing.T) {
in VulnInfos
out []VulnInfo
}{
//0
{
in: VulnInfos{
"CVE-2017-0002": {
@@ -333,7 +399,7 @@ func TestToSortedSlice(t *testing.T) {
},
},
},
// When max scores are the same, sort by CVE-ID
//[1] When max scores are the same, sort by CVE-ID
{
in: VulnInfos{
"CVE-2017-0002": {
@@ -354,7 +420,7 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
RedHat: {
Type: RedHat,
Cvss2Score: 7.0,
Cvss3Score: 7.0,
},
},
},
@@ -365,7 +431,7 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
RedHat: {
Type: RedHat,
Cvss2Score: 7.0,
Cvss3Score: 7.0,
},
},
},
@@ -384,7 +450,7 @@ func TestToSortedSlice(t *testing.T) {
},
},
},
// When there are no cvss scores, sort by severity
//[2] When there are no cvss scores, sort by severity
{
in: VulnInfos{
"CVE-2017-0002": {
@@ -392,7 +458,7 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "High",
Cvss3Severity: "High",
},
},
},
@@ -401,7 +467,7 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "Low",
Cvss3Severity: "Low",
},
},
},
@@ -412,7 +478,7 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "High",
Cvss3Severity: "High",
},
},
},
@@ -421,17 +487,17 @@ func TestToSortedSlice(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "Low",
Cvss3Severity: "Low",
},
},
},
},
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.ToSortedSlice()
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual)
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -462,18 +528,16 @@ func TestCvss2Scores(t *testing.T) {
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Cvss2Severity: "HIGH",
},
//v3
RedHatAPI: {
Type: RedHatAPI,
Cvss3Score: 8.1,
Cvss3Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Cvss3Severity: "HIGH",
},
},
},
out: []CveContentCvss{
{
Type: Nvd,
Value: Cvss{
Type: CVSS2,
Score: 8.1,
Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Severity: "HIGH",
},
},
{
Type: RedHat,
Value: Cvss{
@@ -483,6 +547,15 @@ func TestCvss2Scores(t *testing.T) {
Severity: "HIGH",
},
},
{
Type: Nvd,
Value: Cvss{
Type: CVSS2,
Score: 8.1,
Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Severity: "HIGH",
},
},
{
Type: Jvn,
Value: Cvss{
@@ -501,7 +574,7 @@ func TestCvss2Scores(t *testing.T) {
},
}
for i, tt := range tests {
actual := tt.in.Cvss2Scores("redhat")
actual := tt.in.Cvss2Scores()
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
@@ -513,6 +586,7 @@ func TestMaxCvss2Scores(t *testing.T) {
in VulnInfo
out CveContentCvss
}{
// 0
{
in: VulnInfo{
CveContents: CveContents{
@@ -546,26 +620,6 @@ func TestMaxCvss2Scores(t *testing.T) {
},
},
},
// Severity in OVAL
{
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "HIGH",
},
},
},
out: CveContentCvss{
Type: Ubuntu,
Value: Cvss{
Type: CVSS2,
Score: 8.9,
CalculatedBySeverity: true,
Severity: "HIGH",
},
},
},
// Empty
{
in: VulnInfo{},
@@ -611,13 +665,6 @@ func TestCvss3Scores(t *testing.T) {
},
},
out: []CveContentCvss{
{
Type: Nvd,
Value: Cvss{
Type: CVSS3,
Score: 0.0,
},
},
{
Type: RedHat,
Value: Cvss{
@@ -629,16 +676,38 @@ func TestCvss3Scores(t *testing.T) {
},
},
},
// [1] Severity in OVAL
{
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss3Severity: "HIGH",
},
},
},
out: []CveContentCvss{
{
Type: Ubuntu,
Value: Cvss{
Type: CVSS3,
Score: 8.9,
CalculatedBySeverity: true,
Severity: "HIGH",
},
},
},
},
// Empty
{
in: VulnInfo{},
out: nil,
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.Cvss3Scores()
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual)
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -710,10 +779,10 @@ func TestMaxCvssScores(t *testing.T) {
},
},
out: CveContentCvss{
Type: RedHat,
Type: Nvd,
Value: Cvss{
Type: CVSS2,
Score: 8.0,
Type: CVSS3,
Score: 7.0,
},
},
},
@@ -740,14 +809,14 @@ func TestMaxCvssScores(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "HIGH",
Cvss3Severity: "HIGH",
},
},
},
out: CveContentCvss{
Type: Ubuntu,
Value: Cvss{
Type: CVSS2,
Type: CVSS3,
Score: 8.9,
CalculatedBySeverity: true,
Severity: "HIGH",
@@ -760,7 +829,7 @@ func TestMaxCvssScores(t *testing.T) {
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "MEDIUM",
Cvss3Severity: "MEDIUM",
},
Nvd: {
Type: Nvd,
@@ -770,11 +839,12 @@ func TestMaxCvssScores(t *testing.T) {
},
},
out: CveContentCvss{
Type: Nvd,
Type: Ubuntu,
Value: Cvss{
Type: CVSS2,
Score: 7.0,
Severity: "HIGH",
Type: CVSS3,
Score: 6.9,
Severity: "MEDIUM",
CalculatedBySeverity: true,
},
},
},
@@ -790,20 +860,20 @@ func TestMaxCvssScores(t *testing.T) {
out: CveContentCvss{
Type: "Vendor",
Value: Cvss{
Type: CVSS2,
Type: CVSS3,
Score: 8.9,
CalculatedBySeverity: true,
Vector: "-",
Severity: "HIGH",
},
},
},
//5
{
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Cvss2Severity: "MEDIUM",
Cvss3Severity: "MEDIUM",
},
Nvd: {
Type: Nvd,
@@ -818,11 +888,12 @@ func TestMaxCvssScores(t *testing.T) {
},
},
out: CveContentCvss{
Type: Nvd,
Type: "Vendor",
Value: Cvss{
Type: CVSS2,
Score: 4,
Severity: "MEDIUM",
Type: CVSS3,
Score: 8.9,
Severity: "HIGH",
CalculatedBySeverity: true,
},
},
},
@@ -861,7 +932,7 @@ func TestFormatMaxCvssScore(t *testing.T) {
},
RedHat: {
Type: RedHat,
Cvss2Severity: "HIGH",
Cvss3Severity: "HIGH",
Cvss3Score: 8.0,
},
Nvd: {
@@ -871,7 +942,7 @@ func TestFormatMaxCvssScore(t *testing.T) {
},
},
},
out: "8.3 HIGH (jvn)",
out: "8.0 HIGH (redhat)",
},
{
in: VulnInfo{
@@ -897,10 +968,10 @@ func TestFormatMaxCvssScore(t *testing.T) {
out: "9.9 HIGH (redhat)",
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.FormatMaxCvssScore()
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual)
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -1170,3 +1241,372 @@ func TestVulnInfo_AttackVector(t *testing.T) {
})
}
}
func TestVulnInfos_FilterByCvssOver(t *testing.T) {
type args struct {
over float64
}
tests := []struct {
name string
v VulnInfos
args args
want VulnInfos
}{
{
name: "over 7.0",
args: args{over: 7.0},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0002",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
},
),
},
},
want: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Nvd,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
},
),
},
},
},
{
name: "over high",
args: args{over: 7.0},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss3Severity: "HIGH",
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: Debian,
CveID: "CVE-2017-0002",
Cvss3Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: GitHub,
CveID: "CVE-2017-0003",
Cvss3Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
},
want: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss3Severity: "HIGH",
LastModified: time.Time{},
},
),
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: Debian,
CveID: "CVE-2017-0002",
Cvss3Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: GitHub,
CveID: "CVE-2017-0003",
Cvss3Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.v.FilterByCvssOver(tt.args.over); !reflect.DeepEqual(got, tt.want) {
t.Errorf("VulnInfos.FindByCvssOver() = %v, want %v", got, tt.want)
}
})
}
}
func TestVulnInfos_FilterIgnoreCves(t *testing.T) {
type args struct {
ignoreCveIDs []string
}
tests := []struct {
name string
v VulnInfos
args args
want VulnInfos
}{
{
name: "filter ignored",
args: args{ignoreCveIDs: []string{"CVE-2017-0002"}},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
want: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.v.FilterIgnoreCves(tt.args.ignoreCveIDs); !reflect.DeepEqual(got, tt.want) {
t.Errorf("VulnInfos.FindIgnoreCves() = %v, want %v", got, tt.want)
}
})
}
}
func TestVulnInfos_FilterUnfixed(t *testing.T) {
type args struct {
ignoreUnfixed bool
}
tests := []struct {
name string
v VulnInfos
args args
want VulnInfos
}{
{
name: "filter ok",
args: args{ignoreUnfixed: true},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{
Name: "a",
NotFixedYet: true,
},
},
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
AffectedPackages: PackageFixStatuses{
{
Name: "b",
NotFixedYet: false,
},
},
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
AffectedPackages: PackageFixStatuses{
{
Name: "c",
NotFixedYet: true,
},
{
Name: "d",
NotFixedYet: false,
},
},
},
},
want: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
AffectedPackages: PackageFixStatuses{
{
Name: "b",
NotFixedYet: false,
},
},
},
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
AffectedPackages: PackageFixStatuses{
{
Name: "c",
NotFixedYet: true,
},
{
Name: "d",
NotFixedYet: false,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.v.FilterUnfixed(tt.args.ignoreUnfixed); !reflect.DeepEqual(got, tt.want) {
t.Errorf("VulnInfos.FilterUnfixed() = %v, want %v", got, tt.want)
}
})
}
}
func TestVulnInfos_FilterIgnorePkgs(t *testing.T) {
type args struct {
ignorePkgsRegexps []string
}
tests := []struct {
name string
v VulnInfos
args args
want VulnInfos
}{
{
name: "filter pkgs 1",
args: args{ignorePkgsRegexps: []string{"^kernel"}},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
},
},
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
want: VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
},
},
},
{
name: "filter pkgs 2",
args: args{ignorePkgsRegexps: []string{"^kernel"}},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
want: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
{
name: "filter pkgs 3",
args: args{ignorePkgsRegexps: []string{"^kernel", "^vim", "^bind"}},
v: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageFixStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
want: VulnInfos{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.v.FilterIgnorePkgs(tt.args.ignorePkgsRegexps); !reflect.DeepEqual(got, tt.want) {
t.Errorf("VulnInfos.FilterIgnorePkgs() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1 +0,0 @@
package msf

View File

@@ -1,75 +0,0 @@
// +build !scanner
package msf
import (
"fmt"
"net/http"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/parnurzeal/gorequest"
"github.com/takuzoo3868/go-msfdb/db"
metasploitmodels "github.com/takuzoo3868/go-msfdb/models"
"golang.org/x/xerrors"
)
// FillWithMetasploit fills metasploit module information that has in module
func FillWithMetasploit(driver db.DB, r *models.ScanResult) (nMetasploitCve int, err error) {
if driver == nil {
return 0, nil
}
for cveID, vuln := range r.ScannedCves {
if cveID == "" {
continue
}
ms := driver.GetModuleByCveID(cveID)
if len(ms) == 0 {
continue
}
modules := ConvertToModels(ms)
vuln.Metasploits = modules
r.ScannedCves[cveID] = vuln
nMetasploitCve++
}
return nMetasploitCve, nil
}
// ConvertToModels converts gost model to vuls model
func ConvertToModels(ms []*metasploitmodels.Metasploit) (modules []models.Metasploit) {
for _, m := range ms {
var links []string
if 0 < len(m.References) {
for _, u := range m.References {
links = append(links, u.Link)
}
}
module := models.Metasploit{
Name: m.Name,
Title: m.Title,
Description: m.Description,
URLs: links,
}
modules = append(modules, module)
}
return modules
}
// CheckHTTPHealth do health check
func CheckHTTPHealth() error {
if !cnf.Conf.Metasploit.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", cnf.Conf.Metasploit.URL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().Get(url).End()
// resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to connect to metasploit server. url: %s, errs: %w", url, errs)
}
return nil
}

View File

@@ -4,9 +4,9 @@ package oval
import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
)
// Alpine is the struct of Alpine Linux
@@ -15,22 +15,33 @@ type Alpine struct {
}
// NewAlpine creates OVAL client for SUSE
func NewAlpine() Alpine {
func NewAlpine(cnf config.VulnDictInterface) Alpine {
return Alpine{
Base{
family: config.Alpine,
family: constant.Alpine,
Cnf: cnf,
},
}
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Alpine) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
func (o Alpine) FillWithOval(r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if config.Conf.OvalDict.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
if o.Cnf.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r, o.Cnf.GetURL()); err != nil {
return 0, err
}
} else {
driver, err := newOvalDB(o.Cnf, r.Family)
if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
@@ -46,7 +57,7 @@ func (o Alpine) update(r *models.ScanResult, defPacks defPacks) {
cveID := defPacks.def.Advisory.Cves[0].CveID
vinfo, ok := r.ScannedCves[cveID]
if !ok {
util.Log.Debugf("%s is newly detected by OVAL", cveID)
logging.Log.Debugf("%s is newly detected by OVAL", cveID)
vinfo = models.VulnInfo{
CveID: cveID,
Confidences: []models.Confidence{models.OvalMatch},

View File

@@ -7,9 +7,10 @@ import (
"strings"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -23,7 +24,7 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) {
ovalContent.Type = models.NewCveContentType(o.family)
vinfo, ok := r.ScannedCves[defPacks.def.Debian.CveID]
if !ok {
util.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Debian.CveID)
logging.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Debian.CveID)
vinfo = models.VulnInfo{
CveID: defPacks.def.Debian.CveID,
Confidences: []models.Confidence{models.OvalMatch},
@@ -33,14 +34,14 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) {
cveContents := vinfo.CveContents
ctype := models.NewCveContentType(o.family)
if _, ok := vinfo.CveContents[ctype]; ok {
util.Log.Debugf("%s OVAL will be overwritten",
logging.Log.Debugf("%s OVAL will be overwritten",
defPacks.def.Debian.CveID)
} else {
util.Log.Debugf("%s is also detected by OVAL",
logging.Log.Debugf("%s is also detected by OVAL",
defPacks.def.Debian.CveID)
cveContents = models.CveContents{}
}
if r.Family != config.Raspbian {
if r.Family != constant.Raspbian {
vinfo.Confidences.AppendIfMissing(models.OvalMatch)
} else {
if len(vinfo.Confidences) == 0 {
@@ -109,18 +110,19 @@ type Debian struct {
}
// NewDebian creates OVAL client for Debian
func NewDebian() Debian {
func NewDebian(cnf config.VulnDictInterface) Debian {
return Debian{
DebianBase{
Base{
family: config.Debian,
family: constant.Debian,
Cnf: cnf,
},
},
}
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Debian) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
func (o Debian) FillWithOval(r *models.ScanResult) (nCVEs int, err error) {
//Debian's uname gives both of kernel release(uname -r), version(kernel-image version)
linuxImage := "linux-image-" + r.RunningKernel.Release
@@ -139,20 +141,30 @@ func (o Debian) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err
}
var relatedDefs ovalResult
if config.Conf.OvalDict.IsFetchViaHTTP() {
if r.Family != config.Raspbian {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
if o.Cnf.IsFetchViaHTTP() {
if r.Family != constant.Raspbian {
if relatedDefs, err = getDefsByPackNameViaHTTP(r, o.Cnf.GetURL()); err != nil {
return 0, err
}
} else {
// OVAL does not support Package for Raspbian, so skip it.
result := r.RemoveRaspbianPackFromResult()
if relatedDefs, err = getDefsByPackNameViaHTTP(&result); err != nil {
if relatedDefs, err = getDefsByPackNameViaHTTP(&result, o.Cnf.GetURL()); err != nil {
return 0, err
}
}
} else {
if r.Family != config.Raspbian {
driver, err := newOvalDB(o.Cnf, r.Family)
if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
if r.Family != constant.Raspbian {
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
@@ -199,18 +211,19 @@ type Ubuntu struct {
}
// NewUbuntu creates OVAL client for Debian
func NewUbuntu() Ubuntu {
func NewUbuntu(cnf config.VulnDictInterface) Ubuntu {
return Ubuntu{
DebianBase{
Base{
family: config.Ubuntu,
family: constant.Ubuntu,
Cnf: cnf,
},
},
}
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
func (o Ubuntu) FillWithOval(r *models.ScanResult) (nCVEs int, err error) {
switch util.Major(r.Release) {
case "14":
kernelNamesInOval := []string{
@@ -226,7 +239,7 @@ func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err
"linux-signed-lts-xenial",
"linux",
}
return o.fillWithOval(driver, r, kernelNamesInOval)
return o.fillWithOval(r, kernelNamesInOval)
case "16":
kernelNamesInOval := []string{
"linux-aws",
@@ -261,7 +274,7 @@ func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err
"linux-snapdragon",
"linux",
}
return o.fillWithOval(driver, r, kernelNamesInOval)
return o.fillWithOval(r, kernelNamesInOval)
case "18":
kernelNamesInOval := []string{
"linux-aws",
@@ -316,7 +329,7 @@ func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err
"linux-snapdragon",
"linux",
}
return o.fillWithOval(driver, r, kernelNamesInOval)
return o.fillWithOval(r, kernelNamesInOval)
case "20":
kernelNamesInOval := []string{
"linux-aws",
@@ -344,12 +357,12 @@ func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err
"linux-signed-oracle",
"linux",
}
return o.fillWithOval(driver, r, kernelNamesInOval)
return o.fillWithOval(r, kernelNamesInOval)
}
return 0, fmt.Errorf("Ubuntu %s is not support for now", r.Release)
}
func (o Ubuntu) fillWithOval(driver db.DB, r *models.ScanResult, kernelNamesInOval []string) (nCVEs int, err error) {
func (o Ubuntu) fillWithOval(r *models.ScanResult, kernelNamesInOval []string) (nCVEs int, err error) {
linuxImage := "linux-image-" + r.RunningKernel.Release
runningKernelVersion := ""
kernelPkgInOVAL := ""
@@ -361,7 +374,7 @@ func (o Ubuntu) fillWithOval(driver db.DB, r *models.ScanResult, kernelNamesInOv
if v, ok := r.Packages[linuxImage]; ok {
runningKernelVersion = v.Version
} else {
util.Log.Warnf("Unable to detect vulns of running kernel because the version of the running kernel is unknown. server: %s",
logging.Log.Warnf("Unable to detect vulns of running kernel because the version of the running kernel is unknown. server: %s",
r.ServerName)
}
@@ -400,7 +413,7 @@ func (o Ubuntu) fillWithOval(driver db.DB, r *models.ScanResult, kernelNamesInOv
}
if kernelPkgInOVAL == "" {
util.Log.Warnf("The OVAL name of the running kernel image %+v is not found. So vulns of `linux` wll be detected. server: %s",
logging.Log.Warnf("The OVAL name of the running kernel image %+v is not found. So vulns of `linux` wll be detected. server: %s",
r.RunningKernel, r.ServerName)
kernelPkgInOVAL = "linux"
isOVALKernelPkgAdded = true
@@ -415,11 +428,21 @@ func (o Ubuntu) fillWithOval(driver db.DB, r *models.ScanResult, kernelNamesInOv
}
var relatedDefs ovalResult
if config.Conf.OvalDict.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
if o.Cnf.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r, o.Cnf.GetURL()); err != nil {
return 0, err
}
} else {
driver, err := newOvalDB(o.Cnf, r.Family)
if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}

View File

@@ -6,9 +6,7 @@ import (
"reflect"
"testing"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -56,7 +54,7 @@ func TestPackNamesOfUpdateDebian(t *testing.T) {
},
}
util.Log = util.NewCustomLogger(config.ServerInfo{})
// util.Log = util.NewCustomLogger()
for i, tt := range tests {
Debian{}.update(&tt.in, tt.defPacks)
e := tt.out.ScannedCves["CVE-2000-1000"].AffectedPackages

View File

@@ -4,11 +4,10 @@ package oval
import (
"encoding/json"
"fmt"
"net/http"
"time"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
@@ -18,70 +17,78 @@ import (
// Client is the interface of OVAL client.
type Client interface {
CheckHTTPHealth() error
FillWithOval(db.DB, *models.ScanResult) (int, error)
// CheckIfOvalFetched checks if oval entries are in DB by family, release.
CheckIfOvalFetched(db.DB, string, string) (bool, error)
CheckIfOvalFresh(db.DB, string, string) (bool, error)
FillWithOval(*models.ScanResult) (int, error)
CheckIfOvalFetched(string, string) (bool, error)
CheckIfOvalFresh(string, string) (bool, error)
}
// Base is a base struct
type Base struct {
family string
}
// CheckHTTPHealth do health check
func (b Base) CheckHTTPHealth() error {
if !cnf.Conf.OvalDict.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", cnf.Conf.OvalDict.URL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().Get(url).End()
// resp, _, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
// resp, _, errs = gorequest.New().Proxy(api.httpProxy).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return xerrors.Errorf("Failed to request to OVAL server. url: %s, errs: %w",
url, errs)
}
return nil
Cnf config.VulnDictInterface
}
// CheckIfOvalFetched checks if oval entries are in DB by family, release.
func (b Base) CheckIfOvalFetched(driver db.DB, osFamily, release string) (fetched bool, err error) {
if !cnf.Conf.OvalDict.IsFetchViaHTTP() {
count, err := driver.CountDefs(osFamily, release)
func (b Base) CheckIfOvalFetched(osFamily, release string) (fetched bool, err error) {
ovalFamily, err := GetFamilyInOval(osFamily)
if err != nil {
return false, err
}
if !b.Cnf.IsFetchViaHTTP() {
driver, err := newOvalDB(b.Cnf, ovalFamily)
if err != nil {
return false, xerrors.Errorf("Failed to count OVAL defs: %s, %s, %w", osFamily, release, err)
return false, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
count, err := driver.CountDefs(ovalFamily, release)
if err != nil {
return false, xerrors.Errorf("Failed to count OVAL defs: %s, %s, %w", ovalFamily, release, err)
}
logging.Log.Infof("OVAL %s %s found. defs: %d", osFamily, release, count)
return 0 < count, nil
}
url, _ := util.URLPathJoin(cnf.Conf.OvalDict.URL, "count", osFamily, release)
resp, body, errs := gorequest.New().Get(url).End()
url, _ := util.URLPathJoin(config.Conf.OvalDict.URL, "count", ovalFamily, release)
resp, body, errs := gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return false, xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %w", url, resp, errs)
return false, xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %+v", url, resp, errs)
}
count := 0
if err := json.Unmarshal([]byte(body), &count); err != nil {
return false, xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err)
}
logging.Log.Infof("OVAL %s %s is fresh. defs: %d", osFamily, release, count)
return 0 < count, nil
}
// CheckIfOvalFresh checks if oval entries are fresh enough
func (b Base) CheckIfOvalFresh(driver db.DB, osFamily, release string) (ok bool, err error) {
func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) {
ovalFamily, err := GetFamilyInOval(osFamily)
if err != nil {
return false, err
}
var lastModified time.Time
if !cnf.Conf.OvalDict.IsFetchViaHTTP() {
lastModified = driver.GetLastModified(osFamily, release)
if !b.Cnf.IsFetchViaHTTP() {
driver, err := newOvalDB(b.Cnf, ovalFamily)
if err != nil {
return false, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v", err)
}
}()
lastModified = driver.GetLastModified(ovalFamily, release)
} else {
url, _ := util.URLPathJoin(cnf.Conf.OvalDict.URL, "lastmodified", osFamily, release)
resp, body, errs := gorequest.New().Get(url).End()
url, _ := util.URLPathJoin(config.Conf.OvalDict.URL, "lastmodified", ovalFamily, release)
resp, body, errs := gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return false, xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %w", url, resp, errs)
return false, xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %+v", url, resp, errs)
}
if err := json.Unmarshal([]byte(body), &lastModified); err != nil {
@@ -92,10 +99,37 @@ func (b Base) CheckIfOvalFresh(driver db.DB, osFamily, release string) (ok bool,
since := time.Now()
since = since.AddDate(0, 0, -3)
if lastModified.Before(since) {
util.Log.Warnf("OVAL for %s %s is old, last modified is %s. It's recommended to update OVAL to improve scanning accuracy. How to update OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage",
logging.Log.Warnf("OVAL for %s %s is old, last modified is %s. It's recommended to update OVAL to improve scanning accuracy. How to update OVAL database, see https://github.com/kotakanbe/goval-dictionary#usage",
osFamily, release, lastModified)
return false, nil
}
util.Log.Infof("OVAL is fresh: %s %s ", osFamily, release)
logging.Log.Infof("OVAL %s %s is fresh. lastModified: %s", osFamily, release, lastModified.Format(time.RFC3339))
return true, nil
}
// NewOvalDB returns oval db client
func newOvalDB(cnf config.VulnDictInterface, familyInScanResult string) (driver db.DB, err error) {
if cnf.IsFetchViaHTTP() {
return nil, nil
}
path := cnf.GetURL()
if cnf.GetType() == "sqlite3" {
path = cnf.GetSQLite3Path()
}
ovalFamily, err := GetFamilyInOval(familyInScanResult)
if err != nil {
return nil, err
}
driver, locked, err := db.NewDB(ovalFamily, cnf.GetType(), path, cnf.GetDebugSQL())
if err != nil {
if locked {
err = xerrors.Errorf("SQLite3: %s is locked. err: %w", cnf.GetSQLite3Path(), err)
}
err = xerrors.Errorf("Failed to new OVAL DB. err: %w", err)
return nil, err
}
return driver, nil
}

View File

@@ -8,9 +8,9 @@ import (
"strings"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -20,18 +20,29 @@ type RedHatBase struct {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o RedHatBase) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
func (o RedHatBase) FillWithOval(r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if config.Conf.OvalDict.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
if o.Cnf.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r, o.Cnf.GetURL()); err != nil {
return 0, err
}
} else {
driver, err := newOvalDB(o.Cnf, r.Family)
if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v")
}
}()
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
relatedDefs.Sort()
for _, defPacks := range relatedDefs.entries {
nCVEs += o.update(r, defPacks)
}
@@ -91,7 +102,7 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) (nCVEs int)
ovalContent := *o.convertToModel(cve.CveID, &defPacks.def)
vinfo, ok := r.ScannedCves[cve.CveID]
if !ok {
util.Log.Debugf("%s is newly detected by OVAL", cve.CveID)
logging.Log.Debugf("%s is newly detected by OVAL: DefID: %s", cve.CveID, defPacks.def.DefinitionID)
vinfo = models.VulnInfo{
CveID: cve.CveID,
Confidences: models.Confidences{models.OvalMatch},
@@ -102,13 +113,12 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) (nCVEs int)
cveContents := vinfo.CveContents
if v, ok := vinfo.CveContents[ctype]; ok {
if v.LastModified.After(ovalContent.LastModified) {
util.Log.Debugf("%s, OvalID: %d ignored: ",
cve.CveID, defPacks.def.ID)
logging.Log.Debugf("%s ignored. DefID: %s ", cve.CveID, defPacks.def.DefinitionID)
} else {
util.Log.Debugf("%s OVAL will be overwritten", cve.CveID)
logging.Log.Debugf("%s OVAL will be overwritten. DefID: %s", cve.CveID, defPacks.def.DefinitionID)
}
} else {
util.Log.Debugf("%s also detected by OVAL", cve.CveID)
logging.Log.Debugf("%s also detected by OVAL. DefID: %s", cve.CveID, defPacks.def.DefinitionID)
cveContents = models.CveContents{}
}
@@ -143,7 +153,7 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) (nCVEs int)
func (o RedHatBase) convertToDistroAdvisory(def *ovalmodels.Definition) *models.DistroAdvisory {
advisoryID := def.Title
if (o.family == config.RedHat || o.family == config.CentOS) && len(advisoryID) > 0 {
if (o.family == constant.RedHat || o.family == constant.CentOS) && len(advisoryID) > 0 {
ss := strings.Fields(def.Title)
advisoryID = strings.TrimSuffix(ss[0], ":")
}
@@ -173,17 +183,15 @@ func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *mo
score2, vec2 := o.parseCvss2(cve.Cvss2)
score3, vec3 := o.parseCvss3(cve.Cvss3)
severity := def.Advisory.Severity
sev2, sev3, severity := "", "", def.Advisory.Severity
if cve.Impact != "" {
severity = cve.Impact
}
sev2, sev3 := "", ""
if score2 == 0 {
sev2 = severity
}
if score3 == 0 {
if severity != "None" {
sev3 = severity
if score2 != 0 {
sev2 = severity
}
}
// CWE-ID in RedHat OVAL may have multiple cweIDs separated by space
@@ -248,11 +256,12 @@ type RedHat struct {
}
// NewRedhat creates OVAL client for Redhat
func NewRedhat() RedHat {
func NewRedhat(cnf config.VulnDictInterface) RedHat {
return RedHat{
RedHatBase{
Base{
family: config.RedHat,
family: constant.RedHat,
Cnf: cnf,
},
},
}
@@ -264,11 +273,12 @@ type CentOS struct {
}
// NewCentOS creates OVAL client for CentOS
func NewCentOS() CentOS {
func NewCentOS(cnf config.VulnDictInterface) CentOS {
return CentOS{
RedHatBase{
Base{
family: config.CentOS,
family: constant.CentOS,
Cnf: cnf,
},
},
}
@@ -280,11 +290,12 @@ type Oracle struct {
}
// NewOracle creates OVAL client for Oracle
func NewOracle() Oracle {
func NewOracle(cnf config.VulnDictInterface) Oracle {
return Oracle{
RedHatBase{
Base{
family: config.Oracle,
family: constant.Oracle,
Cnf: cnf,
},
},
}
@@ -297,11 +308,12 @@ type Amazon struct {
}
// NewAmazon creates OVAL client for Amazon Linux
func NewAmazon() Amazon {
func NewAmazon(cnf config.VulnDictInterface) Amazon {
return Amazon{
RedHatBase{
Base{
family: config.Amazon,
family: constant.Amazon,
Cnf: cnf,
},
},
}

View File

@@ -6,9 +6,7 @@ import (
"reflect"
"testing"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -132,7 +130,7 @@ func TestPackNamesOfUpdate(t *testing.T) {
},
}
util.Log = util.NewCustomLogger(config.ServerInfo{})
// util.Log = util.Logger{}.NewCustomLogger()
for i, tt := range tests {
RedHat{}.update(&tt.in, tt.defPacks)
e := tt.out.ScannedCves["CVE-2000-1000"].AffectedPackages

View File

@@ -4,9 +4,9 @@ package oval
import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -16,23 +16,34 @@ type SUSE struct {
}
// NewSUSE creates OVAL client for SUSE
func NewSUSE() SUSE {
func NewSUSE(cnf config.VulnDictInterface) SUSE {
// TODO implement other family
return SUSE{
Base{
family: config.SUSEEnterpriseServer,
family: constant.SUSEEnterpriseServer,
Cnf: cnf,
},
}
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o SUSE) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
func (o SUSE) FillWithOval(r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if config.Conf.OvalDict.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
if o.Cnf.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r, o.Cnf.GetURL()); err != nil {
return 0, err
}
} else {
driver, err := newOvalDB(o.Cnf, r.Family)
if err != nil {
return 0, err
}
defer func() {
if err := driver.CloseDB(); err != nil {
logging.Log.Errorf("Failed to close DB. err: %+v")
}
}()
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
@@ -55,7 +66,7 @@ func (o SUSE) update(r *models.ScanResult, defPacks defPacks) {
ovalContent.Type = models.NewCveContentType(o.family)
vinfo, ok := r.ScannedCves[defPacks.def.Title]
if !ok {
util.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Title)
logging.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Title)
vinfo = models.VulnInfo{
CveID: defPacks.def.Title,
Confidences: models.Confidences{models.OvalMatch},
@@ -65,9 +76,9 @@ func (o SUSE) update(r *models.ScanResult, defPacks defPacks) {
cveContents := vinfo.CveContents
ctype := models.NewCveContentType(o.family)
if _, ok := vinfo.CveContents[ctype]; ok {
util.Log.Debugf("%s OVAL will be overwritten", defPacks.def.Title)
logging.Log.Debugf("%s OVAL will be overwritten", defPacks.def.Title)
} else {
util.Log.Debugf("%s is also detected by OVAL", defPacks.def.Title)
logging.Log.Debugf("%s is also detected by OVAL", defPacks.def.Title)
cveContents = models.CveContents{}
}
vinfo.Confidences.AppendIfMissing(models.OvalMatch)

View File

@@ -6,10 +6,13 @@ import (
"encoding/json"
"net/http"
"regexp"
"sort"
"time"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
apkver "github.com/knqyf263/go-apk-version"
@@ -70,6 +73,12 @@ func (e *ovalResult) upsert(def ovalmodels.Definition, packName string, fstat fi
return false
}
func (e *ovalResult) Sort() {
sort.SliceStable(e.entries, func(i, j int) bool {
return e.entries[i].def.DefinitionID < e.entries[j].def.DefinitionID
})
}
type request struct {
packName string
versionRelease string
@@ -86,8 +95,7 @@ type response struct {
}
// getDefsByPackNameViaHTTP fetches OVAL information via HTTP
func getDefsByPackNameViaHTTP(r *models.ScanResult) (
relatedDefs ovalResult, err error) {
func getDefsByPackNameViaHTTP(r *models.ScanResult, url string) (relatedDefs ovalResult, err error) {
nReq := len(r.Packages) + len(r.SrcPackages)
reqChan := make(chan request, nReq)
@@ -125,7 +133,7 @@ func getDefsByPackNameViaHTTP(r *models.ScanResult) (
select {
case req := <-reqChan:
url, err := util.URLPathJoin(
config.Conf.OvalDict.URL,
url,
"packs",
r.Family,
r.Release,
@@ -134,7 +142,7 @@ func getDefsByPackNameViaHTTP(r *models.ScanResult) (
if err != nil {
errChan <- err
} else {
util.Log.Debugf("HTTP Request to %s", url)
logging.Log.Debugf("HTTP Request to %s", url)
httpGet(url, req, resChan, errChan)
}
}
@@ -188,19 +196,18 @@ func httpGet(url string, req request, resChan chan<- response, errChan chan<- er
var resp *http.Response
count, retryMax := 0, 3
f := func() (err error) {
// resp, body, errs = gorequest.New().SetDebug(config.Conf.Debug).Get(url).End()
resp, body, errs = gorequest.New().Get(url).End()
resp, body, errs = gorequest.New().Timeout(10 * time.Second).Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
count++
if count == retryMax {
return nil
}
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %w", url, resp, errs)
return xerrors.Errorf("HTTP GET error, url: %s, resp: %v, err: %+v", url, resp, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
util.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %s", t, err)
logging.Log.Warnf("Failed to HTTP GET. retrying in %s seconds. err: %+v", t, err)
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
@@ -244,13 +251,18 @@ func getDefsByPackNameFromOvalDB(driver db.DB, r *models.ScanResult) (relatedDef
})
}
ovalFamily, err := GetFamilyInOval(r.Family)
if err != nil {
return relatedDefs, err
}
for _, req := range requests {
definitions, err := driver.GetByPackName(r.Family, r.Release, req.packName, req.arch)
definitions, err := driver.GetByPackName(ovalFamily, r.Release, req.packName, req.arch)
if err != nil {
return relatedDefs, xerrors.Errorf("Failed to get %s OVAL info by package: %#v, err: %w", r.Family, req, err)
}
for _, def := range definitions {
affected, notFixedYet, fixedIn := isOvalDefAffected(def, req, r.Family, r.RunningKernel, r.EnabledDnfModules)
affected, notFixedYet, fixedIn := isOvalDefAffected(def, req, ovalFamily, r.RunningKernel, r.EnabledDnfModules)
if !affected {
continue
}
@@ -300,7 +312,7 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
if running.Release != "" {
switch family {
case config.RedHat, config.CentOS:
case constant.RedHat, constant.CentOS:
// For kernel related packages, ignore OVAL information with different major versions
if _, ok := kernelRelatedPackNames[ovalPack.Name]; ok {
if util.Major(ovalPack.Version) != util.Major(running.Release) {
@@ -317,7 +329,7 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
// Compare between the installed version vs the version in OVAL
less, err := lessThan(family, req.versionRelease, ovalPack)
if err != nil {
util.Log.Debugf("Failed to parse versions: %s, Ver: %#v, OVAL: %#v, DefID: %s",
logging.Log.Debugf("Failed to parse versions: %s, Ver: %#v, OVAL: %#v, DefID: %s",
err, req.versionRelease, ovalPack, def.DefinitionID)
return false, false, ovalPack.Version
}
@@ -329,12 +341,12 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
// If the version of installed is less than in OVAL
switch family {
case config.RedHat,
config.Amazon,
config.SUSEEnterpriseServer,
config.Debian,
config.Ubuntu,
config.Raspbian:
case constant.RedHat,
constant.Amazon,
constant.SUSEEnterpriseServer,
constant.Debian,
constant.Ubuntu,
constant.Raspbian:
// Use fixed state in OVAL for these distros.
return true, false, ovalPack.Version
}
@@ -353,7 +365,7 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
// compare version: newVer vs oval
less, err := lessThan(family, req.newVersionRelease, ovalPack)
if err != nil {
util.Log.Debugf("Failed to parse versions: %s, NewVer: %#v, OVAL: %#v, DefID: %s",
logging.Log.Debugf("Failed to parse versions: %s, NewVer: %#v, OVAL: %#v, DefID: %s",
err, req.newVersionRelease, ovalPack, def.DefinitionID)
return false, false, ovalPack.Version
}
@@ -365,9 +377,9 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
func lessThan(family, newVer string, packInOVAL ovalmodels.Package) (bool, error) {
switch family {
case config.Debian,
config.Ubuntu,
config.Raspbian:
case constant.Debian,
constant.Ubuntu,
constant.Raspbian:
vera, err := debver.NewVersion(newVer)
if err != nil {
return false, err
@@ -378,7 +390,7 @@ func lessThan(family, newVer string, packInOVAL ovalmodels.Package) (bool, error
}
return vera.LessThan(verb), nil
case config.Alpine:
case constant.Alpine:
vera, err := apkver.NewVersion(newVer)
if err != nil {
return false, err
@@ -389,23 +401,22 @@ func lessThan(family, newVer string, packInOVAL ovalmodels.Package) (bool, error
}
return vera.LessThan(verb), nil
case config.Oracle,
config.SUSEEnterpriseServer,
config.Amazon:
case constant.Oracle,
constant.SUSEEnterpriseServer,
constant.Amazon:
vera := rpmver.NewVersion(newVer)
verb := rpmver.NewVersion(packInOVAL.Version)
return vera.LessThan(verb), nil
case config.RedHat,
config.CentOS:
case constant.RedHat,
constant.CentOS:
vera := rpmver.NewVersion(centOSVersionToRHEL(newVer))
verb := rpmver.NewVersion(packInOVAL.Version)
verb := rpmver.NewVersion(centOSVersionToRHEL(packInOVAL.Version))
return vera.LessThan(verb), nil
default:
util.Log.Errorf("Not implemented yet: %s", family)
return false, xerrors.Errorf("Not implemented yet: %s", family)
}
return false, xerrors.Errorf("Package version comparison not supported: %s", family)
}
var centosVerPattern = regexp.MustCompile(`\.[es]l(\d+)(?:_\d+)?(?:\.centos)?`)
@@ -413,3 +424,71 @@ var centosVerPattern = regexp.MustCompile(`\.[es]l(\d+)(?:_\d+)?(?:\.centos)?`)
func centOSVersionToRHEL(ver string) string {
return centosVerPattern.ReplaceAllString(ver, ".el$1")
}
// NewOVALClient returns a client for OVAL database
func NewOVALClient(family string, cnf config.GovalDictConf) (Client, error) {
switch family {
case constant.Debian, constant.Raspbian:
return NewDebian(&cnf), nil
case constant.Ubuntu:
return NewUbuntu(&cnf), nil
case constant.RedHat:
return NewRedhat(&cnf), nil
case constant.CentOS:
//use RedHat's OVAL
return NewCentOS(&cnf), nil
case constant.Oracle:
return NewOracle(&cnf), nil
case constant.SUSEEnterpriseServer:
// TODO other suse family
return NewSUSE(&cnf), nil
case constant.Alpine:
return NewAlpine(&cnf), nil
case constant.Amazon:
return NewAmazon(&cnf), nil
case constant.FreeBSD, constant.Windows:
return nil, nil
case constant.ServerTypePseudo:
return nil, nil
default:
if family == "" {
return nil, xerrors.New("Probably an error occurred during scanning. Check the error message")
}
return nil, xerrors.Errorf("OVAL for %s is not implemented yet", family)
}
}
// GetFamilyInOval returns the OS family name in OVAL
// For example, CentOS uses Red Hat's OVAL, so return 'redhat'
func GetFamilyInOval(familyInScanResult string) (string, error) {
switch familyInScanResult {
case constant.Debian, constant.Raspbian:
return constant.Debian, nil
case constant.Ubuntu:
return constant.Ubuntu, nil
case constant.RedHat:
return constant.RedHat, nil
case constant.CentOS:
//use RedHat's OVAL
return constant.RedHat, nil
case constant.Oracle:
return constant.Oracle, nil
case constant.SUSEEnterpriseServer:
// TODO other suse family
return constant.SUSEEnterpriseServer, nil
case constant.Alpine:
return constant.Alpine, nil
case constant.Amazon:
return constant.Amazon, nil
case constant.FreeBSD, constant.Windows:
return "", nil
case constant.ServerTypePseudo:
return "", nil
default:
if familyInScanResult == "" {
return "", xerrors.New("Probably an error occurred during scanning. Check the error message")
}
return "", xerrors.Errorf("OVAL for %s is not implemented yet", familyInScanResult)
}
}

View File

@@ -7,7 +7,7 @@ import (
"sort"
"testing"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/constant"
"github.com/future-architect/vuls/models"
ovalmodels "github.com/kotakanbe/goval-dictionary/models"
)
@@ -1030,7 +1030,7 @@ func TestIsOvalDefAffected(t *testing.T) {
// For kernel related packages, ignore OVAL with different major versions
{
in: in{
family: config.CentOS,
family: constant.CentOS,
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
@@ -1054,7 +1054,7 @@ func TestIsOvalDefAffected(t *testing.T) {
},
{
in: in{
family: config.CentOS,
family: constant.CentOS,
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
@@ -1080,7 +1080,7 @@ func TestIsOvalDefAffected(t *testing.T) {
// dnf module
{
in: in{
family: config.RedHat,
family: constant.RedHat,
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
@@ -1106,7 +1106,7 @@ func TestIsOvalDefAffected(t *testing.T) {
// dnf module 2
{
in: in{
family: config.RedHat,
family: constant.RedHat,
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
@@ -1131,7 +1131,7 @@ func TestIsOvalDefAffected(t *testing.T) {
// dnf module 3
{
in: in{
family: config.RedHat,
family: constant.RedHat,
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
@@ -1191,6 +1191,13 @@ func Test_centOSVersionToRHEL(t *testing.T) {
},
want: "grub2-tools-2.02-0.80.el7.x86_64",
},
{
name: "remove minor",
args: args{
ver: "sudo-1.8.23-10.el7_9.1",
},
want: "sudo-1.8.23-10.el7.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1200,3 +1207,131 @@ func Test_centOSVersionToRHEL(t *testing.T) {
})
}
}
func Test_lessThan(t *testing.T) {
type args struct {
family string
newVer string
AffectedPacks ovalmodels.Package
}
tests := []struct {
name string
args args
want bool
}{
{
name: "newVer and ovalmodels.Package both have underscoreMinorversion.",
args: args{
family: "centos",
newVer: "1.8.23-10.el7_9.1",
AffectedPacks: ovalmodels.Package{
Name: "sudo",
Version: "1.8.23-10.el7_9.1",
NotFixedYet: false,
},
},
want: false,
},
{
name: "only newVer has underscoreMinorversion.",
args: args{
family: "centos",
newVer: "1.8.23-10.el7_9.1",
AffectedPacks: ovalmodels.Package{
Name: "sudo",
Version: "1.8.23-10.el7.1",
NotFixedYet: false,
},
},
want: false,
},
{
name: "only ovalmodels.Package has underscoreMinorversion.",
args: args{
family: "centos",
newVer: "1.8.23-10.el7.1",
AffectedPacks: ovalmodels.Package{
Name: "sudo",
Version: "1.8.23-10.el7_9.1",
NotFixedYet: false,
},
},
want: false,
},
{
name: "neither newVer nor ovalmodels.Package have underscoreMinorversion.",
args: args{
family: "centos",
newVer: "1.8.23-10.el7.1",
AffectedPacks: ovalmodels.Package{
Name: "sudo",
Version: "1.8.23-10.el7.1",
NotFixedYet: false,
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, _ := lessThan(tt.args.family, tt.args.newVer, tt.args.AffectedPacks)
if got != tt.want {
t.Errorf("lessThan() = %t, want %t", got, tt.want)
}
})
}
}
func Test_ovalResult_Sort(t *testing.T) {
type fields struct {
entries []defPacks
}
tests := []struct {
name string
fields fields
want fields
}{
{
name: "already sorted",
fields: fields{
entries: []defPacks{
{def: ovalmodels.Definition{DefinitionID: "0"}},
{def: ovalmodels.Definition{DefinitionID: "1"}},
},
},
want: fields{
entries: []defPacks{
{def: ovalmodels.Definition{DefinitionID: "0"}},
{def: ovalmodels.Definition{DefinitionID: "1"}},
},
},
},
{
name: "sort",
fields: fields{
entries: []defPacks{
{def: ovalmodels.Definition{DefinitionID: "1"}},
{def: ovalmodels.Definition{DefinitionID: "0"}},
},
},
want: fields{
entries: []defPacks{
{def: ovalmodels.Definition{DefinitionID: "0"}},
{def: ovalmodels.Definition{DefinitionID: "1"}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &ovalResult{
entries: tt.fields.entries,
}
o.Sort()
if !reflect.DeepEqual(o.entries, tt.want.entries) {
t.Errorf("act %#v, want %#v", o.entries, tt.want.entries)
}
})
}
}

View File

@@ -1,233 +0,0 @@
// +build !scanner
package report
import (
"os"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/util"
gostdb "github.com/knqyf263/gost/db"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
ovaldb "github.com/kotakanbe/goval-dictionary/db"
exploitdb "github.com/mozqnet/go-exploitdb/db"
metasploitdb "github.com/takuzoo3868/go-msfdb/db"
"golang.org/x/xerrors"
)
// DBClient is DB client for reporting
type DBClient struct {
CveDB cvedb.DB
OvalDB ovaldb.DB
GostDB gostdb.DB
ExploitDB exploitdb.DB
MetasploitDB metasploitdb.DB
}
// DBClientConf has a configuration of Vulnerability DBs
type DBClientConf struct {
CveDictCnf config.GoCveDictConf
OvalDictCnf config.GovalDictConf
GostCnf config.GostConf
ExploitCnf config.ExploitConf
MetasploitCnf config.MetasploitConf
DebugSQL bool
}
// NewDBClient returns db clients
func NewDBClient(cnf DBClientConf) (dbclient *DBClient, locked bool, err error) {
cveDriver, locked, err := NewCveDB(cnf)
if locked {
return nil, true, xerrors.Errorf("CveDB is locked: %s",
cnf.OvalDictCnf.SQLite3Path)
} else if err != nil {
return nil, locked, err
}
ovaldb, locked, err := NewOvalDB(cnf)
if locked {
return nil, true, xerrors.Errorf("OvalDB is locked: %s",
cnf.OvalDictCnf.SQLite3Path)
} else if err != nil {
util.Log.Warnf("Unable to use OvalDB: %s, err: %s",
cnf.OvalDictCnf.SQLite3Path, err)
}
gostdb, locked, err := NewGostDB(cnf)
if locked {
return nil, true, xerrors.Errorf("gostDB is locked: %s",
cnf.GostCnf.SQLite3Path)
} else if err != nil {
util.Log.Warnf("Unable to use gostDB: %s, err: %s",
cnf.GostCnf.SQLite3Path, err)
}
exploitdb, locked, err := NewExploitDB(cnf)
if locked {
return nil, true, xerrors.Errorf("exploitDB is locked: %s",
cnf.ExploitCnf.SQLite3Path)
} else if err != nil {
util.Log.Warnf("Unable to use exploitDB: %s, err: %s",
cnf.ExploitCnf.SQLite3Path, err)
}
metasploitdb, locked, err := NewMetasploitDB(cnf)
if locked {
return nil, true, xerrors.Errorf("metasploitDB is locked: %s",
cnf.MetasploitCnf.SQLite3Path)
} else if err != nil {
util.Log.Warnf("Unable to use metasploitDB: %s, err: %s",
cnf.MetasploitCnf.SQLite3Path, err)
}
return &DBClient{
CveDB: cveDriver,
OvalDB: ovaldb,
GostDB: gostdb,
ExploitDB: exploitdb,
MetasploitDB: metasploitdb,
}, false, nil
}
// NewCveDB returns cve db client
func NewCveDB(cnf DBClientConf) (driver cvedb.DB, locked bool, err error) {
if config.Conf.CveDict.IsFetchViaHTTP() {
return nil, false, nil
}
util.Log.Debugf("open cve-dictionary db (%s)", cnf.CveDictCnf.Type)
path := cnf.CveDictCnf.URL
if cnf.CveDictCnf.Type == "sqlite3" {
path = cnf.CveDictCnf.SQLite3Path
if _, err := os.Stat(path); os.IsNotExist(err) {
util.Log.Warnf("--cvedb-path=%s file not found. [CPE-scan](https://vuls.io/docs/en/usage-scan-non-os-packages.html#cpe-scan) needs cve-dictionary. if you specify cpe in config.toml, fetch cve-dictionary before reporting. For details, see `https://github.com/kotakanbe/go-cve-dictionary#deploy-go-cve-dictionary`", path)
return nil, false, nil
}
}
util.Log.Debugf("Open cve-dictionary db (%s): %s", cnf.CveDictCnf.Type, path)
driver, locked, err = cvedb.NewDB(cnf.CveDictCnf.Type, path, cnf.DebugSQL)
if err != nil {
err = xerrors.Errorf("Failed to init CVE DB. err: %w, path: %s", err, path)
return nil, locked, err
}
return driver, false, nil
}
// NewOvalDB returns oval db client
func NewOvalDB(cnf DBClientConf) (driver ovaldb.DB, locked bool, err error) {
if config.Conf.OvalDict.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.OvalDictCnf.URL
if cnf.OvalDictCnf.Type == "sqlite3" {
path = cnf.OvalDictCnf.SQLite3Path
if _, err := os.Stat(path); os.IsNotExist(err) {
util.Log.Warnf("--ovaldb-path=%s file not found", path)
return nil, false, nil
}
}
util.Log.Debugf("Open oval-dictionary db (%s): %s", cnf.OvalDictCnf.Type, path)
driver, locked, err = ovaldb.NewDB("", cnf.OvalDictCnf.Type, path, cnf.DebugSQL)
if err != nil {
err = xerrors.Errorf("Failed to new OVAL DB. err: %w", err)
if locked {
return nil, true, err
}
return nil, false, err
}
return driver, false, nil
}
// NewGostDB returns db client for Gost
func NewGostDB(cnf DBClientConf) (driver gostdb.DB, locked bool, err error) {
if config.Conf.Gost.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.GostCnf.URL
if cnf.GostCnf.Type == "sqlite3" {
path = cnf.GostCnf.SQLite3Path
if _, err := os.Stat(path); os.IsNotExist(err) {
util.Log.Warnf("--gostdb-path=%s file not found. Vuls can detect `patch-not-released-CVE-ID` using gost if the scan target server is Debian, RHEL or CentOS, For details, see `https://github.com/knqyf263/gost#fetch-redhat`", path)
return nil, false, nil
}
}
util.Log.Debugf("Open gost db (%s): %s", cnf.GostCnf.Type, path)
if driver, locked, err = gostdb.NewDB(cnf.GostCnf.Type, path, cnf.DebugSQL); err != nil {
if locked {
util.Log.Errorf("gostDB is locked. err: %+v", err)
return nil, true, err
}
return nil, false, err
}
return driver, false, nil
}
// NewExploitDB returns db client for Exploit
func NewExploitDB(cnf DBClientConf) (driver exploitdb.DB, locked bool, err error) {
if config.Conf.Exploit.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.ExploitCnf.URL
if cnf.ExploitCnf.Type == "sqlite3" {
path = cnf.ExploitCnf.SQLite3Path
if _, err := os.Stat(path); os.IsNotExist(err) {
util.Log.Warnf("--exploitdb-path=%s file not found. Fetch go-exploit-db before reporting if you want to display exploit codes of detected CVE-IDs. For details, see `https://github.com/mozqnet/go-exploitdb`", path)
return nil, false, nil
}
}
util.Log.Debugf("Open exploit db (%s): %s", cnf.ExploitCnf.Type, path)
if driver, locked, err = exploitdb.NewDB(cnf.ExploitCnf.Type, path, cnf.DebugSQL); err != nil {
if locked {
util.Log.Errorf("exploitDB is locked. err: %+v", err)
return nil, true, err
}
return nil, false, err
}
return driver, false, nil
}
// NewMetasploitDB returns db client for Metasploit
func NewMetasploitDB(cnf DBClientConf) (driver metasploitdb.DB, locked bool, err error) {
if config.Conf.Metasploit.IsFetchViaHTTP() {
return nil, false, nil
}
path := cnf.MetasploitCnf.URL
if cnf.MetasploitCnf.Type == "sqlite3" {
path = cnf.MetasploitCnf.SQLite3Path
if _, err := os.Stat(path); os.IsNotExist(err) {
util.Log.Warnf("--msfdb-path=%s file not found. Fetch go-msfdb before reporting if you want to display metasploit modules of detected CVE-IDs. For details, see `https://github.com/takuzoo3868/go-msfdb`", path)
return nil, false, nil
}
}
util.Log.Debugf("Open metasploit db (%s): %s", cnf.MetasploitCnf.Type, path)
if driver, locked, err = metasploitdb.NewDB(cnf.MetasploitCnf.Type, path, cnf.DebugSQL, false); err != nil {
if locked {
util.Log.Errorf("metasploitDB is locked. err: %+v", err)
return nil, true, err
}
return nil, false, err
}
return driver, false, nil
}
// CloseDB close dbs
func (d DBClient) CloseDB() {
if d.CveDB != nil {
if err := d.CveDB.CloseDB(); err != nil {
util.Log.Errorf("Failed to close DB. err: %+v", err)
}
}
if d.OvalDB != nil {
if err := d.OvalDB.CloseDB(); err != nil {
util.Log.Errorf("Failed to close DB. err: %+v", err)
}
}
}

View File

@@ -1,518 +0,0 @@
// +build !scanner
package report
import (
"os"
"strings"
"time"
"github.com/future-architect/vuls/config"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/contrib/owasp-dependency-check/parser"
"github.com/future-architect/vuls/cwe"
"github.com/future-architect/vuls/exploit"
"github.com/future-architect/vuls/github"
"github.com/future-architect/vuls/gost"
"github.com/future-architect/vuls/libmanager"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/msf"
"github.com/future-architect/vuls/oval"
"github.com/future-architect/vuls/util"
"github.com/future-architect/vuls/wordpress"
gostdb "github.com/knqyf263/gost/db"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
cvemodels "github.com/kotakanbe/go-cve-dictionary/models"
ovaldb "github.com/kotakanbe/goval-dictionary/db"
exploitdb "github.com/mozqnet/go-exploitdb/db"
metasploitdb "github.com/takuzoo3868/go-msfdb/db"
"golang.org/x/xerrors"
)
// FillCveInfos fills CVE Detailed Information
func FillCveInfos(dbclient DBClient, rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
// Use the same reportedAt for all rs
reportedAt := time.Now()
for i, r := range rs {
if !c.Conf.RefreshCve && !needToRefreshCve(r) {
util.Log.Info("No need to refresh")
continue
}
if !reuseScannedCves(&r) {
r.ScannedCves = models.VulnInfos{}
}
cpeURIs := []string{}
if len(r.Container.ContainerID) == 0 {
cpeURIs = c.Conf.Servers[r.ServerName].CpeNames
owaspDCXMLPath := c.Conf.Servers[r.ServerName].OwaspDCXMLPath
if owaspDCXMLPath != "" {
cpes, err := parser.Parse(owaspDCXMLPath)
if err != nil {
return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
r.ServerName, owaspDCXMLPath, err)
}
cpeURIs = append(cpeURIs, cpes...)
}
} else {
// runningContainer
if s, ok := c.Conf.Servers[r.ServerName]; ok {
if con, ok := s.Containers[r.Container.Name]; ok {
cpeURIs = con.Cpes
owaspDCXMLPath := con.OwaspDCXMLPath
if owaspDCXMLPath != "" {
cpes, err := parser.Parse(owaspDCXMLPath)
if err != nil {
return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
r.ServerInfo(), owaspDCXMLPath, err)
}
cpeURIs = append(cpeURIs, cpes...)
}
}
}
}
if err := libmanager.DetectLibsCves(&r); err != nil {
return nil, xerrors.Errorf("Failed to fill with Library dependency: %w", err)
}
if err := DetectPkgCves(dbclient, &r); err != nil {
return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err)
}
if err := DetectCpeURIsCves(dbclient.CveDB, &r, cpeURIs); err != nil {
return nil, xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err)
}
repos := c.Conf.Servers[r.ServerName].GitHubRepos
if err := DetectGitHubCves(&r, repos); err != nil {
return nil, xerrors.Errorf("Failed to detect GitHub Cves: %w", err)
}
if err := DetectWordPressCves(&r, &config.Conf.WpScan); err != nil {
return nil, xerrors.Errorf("Failed to detect WordPress Cves: %w", err)
}
if err := FillCveInfo(dbclient, &r); err != nil {
return nil, err
}
r.ReportedBy, _ = os.Hostname()
r.Lang = c.Conf.Lang
r.ReportedAt = reportedAt
r.ReportedVersion = c.Version
r.ReportedRevision = c.Revision
r.Config.Report = c.Conf
r.Config.Report.Servers = map[string]c.ServerInfo{
r.ServerName: c.Conf.Servers[r.ServerName],
}
rs[i] = r
}
// Overwrite the json file every time to clear the fields specified in config.IgnoredJSONKeys
for _, r := range rs {
if s, ok := c.Conf.Servers[r.ServerName]; ok {
r = r.ClearFields(s.IgnoredJSONKeys)
}
if err := overwriteJSONFile(dir, r); err != nil {
return nil, xerrors.Errorf("Failed to write JSON: %w", err)
}
}
if c.Conf.Diff {
prevs, err := loadPrevious(rs)
if err != nil {
return nil, err
}
diff, err := diff(rs, prevs)
if err != nil {
return nil, err
}
for i, r := range diff {
if err := fillCvesWithNvdJvn(dbclient.CveDB, &r); err != nil {
return nil, err
}
rs[i] = r
}
}
for i, r := range rs {
r = r.FilterByCvssOver(c.Conf.CvssScoreOver)
r = r.FilterIgnoreCves()
r = r.FilterUnfixed(c.Conf.IgnoreUnfixed)
r = r.FilterIgnorePkgs()
r = r.FilterInactiveWordPressLibs(c.Conf.WpScan.DetectInactive)
if c.Conf.IgnoreUnscoredCves {
r.ScannedCves = r.ScannedCves.FindScoredVulns()
}
rs[i] = r
}
return rs, nil
}
// DetectPkgCves detects OS pkg cves
func DetectPkgCves(dbclient DBClient, r *models.ScanResult) error {
// Pkg Scan
if r.Release != "" {
// OVAL
if err := detectPkgsCvesWithOval(dbclient.OvalDB, r); err != nil {
return xerrors.Errorf("Failed to detect CVE with OVAL: %w", err)
}
// gost
if err := detectPkgsCvesWithGost(dbclient.GostDB, r); err != nil {
return xerrors.Errorf("Failed to detect CVE with gost: %w", err)
}
} else if reuseScannedCves(r) {
util.Log.Infof("r.Release is empty. Use CVEs as it as.")
} else if r.Family == c.ServerTypePseudo {
util.Log.Infof("pseudo type. Skip OVAL and gost detection")
} else {
return xerrors.Errorf("Failed to fill CVEs. r.Release is empty")
}
for i, v := range r.ScannedCves {
for j, p := range v.AffectedPackages {
if p.NotFixedYet && p.FixState == "" {
p.FixState = "Not fixed yet"
r.ScannedCves[i].AffectedPackages[j] = p
}
}
}
// To keep backward compatibility
// Newer versions use ListenPortStats,
// but older versions of Vuls are set to ListenPorts.
// Set ListenPorts to ListenPortStats to allow newer Vuls to report old results.
for i, pkg := range r.Packages {
for j, proc := range pkg.AffectedProcs {
for _, ipPort := range proc.ListenPorts {
ps, err := models.NewPortStat(ipPort)
if err != nil {
util.Log.Warnf("Failed to parse ip:port: %s, err:%+v", ipPort, err)
continue
}
r.Packages[i].AffectedProcs[j].ListenPortStats = append(
r.Packages[i].AffectedProcs[j].ListenPortStats, *ps)
}
}
}
return nil
}
// DetectGitHubCves fetches CVEs from GitHub Security Alerts
func DetectGitHubCves(r *models.ScanResult, githubConfs map[string]c.GitHubConf) error {
if len(githubConfs) == 0 {
return nil
}
for ownerRepo, setting := range githubConfs {
ss := strings.Split(ownerRepo, "/")
if len(ss) != 2 {
return xerrors.Errorf("Failed to parse GitHub owner/repo: %s", ownerRepo)
}
owner, repo := ss[0], ss[1]
n, err := github.DetectGitHubSecurityAlerts(r, owner, repo, setting.Token)
if err != nil {
return xerrors.Errorf("Failed to access GitHub Security Alerts: %w", err)
}
util.Log.Infof("%s: %d CVEs detected with GHSA %s/%s",
r.FormatServerName(), n, owner, repo)
}
return nil
}
// DetectWordPressCves detects CVEs of WordPress
func DetectWordPressCves(r *models.ScanResult, wpCnf *c.WpScanConf) error {
if len(r.Packages) == 0 {
return nil
}
util.Log.Infof("Detect WordPress CVE. pkgs: %d ", len(r.WordPressPackages))
n, err := wordpress.DetectWordPressCves(r, wpCnf)
if err != nil {
return xerrors.Errorf("Failed to detect WordPress CVE: %w", err)
}
util.Log.Infof("%s: found %d WordPress CVEs", r.FormatServerName(), n)
return nil
}
// FillCveInfo fill scanResult with cve info.
func FillCveInfo(dbclient DBClient, r *models.ScanResult) error {
util.Log.Infof("Fill CVE detailed with gost")
if err := gost.NewClient(r.Family).FillCVEsWithRedHat(dbclient.GostDB, r); err != nil {
return xerrors.Errorf("Failed to fill with gost: %w", err)
}
util.Log.Infof("Fill CVE detailed with CVE-DB")
if err := fillCvesWithNvdJvn(dbclient.CveDB, r); err != nil {
return xerrors.Errorf("Failed to fill with CVE: %w", err)
}
util.Log.Infof("Fill exploit with Exploit-DB")
nExploitCve, err := fillWithExploitDB(dbclient.ExploitDB, r)
if err != nil {
return xerrors.Errorf("Failed to fill with exploit: %w", err)
}
util.Log.Infof("%s: %d exploits are detected",
r.FormatServerName(), nExploitCve)
util.Log.Infof("Fill metasploit module with Metasploit-DB")
nMetasploitCve, err := fillWithMetasploit(dbclient.MetasploitDB, r)
if err != nil {
return xerrors.Errorf("Failed to fill with metasploit: %w", err)
}
util.Log.Infof("%s: %d modules are detected",
r.FormatServerName(), nMetasploitCve)
util.Log.Infof("Fill CWE with NVD")
fillCweDict(r)
return nil
}
// fillCvesWithNvdJvn fills CVE detail with NVD, JVN
func fillCvesWithNvdJvn(driver cvedb.DB, r *models.ScanResult) error {
cveIDs := []string{}
for _, v := range r.ScannedCves {
cveIDs = append(cveIDs, v.CveID)
}
ds, err := CveClient.FetchCveDetails(driver, cveIDs)
if err != nil {
return err
}
for _, d := range ds {
nvd, exploits, mitigations := models.ConvertNvdJSONToModel(d.CveID, d.NvdJSON)
jvn := models.ConvertJvnToModel(d.CveID, d.Jvn)
alerts := fillCertAlerts(&d)
for cveID, vinfo := range r.ScannedCves {
if vinfo.CveID == d.CveID {
if vinfo.CveContents == nil {
vinfo.CveContents = models.CveContents{}
}
for _, con := range []*models.CveContent{nvd, jvn} {
if con != nil && !con.Empty() {
vinfo.CveContents[con.Type] = *con
}
}
vinfo.AlertDict = alerts
vinfo.Exploits = append(vinfo.Exploits, exploits...)
vinfo.Mitigations = append(vinfo.Mitigations, mitigations...)
r.ScannedCves[cveID] = vinfo
break
}
}
}
return nil
}
func fillCertAlerts(cvedetail *cvemodels.CveDetail) (dict models.AlertDict) {
if cvedetail.NvdJSON != nil {
for _, cert := range cvedetail.NvdJSON.Certs {
dict.En = append(dict.En, models.Alert{
URL: cert.Link,
Title: cert.Title,
Team: "us",
})
}
}
if cvedetail.Jvn != nil {
for _, cert := range cvedetail.Jvn.Certs {
dict.Ja = append(dict.Ja, models.Alert{
URL: cert.Link,
Title: cert.Title,
Team: "jp",
})
}
}
return dict
}
// detectPkgsCvesWithOval fetches OVAL database
func detectPkgsCvesWithOval(driver ovaldb.DB, r *models.ScanResult) error {
var ovalClient oval.Client
var ovalFamily string
switch r.Family {
case c.Debian, c.Raspbian:
ovalClient = oval.NewDebian()
ovalFamily = c.Debian
case c.Ubuntu:
ovalClient = oval.NewUbuntu()
ovalFamily = c.Ubuntu
case c.RedHat:
ovalClient = oval.NewRedhat()
ovalFamily = c.RedHat
case c.CentOS:
ovalClient = oval.NewCentOS()
//use RedHat's OVAL
ovalFamily = c.RedHat
case c.Oracle:
ovalClient = oval.NewOracle()
ovalFamily = c.Oracle
case c.SUSEEnterpriseServer:
// TODO other suse family
ovalClient = oval.NewSUSE()
ovalFamily = c.SUSEEnterpriseServer
case c.Alpine:
ovalClient = oval.NewAlpine()
ovalFamily = c.Alpine
case c.Amazon:
ovalClient = oval.NewAmazon()
ovalFamily = c.Amazon
case c.FreeBSD, c.Windows:
return nil
case c.ServerTypePseudo:
return nil
default:
if r.Family == "" {
return xerrors.New("Probably an error occurred during scanning. Check the error message")
}
return xerrors.Errorf("OVAL for %s is not implemented yet", r.Family)
}
if !c.Conf.OvalDict.IsFetchViaHTTP() {
if driver == nil {
return xerrors.Errorf("You have to fetch OVAL data for %s before reporting. For details, see `https://github.com/kotakanbe/goval-dictionary#usage`", r.Family)
}
if err := driver.NewOvalDB(ovalFamily); err != nil {
return xerrors.Errorf("Failed to New Oval DB. err: %w", err)
}
}
util.Log.Debugf("Check whether oval fetched: %s %s", ovalFamily, r.Release)
ok, err := ovalClient.CheckIfOvalFetched(driver, ovalFamily, r.Release)
if err != nil {
return err
}
if !ok {
return xerrors.Errorf("OVAL entries of %s %s are not found. Fetch OVAL before reporting. For details, see `https://github.com/kotakanbe/goval-dictionary#usage`", ovalFamily, r.Release)
}
_, err = ovalClient.CheckIfOvalFresh(driver, ovalFamily, r.Release)
if err != nil {
return err
}
nCVEs, err := ovalClient.FillWithOval(driver, r)
if err != nil {
return err
}
util.Log.Infof("%s: %d CVEs are detected with OVAL", r.FormatServerName(), nCVEs)
return nil
}
func detectPkgsCvesWithGost(driver gostdb.DB, r *models.ScanResult) error {
nCVEs, err := gost.NewClient(r.Family).DetectUnfixed(driver, r, true)
util.Log.Infof("%s: %d unfixed CVEs are detected with gost",
r.FormatServerName(), nCVEs)
return err
}
// fillWithExploitDB fills Exploits with exploit dataabase
// https://github.com/mozqnet/go-exploitdb
func fillWithExploitDB(driver exploitdb.DB, r *models.ScanResult) (nExploitCve int, err error) {
return exploit.FillWithExploit(driver, r)
}
// fillWithMetasploit fills metasploit modules with metasploit database
// https://github.com/takuzoo3868/go-msfdb
func fillWithMetasploit(driver metasploitdb.DB, r *models.ScanResult) (nMetasploitCve int, err error) {
return msf.FillWithMetasploit(driver, r)
}
// DetectCpeURIsCves detects CVEs of given CPE-URIs
func DetectCpeURIsCves(driver cvedb.DB, r *models.ScanResult, cpeURIs []string) error {
nCVEs := 0
if len(cpeURIs) != 0 && driver == nil && !c.Conf.CveDict.IsFetchViaHTTP() {
return xerrors.Errorf("cpeURIs %s specified, but cve-dictionary DB not found. Fetch cve-dictionary before reporting. For details, see `https://github.com/kotakanbe/go-cve-dictionary#deploy-go-cve-dictionary`",
cpeURIs)
}
for _, name := range cpeURIs {
details, err := CveClient.FetchCveDetailsByCpeName(driver, name)
if err != nil {
return err
}
for _, detail := range details {
if val, ok := r.ScannedCves[detail.CveID]; ok {
names := val.CpeURIs
names = util.AppendIfMissing(names, name)
val.CpeURIs = names
val.Confidences.AppendIfMissing(models.CpeNameMatch)
r.ScannedCves[detail.CveID] = val
} else {
v := models.VulnInfo{
CveID: detail.CveID,
CpeURIs: []string{name},
Confidences: models.Confidences{models.CpeNameMatch},
}
r.ScannedCves[detail.CveID] = v
nCVEs++
}
}
}
util.Log.Infof("%s: %d CVEs are detected with CPE", r.FormatServerName(), nCVEs)
return nil
}
func fillCweDict(r *models.ScanResult) {
uniqCweIDMap := map[string]bool{}
for _, vinfo := range r.ScannedCves {
for _, cont := range vinfo.CveContents {
for _, id := range cont.CweIDs {
if strings.HasPrefix(id, "CWE-") {
id = strings.TrimPrefix(id, "CWE-")
uniqCweIDMap[id] = true
}
}
}
}
dict := map[string]models.CweDictEntry{}
for id := range uniqCweIDMap {
entry := models.CweDictEntry{}
if e, ok := cwe.CweDictEn[id]; ok {
if rank, ok := cwe.OwaspTopTen2017[id]; ok {
entry.OwaspTopTen2017 = rank
}
if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
entry.CweTopTwentyfive2019 = rank
}
if rank, ok := cwe.SansTopTwentyfive[id]; ok {
entry.SansTopTwentyfive = rank
}
entry.En = &e
} else {
util.Log.Debugf("CWE-ID %s is not found in English CWE Dict", id)
entry.En = &cwe.Cwe{CweID: id}
}
if c.Conf.Lang == "ja" {
if e, ok := cwe.CweDictJa[id]; ok {
if rank, ok := cwe.OwaspTopTen2017[id]; ok {
entry.OwaspTopTen2017 = rank
}
if rank, ok := cwe.CweTopTwentyfive2019[id]; ok {
entry.CweTopTwentyfive2019 = rank
}
if rank, ok := cwe.SansTopTwentyfive[id]; ok {
entry.SansTopTwentyfive = rank
}
entry.Ja = &e
} else {
util.Log.Debugf("CWE-ID %s is not found in Japanese CWE Dict", id)
entry.Ja = &cwe.Cwe{CweID: id}
}
}
dict[id] = entry
}
r.CweDict = dict
return
}

View File

@@ -1,437 +0,0 @@
package report
import (
"os"
"reflect"
"testing"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/k0kubun/pp"
)
func TestMain(m *testing.M) {
util.Log = util.NewCustomLogger(config.ServerInfo{})
code := m.Run()
os.Exit(code)
}
func TestIsCveInfoUpdated(t *testing.T) {
f := "2006-01-02"
old, _ := time.Parse(f, "2015-12-15")
new, _ := time.Parse(f, "2015-12-16")
type In struct {
cveID string
cur models.ScanResult
prev models.ScanResult
}
var tests = []struct {
in In
expected bool
}{
// NVD compare non-initialized times
{
in: In{
cveID: "CVE-2017-0001",
cur: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2017-0001",
LastModified: time.Time{},
},
),
},
},
},
prev: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2017-0001",
LastModified: time.Time{},
},
),
},
},
},
},
expected: false,
},
// JVN not updated
{
in: In{
cveID: "CVE-2017-0002",
cur: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Jvn,
CveID: "CVE-2017-0002",
LastModified: old,
},
),
},
},
},
prev: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Jvn,
CveID: "CVE-2017-0002",
LastModified: old,
},
),
},
},
},
},
expected: false,
},
// OVAL updated
{
in: In{
cveID: "CVE-2017-0003",
cur: models.ScanResult{
Family: "ubuntu",
ScannedCves: models.VulnInfos{
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2017-0002",
LastModified: new,
},
),
},
},
},
prev: models.ScanResult{
Family: "ubuntu",
ScannedCves: models.VulnInfos{
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2017-0002",
LastModified: old,
},
),
},
},
},
},
expected: true,
},
// OVAL newly detected
{
in: In{
cveID: "CVE-2017-0004",
cur: models.ScanResult{
Family: "redhat",
ScannedCves: models.VulnInfos{
"CVE-2017-0004": {
CveID: "CVE-2017-0004",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2017-0002",
LastModified: old,
},
),
},
},
},
prev: models.ScanResult{
Family: "redhat",
ScannedCves: models.VulnInfos{},
},
},
expected: true,
},
}
for i, tt := range tests {
actual := isCveInfoUpdated(tt.in.cveID, tt.in.prev, tt.in.cur)
if actual != tt.expected {
t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected)
}
}
}
func TestDiff(t *testing.T) {
atCurrent, _ := time.Parse("2006-01-02", "2014-12-31")
atPrevious, _ := time.Parse("2006-01-02", "2014-11-31")
var tests = []struct {
inCurrent models.ScanResults
inPrevious models.ScanResults
out models.ScanResult
}{
{
inCurrent: models.ScanResults{
{
ScannedAt: atCurrent,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
ScannedCves: models.VulnInfos{
"CVE-2012-6702": {
CveID: "CVE-2012-6702",
AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
"CVE-2014-9761": {
CveID: "CVE-2014-9761",
AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
},
Packages: models.Packages{},
Errors: []string{},
Optional: map[string]interface{}{},
},
},
inPrevious: models.ScanResults{
{
ScannedAt: atPrevious,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
ScannedCves: models.VulnInfos{
"CVE-2012-6702": {
CveID: "CVE-2012-6702",
AffectedPackages: models.PackageFixStatuses{{Name: "libexpat1"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
"CVE-2014-9761": {
CveID: "CVE-2014-9761",
AffectedPackages: models.PackageFixStatuses{{Name: "libc-bin"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
},
Packages: models.Packages{},
Errors: []string{},
Optional: map[string]interface{}{},
},
},
out: models.ScanResult{
ScannedAt: atCurrent,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
Packages: models.Packages{},
ScannedCves: models.VulnInfos{},
Errors: []string{},
Optional: map[string]interface{}{},
},
},
{
inCurrent: models.ScanResults{
{
ScannedAt: atCurrent,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
ScannedCves: models.VulnInfos{
"CVE-2016-6662": {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
},
Packages: models.Packages{
"mysql-libs": {
Name: "mysql-libs",
Version: "5.1.73",
Release: "7.el6",
NewVersion: "5.1.73",
NewRelease: "8.el6_8",
Repository: "",
Changelog: models.Changelog{
Contents: "",
Method: "",
},
},
},
},
},
inPrevious: models.ScanResults{
{
ScannedAt: atPrevious,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
ScannedCves: models.VulnInfos{},
},
},
out: models.ScanResult{
ScannedAt: atCurrent,
ServerName: "u16",
Family: "ubuntu",
Release: "16.04",
ScannedCves: models.VulnInfos{
"CVE-2016-6662": {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{{Name: "mysql-libs"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeURIs: []string{},
},
},
Packages: models.Packages{
"mysql-libs": {
Name: "mysql-libs",
Version: "5.1.73",
Release: "7.el6",
NewVersion: "5.1.73",
NewRelease: "8.el6_8",
Repository: "",
Changelog: models.Changelog{
Contents: "",
Method: "",
},
},
},
},
},
}
for i, tt := range tests {
diff, _ := diff(tt.inCurrent, tt.inPrevious)
for _, actual := range diff {
if !reflect.DeepEqual(actual.ScannedCves, tt.out.ScannedCves) {
h := pp.Sprint(actual.ScannedCves)
x := pp.Sprint(tt.out.ScannedCves)
t.Errorf("[%d] cves actual: \n %s \n expected: \n %s", i, h, x)
}
for j := range tt.out.Packages {
if !reflect.DeepEqual(tt.out.Packages[j], actual.Packages[j]) {
h := pp.Sprint(tt.out.Packages[j])
x := pp.Sprint(actual.Packages[j])
t.Errorf("[%d] packages actual: \n %s \n expected: \n %s", i, x, h)
}
}
}
}
}
func TestIsCveFixed(t *testing.T) {
type In struct {
v models.VulnInfo
prev models.ScanResult
}
var tests = []struct {
in In
expected bool
}{
{
in: In{
v: models.VulnInfo{
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{
{
Name: "mysql-libs",
NotFixedYet: false,
},
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
),
},
prev: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2016-6662": {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{
{
Name: "mysql-libs",
NotFixedYet: true,
},
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
),
},
},
},
},
expected: true,
},
{
in: In{
v: models.VulnInfo{
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{
{
Name: "mysql-libs",
NotFixedYet: true,
},
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
),
},
prev: models.ScanResult{
ScannedCves: models.VulnInfos{
"CVE-2016-6662": {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageFixStatuses{
{
Name: "mysql-libs",
NotFixedYet: true,
},
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.Nvd,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
),
},
},
},
},
expected: false,
},
}
for i, tt := range tests {
actual := isCveFixed(tt.in.v, tt.in.prev)
if actual != tt.expected {
t.Errorf("[%d] actual: %t, expected: %t", i, actual, tt.expected)
}
}
}

View File

@@ -1,21 +1,28 @@
package report
package reporter
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"time"
storage "github.com/Azure/azure-sdk-for-go/storage"
"golang.org/x/xerrors"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
// AzureBlobWriter writes results to AzureBlob
type AzureBlobWriter struct{}
type AzureBlobWriter struct {
FormatJSON bool
FormatFullText bool
FormatOneLineText bool
FormatList bool
Gzip bool
config.AzureConf
}
// Write results to Azure Blob storage
func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) {
@@ -23,58 +30,46 @@ func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) {
return nil
}
cli, err := getBlobClient()
cli, err := w.getBlobClient()
if err != nil {
return err
}
if c.Conf.FormatOneLineText {
if w.FormatOneLineText {
timestr := rs[0].ScannedAt.Format(time.RFC3339)
k := fmt.Sprintf(timestr + "/summary.txt")
text := formatOneLineSummary(rs...)
b := []byte(text)
if err := createBlockBlob(cli, k, b); err != nil {
if err := w.createBlockBlob(cli, k, b, w.Gzip); err != nil {
return err
}
}
for _, r := range rs {
key := r.ReportKeyName()
if c.Conf.FormatJSON {
if w.FormatJSON {
k := key + ".json"
var b []byte
if b, err = json.Marshal(r); err != nil {
return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
}
if err := createBlockBlob(cli, k, b); err != nil {
if err := w.createBlockBlob(cli, k, b, w.Gzip); err != nil {
return err
}
}
if c.Conf.FormatList {
if w.FormatList {
k := key + "_short.txt"
b := []byte(formatList(r))
if err := createBlockBlob(cli, k, b); err != nil {
if err := w.createBlockBlob(cli, k, b, w.Gzip); err != nil {
return err
}
}
if c.Conf.FormatFullText {
if w.FormatFullText {
k := key + "_full.txt"
b := []byte(formatFullPlainText(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 xerrors.Errorf("Failed to Marshal to XML: %w", err)
}
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
if err := createBlockBlob(cli, k, allBytes); err != nil {
if err := w.createBlockBlob(cli, k, b, w.Gzip); err != nil {
return err
}
}
@@ -82,9 +77,9 @@ func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) {
return
}
// CheckIfAzureContainerExists check the existence of Azure storage container
func CheckIfAzureContainerExists() error {
cli, err := getBlobClient()
// Validate check the existence of Azure storage container
func (w AzureBlobWriter) Validate() error {
cli, err := w.getBlobClient()
if err != nil {
return err
}
@@ -95,39 +90,39 @@ func CheckIfAzureContainerExists() error {
found := false
for _, con := range r.Containers {
if con.Name == c.Conf.Azure.ContainerName {
if con.Name == w.ContainerName {
found = true
break
}
}
if !found {
return xerrors.Errorf("Container not found. Container: %s", c.Conf.Azure.ContainerName)
return xerrors.Errorf("Container not found. Container: %s", w.ContainerName)
}
return nil
}
func getBlobClient() (storage.BlobStorageClient, error) {
api, err := storage.NewBasicClient(c.Conf.Azure.AccountName, c.Conf.Azure.AccountKey)
func (w AzureBlobWriter) getBlobClient() (storage.BlobStorageClient, error) {
api, err := storage.NewBasicClient(w.AccountName, w.AccountKey)
if err != nil {
return storage.BlobStorageClient{}, err
}
return api.GetBlobService(), nil
}
func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte) error {
func (w AzureBlobWriter) createBlockBlob(cli storage.BlobStorageClient, k string, b []byte, gzip bool) error {
var err error
if c.Conf.GZIP {
if gzip {
if b, err = gz(b); err != nil {
return err
}
k += ".gz"
}
ref := cli.GetContainerReference(c.Conf.Azure.ContainerName)
ref := cli.GetContainerReference(w.ContainerName)
blob := ref.GetBlobReference(k)
if err := blob.CreateBlockBlobFromReader(bytes.NewReader(b), nil); err != nil {
return xerrors.Errorf("Failed to upload data to %s/%s, err: %w",
c.Conf.Azure.ContainerName, k, err)
w.ContainerName, k, err)
}
return nil
}

View File

@@ -1,25 +1,30 @@
package report
package reporter
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// ChatWorkWriter send report to ChatWork
type ChatWorkWriter struct{}
type ChatWorkWriter struct {
Cnf config.ChatWorkConf
Proxy string
}
func (w ChatWorkWriter) Write(rs ...models.ScanResult) (err error) {
conf := config.Conf.ChatWork
for _, r := range rs {
serverInfo := fmt.Sprintf("%s", r.ServerInfo())
if err = chatWorkpostMessage(conf.Room, conf.APIToken, serverInfo); err != nil {
if err = w.chatWorkpostMessage(serverInfo); err != nil {
return err
}
@@ -35,9 +40,9 @@ func (w ChatWorkWriter) Write(rs ...models.ScanResult) (err error) {
vinfo.CveID,
strconv.FormatFloat(maxCvss.Value.Score, 'f', 1, 64),
severity,
vinfo.Summaries(config.Conf.Lang, r.Family)[0].Value)
vinfo.Summaries(r.Lang, r.Family)[0].Value)
if err = chatWorkpostMessage(conf.Room, conf.APIToken, message); err != nil {
if err = w.chatWorkpostMessage(message); err != nil {
return err
}
}
@@ -46,28 +51,26 @@ func (w ChatWorkWriter) Write(rs ...models.ScanResult) (err error) {
return nil
}
func chatWorkpostMessage(room, token, message string) error {
uri := fmt.Sprintf("https://api.chatwork.com/v2/rooms/%s/messages=%s", room, token)
payload := url.Values{
"body": {message},
}
reqs, err := http.NewRequest("POST", uri, strings.NewReader(payload.Encode()))
reqs.Header.Add("X-ChatWorkToken", token)
reqs.Header.Add("Content-Type", "application/x-www-form-urlencoded")
func (w ChatWorkWriter) chatWorkpostMessage(message string) error {
uri := fmt.Sprintf("https://api.chatwork.com/v2/rooms/%s/messages=%s", w.Cnf.Room, w.Cnf.APIToken)
payload := url.Values{"body": {message}}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, strings.NewReader(payload.Encode()))
defer cancel()
if err != nil {
return err
}
client := &http.Client{}
resp, err := client.Do(reqs)
req.Header.Add("X-ChatWorkToken", w.Cnf.APIToken)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client, err := util.GetHTTPClient(w.Proxy)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}

View File

@@ -1,4 +1,4 @@
package report
package reporter
import (
"crypto/tls"
@@ -16,15 +16,19 @@ import (
)
// EMailWriter send mail
type EMailWriter struct{}
type EMailWriter struct {
FormatOneEMail bool
FormatOneLineText bool
FormatList bool
Cnf config.SMTPConf
}
func (w EMailWriter) Write(rs ...models.ScanResult) (err error) {
conf := config.Conf
var message string
sender := NewEMailSender()
sender := NewEMailSender(w.Cnf)
m := map[string]int{}
for _, r := range rs {
if conf.FormatOneEMail {
if w.FormatOneEMail {
message += formatFullPlainText(r) + "\r\n\r\n"
mm := r.ScannedCves.CountGroupBySeverity()
keys := []string{"High", "Medium", "Low", "Unknown"}
@@ -35,19 +39,19 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) {
var subject string
if len(r.Errors) != 0 {
subject = fmt.Sprintf("%s%s An error occurred while scanning",
conf.EMail.SubjectPrefix, r.ServerInfo())
w.Cnf.SubjectPrefix, r.ServerInfo())
} else {
subject = fmt.Sprintf("%s%s %s",
conf.EMail.SubjectPrefix,
w.Cnf.SubjectPrefix,
r.ServerInfo(),
r.ScannedCves.FormatCveSummary())
}
if conf.FormatList {
if w.FormatList {
message = formatList(r)
} else {
message = formatFullPlainText(r)
}
if conf.FormatOneLineText {
if w.FormatOneLineText {
message = fmt.Sprintf("One Line Summary\r\n================\r\n%s", formatOneLineSummary(r))
}
if err := sender.Send(subject, message); err != nil {
@@ -55,24 +59,20 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) {
}
}
}
var summary string
if config.Conf.IgnoreUnscoredCves {
summary = fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
m["High"]+m["Medium"]+m["Low"], m["High"], m["Medium"], m["Low"])
} else {
summary = fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
m["High"]+m["Medium"]+m["Low"]+m["Unknown"],
m["High"], m["Medium"], m["Low"], m["Unknown"])
}
summary := fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
m["High"]+m["Medium"]+m["Low"]+m["Unknown"],
m["High"], m["Medium"], m["Low"], m["Unknown"])
origmessage := message
if conf.FormatOneEMail {
if w.FormatOneEMail {
message = fmt.Sprintf("One Line Summary\r\n================\r\n%s", formatOneLineSummary(rs...))
if !conf.FormatOneLineText {
if !w.FormatOneLineText {
message += fmt.Sprintf("\r\n\r\n%s", origmessage)
}
subject := fmt.Sprintf("%s %s",
conf.EMail.SubjectPrefix, summary)
w.Cnf.SubjectPrefix, summary)
return sender.Send(subject, message)
}
return nil
@@ -123,11 +123,11 @@ func (e *emailSender) sendMail(smtpServerAddr, message string) (err error) {
if ok, param := c.Extension("AUTH"); ok {
authList := strings.Split(param, " ")
auth = e.newSaslClient(authList)
if err = c.Auth(auth); err != nil {
return xerrors.Errorf("Failed to authenticate: %w", err)
}
}
if err = c.Auth(auth); err != nil {
return xerrors.Errorf("Failed to authenticate: %w", err)
}
if err = c.Mail(emailConf.From, nil); err != nil {
return xerrors.Errorf("Failed to send Mail command: %w", err)
}
@@ -196,8 +196,8 @@ func (e *emailSender) Send(subject, body string) (err error) {
}
// NewEMailSender creates emailSender
func NewEMailSender() EMailSender {
return &emailSender{config.Conf.EMail}
func NewEMailSender(cnf config.SMTPConf) EMailSender {
return &emailSender{cnf}
}
func (e *emailSender) newSaslClient(authList []string) sasl.Client {

View File

@@ -1,17 +1,18 @@
package report
package reporter
import (
"bytes"
"encoding/json"
"net/http"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"golang.org/x/xerrors"
)
// HTTPRequestWriter writes results to HTTP request
type HTTPRequestWriter struct{}
type HTTPRequestWriter struct {
Proxy string
}
// Write sends results as HTTP response
func (w HTTPRequestWriter) Write(rs ...models.ScanResult) (err error) {
@@ -20,7 +21,7 @@ func (w HTTPRequestWriter) Write(rs ...models.ScanResult) (err error) {
if err := json.NewEncoder(b).Encode(r); err != nil {
return err
}
_, err = http.Post(c.Conf.HTTP.URL, "application/json; charset=utf-8", b)
_, err = http.Post(w.Proxy, "application/json; charset=utf-8", b)
if err != nil {
return err
}

View File

@@ -1,28 +1,33 @@
package report
package reporter
import (
"bytes"
"encoding/json"
"encoding/xml"
"io/ioutil"
"os"
"path/filepath"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"golang.org/x/xerrors"
)
// LocalFileWriter writes results to a local file.
type LocalFileWriter struct {
CurrentDir string
CurrentDir string
DiffPlus bool
DiffMinus bool
FormatJSON bool
FormatCsv bool
FormatFullText bool
FormatOneLineText bool
FormatList bool
Gzip bool
}
func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
if c.Conf.FormatOneLineText {
if w.FormatOneLineText {
path := filepath.Join(w.CurrentDir, "summary.txt")
text := formatOneLineSummary(rs...)
if err := writeFile(path, []byte(text), 0600); err != nil {
if err := w.writeFile(path, []byte(text), 0600); err != nil {
return xerrors.Errorf(
"Failed to write to file. path: %s, err: %w",
path, err)
@@ -30,77 +35,52 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
}
for _, r := range rs {
r.SortForJSONOutput()
path := filepath.Join(w.CurrentDir, r.ReportFileName())
if c.Conf.FormatJSON {
var p string
if c.Conf.Diff {
if w.FormatJSON {
p := path + ".json"
if w.DiffPlus || w.DiffMinus {
p = path + "_diff.json"
} else {
p = path + ".json"
}
var b []byte
if b, err = json.MarshalIndent(r, "", " "); err != nil {
return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
}
if err := writeFile(p, b, 0600); err != nil {
if err := w.writeFile(p, b, 0600); err != nil {
return xerrors.Errorf("Failed to write JSON. path: %s, err: %w", p, err)
}
}
if c.Conf.FormatList {
var p string
if c.Conf.Diff {
if w.FormatList {
p := path + "_short.txt"
if w.DiffPlus || w.DiffMinus {
p = path + "_short_diff.txt"
} else {
p = path + "_short.txt"
}
if err := writeFile(
if err := w.writeFile(
p, []byte(formatList(r)), 0600); err != nil {
return xerrors.Errorf(
"Failed to write text files. path: %s, err: %w", p, err)
}
}
if c.Conf.FormatFullText {
var p string
if c.Conf.Diff {
if w.FormatFullText {
p := path + "_full.txt"
if w.DiffPlus || w.DiffMinus {
p = path + "_full_diff.txt"
} else {
p = path + "_full.txt"
}
if err := writeFile(
if err := w.writeFile(
p, []byte(formatFullPlainText(r)), 0600); err != nil {
return xerrors.Errorf(
"Failed to write text files. path: %s, err: %w", p, err)
}
}
if c.Conf.FormatXML {
var p string
if c.Conf.Diff {
p = path + "_diff.xml"
} else {
p = path + ".xml"
}
var b []byte
if b, err = xml.Marshal(r); err != nil {
return xerrors.Errorf("Failed to Marshal to XML: %w", err)
}
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
if err := writeFile(p, allBytes, 0600); err != nil {
return xerrors.Errorf("Failed to write XML. path: %s, err: %w", p, err)
}
}
if c.Conf.FormatCsvList {
p := path + "_short.csv"
if c.Conf.Diff {
p = path + "_short_diff.csv"
if w.FormatCsv {
p := path + ".csv"
if w.DiffPlus || w.DiffMinus {
p = path + "_diff.csv"
}
if err := formatCsvList(r, p); err != nil {
return xerrors.Errorf("Failed to write CSV: %s, %w", p, err)
@@ -111,10 +91,10 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
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 {
func (w LocalFileWriter) writeFile(path string, data []byte, perm os.FileMode) (err error) {
if w.Gzip {
data, err = gz(data)
if err != nil {
return err
}
path += ".gz"

View File

@@ -1,9 +1,8 @@
package report
package reporter
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"path"
"time"
@@ -16,23 +15,31 @@ import (
"github.com/aws/aws-sdk-go/service/s3"
"golang.org/x/xerrors"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
// S3Writer writes results to S3
type S3Writer struct{}
type S3Writer struct {
FormatJSON bool
FormatFullText bool
FormatOneLineText bool
FormatList bool
Gzip bool
func getS3() (*s3.S3, error) {
config.AWSConf
}
func (w S3Writer) getS3() (*s3.S3, error) {
ses, err := session.NewSession()
if err != nil {
return nil, err
}
config := &aws.Config{
Region: aws.String(c.Conf.AWS.Region),
Region: aws.String(w.Region),
Credentials: credentials.NewChainCredentials([]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: c.Conf.AWS.Profile},
&credentials.SharedCredentialsProvider{Filename: "", Profile: w.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(ses)},
}),
}
@@ -50,57 +57,45 @@ func (w S3Writer) Write(rs ...models.ScanResult) (err error) {
return nil
}
svc, err := getS3()
svc, err := w.getS3()
if err != nil {
return err
}
if c.Conf.FormatOneLineText {
if w.FormatOneLineText {
timestr := rs[0].ScannedAt.Format(time.RFC3339)
k := fmt.Sprintf(timestr + "/summary.txt")
text := formatOneLineSummary(rs...)
if err := putObject(svc, k, []byte(text)); err != nil {
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
for _, r := range rs {
key := r.ReportKeyName()
if c.Conf.FormatJSON {
if w.FormatJSON {
k := key + ".json"
var b []byte
if b, err = json.Marshal(r); err != nil {
return xerrors.Errorf("Failed to Marshal to JSON: %w", err)
}
if err := putObject(svc, k, b); err != nil {
if err := w.putObject(svc, k, b, w.Gzip); err != nil {
return err
}
}
if c.Conf.FormatList {
if w.FormatList {
k := key + "_short.txt"
text := formatList(r)
if err := putObject(svc, k, []byte(text)); err != nil {
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
if c.Conf.FormatFullText {
if w.FormatFullText {
k := key + "_full.txt"
text := formatFullPlainText(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 xerrors.Errorf("Failed to Marshal to XML: %w", err)
}
allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{})
if err := putObject(svc, k, allBytes); err != nil {
if err := w.putObject(svc, k, []byte(text), w.Gzip); err != nil {
return err
}
}
@@ -108,38 +103,36 @@ func (w S3Writer) Write(rs ...models.ScanResult) (err error) {
return nil
}
// CheckIfBucketExists check the existence of S3 bucket
func CheckIfBucketExists() error {
svc, err := getS3()
// Validate check the existence of S3 bucket
func (w S3Writer) Validate() error {
svc, err := w.getS3()
if err != nil {
return err
}
result, err := svc.ListBuckets(&s3.ListBucketsInput{})
if err != nil {
return xerrors.Errorf(
"Failed to list buckets. err: %w, profile: %s, region: %s",
err, c.Conf.AWS.Profile, c.Conf.AWS.Region)
return xerrors.Errorf("Failed to list buckets. err: %w, profile: %s, region: %s",
err, w.Profile, w.Region)
}
found := false
for _, bucket := range result.Buckets {
if *bucket.Name == c.Conf.AWS.S3Bucket {
if *bucket.Name == w.S3Bucket {
found = true
break
}
}
if !found {
return xerrors.Errorf(
"Failed to find the buckets. profile: %s, region: %s, bucket: %s",
c.Conf.AWS.Profile, c.Conf.AWS.Region, c.Conf.AWS.S3Bucket)
return xerrors.Errorf("Failed to find the buckets. profile: %s, region: %s, bucket: %s",
w.Profile, w.Region, w.S3Bucket)
}
return nil
}
func putObject(svc *s3.S3, k string, b []byte) error {
func (w S3Writer) putObject(svc *s3.S3, k string, b []byte, gzip bool) error {
var err error
if c.Conf.GZIP {
if gzip {
if b, err = gz(b); err != nil {
return err
}
@@ -147,18 +140,18 @@ func putObject(svc *s3.S3, k string, b []byte) error {
}
putObjectInput := &s3.PutObjectInput{
Bucket: aws.String(c.Conf.AWS.S3Bucket),
Key: aws.String(path.Join(c.Conf.AWS.S3ResultsDir, k)),
Bucket: aws.String(w.S3Bucket),
Key: aws.String(path.Join(w.S3ResultsDir, k)),
Body: bytes.NewReader(b),
}
if c.Conf.AWS.S3ServerSideEncryption != "" {
putObjectInput.ServerSideEncryption = aws.String(c.Conf.AWS.S3ServerSideEncryption)
if w.S3ServerSideEncryption != "" {
putObjectInput.ServerSideEncryption = aws.String(w.S3ServerSideEncryption)
}
if _, err := svc.PutObject(putObjectInput); err != nil {
return xerrors.Errorf("Failed to upload data to %s/%s, err: %w",
c.Conf.AWS.S3Bucket, k, err)
w.S3Bucket, k, err)
}
return nil
}

View File

@@ -1,4 +1,4 @@
package report
package reporter
import (
"encoding/json"
@@ -9,13 +9,22 @@ import (
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"github.com/nlopes/slack"
"github.com/parnurzeal/gorequest"
log "github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
// SlackWriter send report to slack
type SlackWriter struct {
FormatOneLineText bool
lang string
osFamily string
Cnf config.SlackConf
Proxy string
}
type message struct {
Text string `json:"text"`
Username string `json:"username"`
@@ -24,15 +33,11 @@ type message struct {
Attachments []slack.Attachment `json:"attachments"`
}
// SlackWriter send report to slack
type SlackWriter struct{}
func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
conf := config.Conf.Slack
channel := conf.Channel
token := conf.LegacyToken
channel := w.Cnf.Channel
for _, r := range rs {
w.lang, w.osFamily = r.Lang, r.Family
if channel == "${servername}" {
channel = fmt.Sprintf("#%s", r.ServerName)
}
@@ -42,7 +47,7 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
// https://api.slack.com/methods/chat.postMessage
maxAttachments := 100
m := map[int][]slack.Attachment{}
for i, a := range toSlackAttachments(r) {
for i, a := range w.toSlackAttachments(r) {
m[i/maxAttachments] = append(m[i/maxAttachments], a)
}
chunkKeys := []int{}
@@ -52,15 +57,15 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
sort.Ints(chunkKeys)
summary := fmt.Sprintf("%s\n%s",
getNotifyUsers(config.Conf.Slack.NotifyUsers),
w.getNotifyUsers(w.Cnf.NotifyUsers),
formatOneLineSummary(r))
// Send slack by API
if 0 < len(token) {
api := slack.New(token)
if 0 < len(w.Cnf.LegacyToken) {
api := slack.New(w.Cnf.LegacyToken)
msgPrms := slack.PostMessageParameters{
Username: conf.AuthUser,
IconEmoji: conf.IconEmoji,
Username: w.Cnf.AuthUser,
IconEmoji: w.Cnf.IconEmoji,
}
var ts string
@@ -72,14 +77,14 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
return err
}
if config.Conf.FormatOneLineText || 0 < len(r.Errors) {
if w.FormatOneLineText || 0 < len(r.Errors) {
continue
}
for _, k := range chunkKeys {
params := slack.PostMessageParameters{
Username: conf.AuthUser,
IconEmoji: conf.IconEmoji,
Username: w.Cnf.AuthUser,
IconEmoji: w.Cnf.IconEmoji,
ThreadTimestamp: ts,
}
if _, _, err = api.PostMessage(
@@ -94,15 +99,15 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
} else {
msg := message{
Text: summary,
Username: conf.AuthUser,
IconEmoji: conf.IconEmoji,
Username: w.Cnf.AuthUser,
IconEmoji: w.Cnf.IconEmoji,
Channel: channel,
}
if err := send(msg); err != nil {
if err := w.send(msg); err != nil {
return err
}
if config.Conf.FormatOneLineText || 0 < len(r.Errors) {
if w.FormatOneLineText || 0 < len(r.Errors) {
continue
}
@@ -114,12 +119,12 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
msg := message{
Text: txt,
Username: conf.AuthUser,
IconEmoji: conf.IconEmoji,
Username: w.Cnf.AuthUser,
IconEmoji: w.Cnf.IconEmoji,
Channel: channel,
Attachments: m[k],
}
if err = send(msg); err != nil {
if err = w.send(msg); err != nil {
return err
}
}
@@ -128,29 +133,28 @@ func (w SlackWriter) Write(rs ...models.ScanResult) (err error) {
return nil
}
func send(msg message) error {
conf := config.Conf.Slack
count, retryMax := 0, 10
func (w SlackWriter) send(msg message) error {
count, retryMax := 0, 10
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()
resp, body, errs := gorequest.New().Timeout(10 * time.Second).Proxy(w.Proxy).Post(w.Cnf.HookURL).Send(string(jsonBody)).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
count++
if count == retryMax {
return nil
}
return xerrors.Errorf(
"HTTP POST error. url: %s, resp: %v, body: %s, err: %w",
conf.HookURL, resp, body, errs)
"HTTP POST error. url: %s, resp: %v, body: %s, err: %+v",
w.Cnf.HookURL, resp, body, errs)
}
return nil
}
notify := func(err error, t time.Duration) {
log.Warnf("Error %s", err)
log.Warn("Retrying in ", t)
logging.Log.Warnf("Error %s", err)
logging.Log.Warn("Retrying in ", t)
}
boff := backoff.NewExponentialBackOff()
if err := backoff.RetryNotify(f, boff, notify); err != nil {
@@ -162,7 +166,7 @@ func send(msg message) error {
return nil
}
func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
func (w SlackWriter) toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
vinfos := r.ScannedCves.ToSortedSlice()
for _, vinfo := range vinfos {
@@ -206,9 +210,9 @@ func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
}
a := slack.Attachment{
Title: vinfo.CveID,
Title: vinfo.CveIDDiffFormat(),
TitleLink: "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID,
Text: attachmentText(vinfo, r.Family, r.CweDict, r.Packages),
Text: w.attachmentText(vinfo, r.CweDict, r.Packages),
MarkdownIn: []string{"text", "pretext"},
Fields: []slack.AttachmentField{
{
@@ -244,11 +248,11 @@ func cvssColor(cvssScore float64) string {
}
}
func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
func (w SlackWriter) attachmentText(vinfo models.VulnInfo, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
maxCvss := vinfo.MaxCvssScore()
vectors := []string{}
scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores(osFamily)...)
scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)
for _, cvss := range scores {
if cvss.Value.Severity == "" {
continue
@@ -277,7 +281,7 @@ func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]m
} else {
if 0 < len(vinfo.DistroAdvisories) {
links := []string{}
for _, v := range vinfo.CveContents.PrimarySrcURLs(config.Conf.Lang, osFamily, vinfo.CveID) {
for _, v := range vinfo.CveContents.PrimarySrcURLs(w.lang, w.osFamily, vinfo.CveID) {
links = append(links, fmt.Sprintf("<%s|%s>", v.Value, v.Type))
}
@@ -312,16 +316,16 @@ func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]m
nwvec,
vinfo.PatchStatus(packs),
strings.Join(vectors, "\n"),
vinfo.Summaries(config.Conf.Lang, osFamily)[0].Value,
vinfo.Summaries(w.lang, w.osFamily)[0].Value,
mitigation,
cweIDs(vinfo, osFamily, cweDict),
w.cweIDs(vinfo, w.osFamily, cweDict),
)
}
func cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
func (w SlackWriter) cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
links := []string{}
for _, c := range vinfo.CveContents.UniqCweIDs(osFamily) {
name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL := cweDict.Get(c.Value, osFamily)
name, url, top10Rank, top10URL, cweTop25Rank, cweTop25URL, sansTop25Rank, sansTop25URL := cweDict.Get(c.Value, w.lang)
line := ""
if top10Rank != "" {
line = fmt.Sprintf("<%s|[OWASP Top %s]>",
@@ -344,7 +348,7 @@ func cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) stri
}
// See testcase
func getNotifyUsers(notifyUsers []string) string {
func (w SlackWriter) getNotifyUsers(notifyUsers []string) string {
slackStyleTexts := []string{}
for _, username := range notifyUsers {
slackStyleTexts = append(slackStyleTexts, fmt.Sprintf("<%s>", username))

View File

@@ -1,4 +1,4 @@
package report
package reporter
import "testing"
@@ -14,7 +14,7 @@ func TestGetNotifyUsers(t *testing.T) {
}
for _, tt := range tests {
actual := getNotifyUsers(tt.in)
actual := SlackWriter{}.getNotifyUsers(tt.in)
if tt.expected != actual {
t.Errorf("expected %s, actual %s", tt.expected, actual)
}

View File

@@ -1,14 +1,20 @@
package report
package reporter
import (
"fmt"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
// StdoutWriter write to stdout
type StdoutWriter struct{}
type StdoutWriter struct {
FormatCsv bool
FormatFullText bool
FormatOneLineText bool
FormatList bool
}
//TODO support -format-jSON
// WriteScanSummary prints Scan summary at the end of scan
func (w StdoutWriter) WriteScanSummary(rs ...models.ScanResult) {
@@ -19,7 +25,7 @@ func (w StdoutWriter) WriteScanSummary(rs ...models.ScanResult) {
}
func (w StdoutWriter) Write(rs ...models.ScanResult) error {
if c.Conf.FormatOneLineText {
if w.FormatOneLineText {
fmt.Print("\n\n")
fmt.Println("One Line Summary")
fmt.Println("================")
@@ -27,13 +33,13 @@ func (w StdoutWriter) Write(rs ...models.ScanResult) error {
fmt.Print("\n")
}
if c.Conf.FormatList || c.Conf.FormatCsvList {
if w.FormatList || w.FormatCsv {
for _, r := range rs {
fmt.Println(formatList(r))
}
}
if c.Conf.FormatFullText {
if w.FormatFullText {
for _, r := range rs {
fmt.Println(formatFullPlainText(r))
}

View File

@@ -1,4 +1,4 @@
package report
package reporter
import (
"fmt"
@@ -12,15 +12,16 @@ import (
)
// SyslogWriter send report to syslog
type SyslogWriter struct{}
type SyslogWriter struct {
Cnf config.SyslogConf
}
func (w SyslogWriter) Write(rs ...models.ScanResult) (err error) {
conf := config.Conf.Syslog
facility, _ := conf.GetFacility()
severity, _ := conf.GetSeverity()
raddr := fmt.Sprintf("%s:%s", conf.Host, conf.Port)
facility, _ := w.Cnf.GetFacility()
severity, _ := w.Cnf.GetSeverity()
raddr := fmt.Sprintf("%s:%s", w.Cnf.Host, w.Cnf.Port)
sysLog, err := syslog.Dial(conf.Protocol, raddr, severity|facility, conf.Tag)
sysLog, err := syslog.Dial(w.Cnf.Protocol, raddr, severity|facility, w.Cnf.Tag)
if err != nil {
return xerrors.Errorf("Failed to initialize syslog client: %w", err)
}
@@ -59,7 +60,7 @@ func (w SyslogWriter) encodeSyslog(result models.ScanResult) (messages []string)
kvPairs = append(kvPairs, fmt.Sprintf(`packages="%s"`, pkgs))
kvPairs = append(kvPairs, fmt.Sprintf(`cve_id="%s"`, cveID))
for _, cvss := range vinfo.Cvss2Scores(result.Family) {
for _, cvss := range vinfo.Cvss2Scores() {
kvPairs = append(kvPairs, fmt.Sprintf(`cvss_score_%s_v2="%.2f"`, cvss.Type, cvss.Value.Score))
kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v2="%s"`, cvss.Type, cvss.Value.Vector))
}
@@ -72,7 +73,7 @@ func (w SyslogWriter) encodeSyslog(result models.ScanResult) (messages []string)
if content, ok := vinfo.CveContents[models.Nvd]; ok {
cwes := strings.Join(content.CweIDs, ",")
kvPairs = append(kvPairs, fmt.Sprintf(`cwe_ids="%s"`, cwes))
if config.Conf.Syslog.Verbose {
if w.Cnf.Verbose {
kvPairs = append(kvPairs, fmt.Sprintf(`source_link="%s"`, content.SourceLink))
kvPairs = append(kvPairs, fmt.Sprintf(`summary="%s"`, content.Summary))
}

View File

@@ -1,4 +1,4 @@
package report
package reporter
import (
"sort"
@@ -51,6 +51,7 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
`scanned_at="2018-06-13 16:10:00 +0000 UTC" server_name="teste01" os_family="ubuntu" os_release="16.04" ipv4_addr="192.168.0.1,10.0.2.15" ipv6_addr="" packages="pkg3,pkg4" cve_id="CVE-2017-0002" cvss_score_nvd_v2="5.00" cvss_vector_nvd_v2="AV:L/AC:L/Au:N/C:N/I:N/A:C" cvss_score_nvd_v3="9.80" cvss_vector_nvd_v3="AV:L/AC:L/Au:N/C:N/I:N/A:C" cwe_ids="CWE-20"`,
},
},
// 1
{
result: models.ScanResult{
ScannedAt: time.Date(2018, 6, 13, 17, 10, 0, 0, time.UTC),
@@ -65,10 +66,11 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
},
CveContents: models.CveContents{
models.RedHat: models.CveContent{
Cvss3Score: 5.0,
Cvss3Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C",
CweIDs: []string{"CWE-284"},
Title: "RHSA-2017:0001: pkg5 security update (Important)",
Cvss3Score: 5.0,
Cvss3Severity: "Medium",
Cvss3Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C",
CweIDs: []string{"CWE-284"},
Title: "RHSA-2017:0001: pkg5 security update (Important)",
},
},
},

Some files were not shown because too many files have changed in this diff Show More