v0.5.0 (no backwards compatibility) (#478)

* Change config.toml, Auto-generate UUIDs, change structure of optional field

* Detect processes affected by update using yum-ps (#482)

Detect processes affected by update using yum-ps

* Detect processes needs restart using checkrestart on Debian and Ubuntu.

* pass cpename by args when calling FillCveInfo (#513)

* fix new db (#502)

* Include Version,Revision in JSON

* Include hostname in JSON

* Update goval-dictionary's commit hash in Gopkg.lock

* Remove README.ja.md

* update packages (#596)

* fix: change ControlPath to .vuls of SSH option (#618)

* feat: checkrestart for Ubuntu and Debian (#622)

* feat: checkrestart for Ubuntu and Debian

* fix: dependencies check logic of configtest

* feat: need-restarting on RedHat

* refactor: Process.ProcName to Process.Name

* feat: detect a systemd service name of need-restarting-process

* feat: detect a systemd service name of need-restarting-process on Ubuntu

* feat: fill a service name of need-restarting-process, init-system

* Support NVD JSON and CVSS3 of JVN (#605)

* fix: compile errors

* fix: Show CVSS3 on TUI

* fix: test cases

* fix: Avoid null in JSON

* Fix maxCvssScore (#621)

* Fix maxCvssScore

* Update vulninfos.go

* fix(init): remove unnecessary log initialization

* refactor(nvd): use only json feed if exists json data. if not, use xml feed

* fix(scan): make Confidence slice

* feat(CWE): Display CWE name to TUI

* feat(cwe): import CWE defs in Japanese

* feat(cwe): add OWASP Top 10 ranking to CWE if applicable

* feat(scan): add -fast-root mode, implement scan/amazon.go

* refactor(const): change const name JVN to Jvn

* feat(scan): add -fast-root mode, implement scan/centos.go

* refactor(dep): update deps

* fix(amazon): deps check

* feat(scan): add -fast-root mode, implement scan/rhel.go

* feat(scan): add -fast-root mode, implement scan/oracle.go

* fix complile err

* feat(scan): add -fast-root mode, implement scan/debian.go

* fix testcase

* fix(amazon): scan using yum

* fix(configtest): change error message, status when no scannnable servers

* Fix(scan): detect init process logic

* fix(tui): display cvss as table format

* fix(scan): parse a output of reboot-notifier on CentOS6.9

* fix(tui): don't display score, vector when score is zero

* fix(scan): add -offline mode to suse scanner

* fix(scan): fix help message

* feat(scan): enable to define scan mode for each servers in config.toml #510

* refactor(config): chagne cpeNames to cpeURIs

* refactor(config): change dependencyCheckXMLPath to owaspDCXMLPath

* fix(config): containers -> containersIncluded, Excluded, containerType

* feature(report): enable to define cpeURIs for each contaner

* feature(report): enable to specify owasp dc xml path for each container

* fix(discover): fix a template displayed at the end of discover

* feature(report): add ignorePkgsRegexp #665

* feature(report): enable to define ignoreCves for each container #666

* fix(report): Displayed nothing in TUI detail area when CweID is nil

* Gopkg.toml diet

* feat(server): support server mode (#678)

* feat(server): support server mode

* Lock go version

* Use the latest kernel release among the installed release when the running kernel release is unknown

* Add TestViaHTTP

* Set logger to go-cve-dictionary client

* Add -to-localfile

* Add -to-http option to report

* Load -to-http conf from config.toml

* Support gost (#676)

* feat(gost): Support RedHat API

* feat(gost): Support Debian Security Tracker

* feat(db): display error msg when SQLite3 is locked at the beginning of reporting.

* feat(gost): TUI

* Only use RedHat information of installed packages

* feat(tui): show mitigation on TUI

* feat(gost): support redis backend

* fix test case

* fix nil pointer when db is nil

* fix(gost): detect vulns of src packages for Debian

* feat(gost): implement redis backend for gost redhat api

* feat(report): display fixState of unfixed pkgs

* fix(report): display distincted cweIDs

* feat(slack): display gost info

* feat(slack): display mitigation

* feat(report): display available patch state as fixed/total

* fix(tui): display - if source of reference is empty

* update deps

* fix(report): key in ScanResult JSON be lowerCamelcase.

* some keys to lower camel

* fix(configtest): dep check logic of yum-plugin-ps

* fix(tui): format

* feat(report): add -format-list option

* fix(report): -format-full-text

* fix(report): report -format-full-text

* fix(report): display v3 score detected by gost

* fix(scan): scan in fast mode if not defined in config.toml

* fix(gost): fetch RedHat data for fixed CVEs

* feat(report): show number of cves detected in each database

* fix(report): show new version as `Unknown` in offline and fast scan mode

* fix(report): fix num of upadtable and fixed

* fix(report): set `Not fixed yet` if packageStatus is empty

* refact(gost): make convertToModel public

* fix(test): fix test case

* update deps

* fix(report): include gost score in MaxCvssScore

* [WIP] feat(config): enable to set options in config.toml instead of cmd opt (#690)

* feat(config): enable to set options in config.toml instead of cmd opt

* fix(config): change Conf.Report.Slack to Conf.Slack

* fix(discover): change tempalte

* fix(report): fix config.toml auto-generate with -uuid

* Add endpoint for health check and change endpoint

* refact(cmd): refactor flag set

* fix(report): enable to specify opts with cmd arg and env value

* fix(scan): enable to parse the release version of amazon linux 2

* add(report) add -to-saas option (#695)

* add(report) add -to-saas option

* ignore other writer if -to-saas

* fix(saas) fix bug

* fix(scan): need-restarting needs internet connection

* fix(scan,configtest): check scan mode

* refactor(scan): change func name

* fix(suse): support offline mode, bug fix on AWS, zypper --no-color

* fix(tui): fix nil pointer when no vulns in tui

* feat(report): enable to define CPE FS format in config.toml

* fix(vet): fix warnings of go vet

* fix(travis): go version to 1.11

* update deps
This commit is contained in:
Kota Kanbe
2018-08-27 13:51:09 +09:00
committed by GitHub
parent d785fc2a54
commit 44fa2c5800
82 changed files with 14019 additions and 2485 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
vuls
.vscode
*.txt
*.json
*.sqlite3*
*.db
tags

View File

@@ -1,7 +1,7 @@
language: go
go:
- "1.10"
- "1.11"
after_success:
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash

View File

@@ -18,18 +18,18 @@ SRCS = $(shell git ls-files '*.go')
PKGS = $(shell go list ./...)
VERSION := $(shell git describe --tags --abbrev=0)
REVISION := $(shell git rev-parse --short HEAD)
LDFLAGS := -X 'main.version=$(VERSION)' \
-X 'main.revision=$(REVISION)'
LDFLAGS := -X 'github.com/future-architect/vuls/config.Version=$(VERSION)' \
-X 'github.com/future-architect/vuls/config.Revision=$(REVISION)'
all: dep build test
all: dep build
dep:
go get -u github.com/golang/dep/...
dep ensure
dep ensure -v
depup:
go get -u github.com/golang/dep/...
dep ensure -update
dep ensure -update -v
build: main.go dep pretest
go build -ldflags "$(LDFLAGS)" -o vuls $<
@@ -49,13 +49,15 @@ vet:
fmt:
gofmt -s -w $(SRCS)
mlint:
$(foreach file,$(SRCS),gometalinter $(file) || exit;)
fmtcheck:
$(foreach file,$(SRCS),gofmt -s -d $(file);)
pretest: lint vet fmtcheck
test: pretest
go install
test:
echo $(PKGS) | xargs go test -cover -v || exit;
unused:

374
Gopkg.lock generated
View File

@@ -2,31 +2,33 @@
[[projects]]
digest = "1:5fa4dde7611b07310d44d104e4fbebb2830cb03a2cf99798433d2f828c7012ca"
digest = "1:46ea9487304f4b3c787f54483ecb13a338d686dcd670db0ab1a112ed0ae2128e"
name = "github.com/Azure/azure-sdk-for-go"
packages = [
"storage",
"version",
]
pruneopts = "UT"
revision = "fbe7db0e3f9793ba3e5704efbab84f51436c136e"
version = "v18.0.0"
revision = "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a"
version = "v19.1.0"
[[projects]]
digest = "1:2de18968f9e072e3c6f9588c651a3d72780a0199f721216d4fbb29cc6f9ac201"
digest = "1:327b9226c8ea5f1cd9952ba859bb7c335cab40fd8781c4a790ef259b0c5fbc40"
name = "github.com/Azure/go-autorest"
packages = [
"autorest",
"autorest/adal",
"autorest/azure",
"autorest/date",
"logger",
"version",
]
pruneopts = "UT"
revision = "1f7cd6cfe0adea687ad44a512dfe76140f804318"
version = "v10.12.0"
revision = "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf"
version = "v10.15.2"
[[projects]]
digest = "1:282acb7fe0ca456de4ea5c3174c2691a2bd74008462e966b69094ad0b2c23e6e"
digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37"
name = "github.com/BurntSushi/toml"
packages = ["."]
pruneopts = "UT"
@@ -42,7 +44,7 @@
version = "v9"
[[projects]]
digest = "1:9ecbc06b996b582f9530fd872c35ab0aa1e828fe97fa44bd24a5efef443a3852"
digest = "1:4f8b94c4cb403af4e7834e2a6455a25a5209dc61771b0d24a820ae9ae30f3f74"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
@@ -64,6 +66,7 @@
"aws/signer/v4",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/eventstream",
@@ -77,8 +80,8 @@
"service/sts",
]
pruneopts = "UT"
revision = "66e86be40287fdef6990768b65f713f4bce11deb"
version = "v1.14.23"
revision = "4324bc9d8865bdb3e6aa86ec7772ca1272d2750e"
version = "v1.15.21"
[[projects]]
digest = "1:0f98f59e9a2f4070d66f0c9c39561f68fcd1dc837b22a852d28d0003aebd1b1e"
@@ -97,12 +100,12 @@
version = "v2.0.0"
[[projects]]
branch = "master"
digest = "1:d8cd4f14785b5ae65100524a29ebba8b9dfc5401020fe7504f80b438bb8e8e0d"
digest = "1:e04c00d619875ce5fa67180891984a9b1fadcc031af36bcd7a3509cbdad1df15"
name = "github.com/cheggaaa/pb"
packages = ["."]
pruneopts = "UT"
revision = "2af8bbdea9e99e83b3ac400d8f6b6d1b8cbbf338"
revision = "c112833d014c77e8bde723fd0158e3156951639f"
version = "v2.0.6"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
@@ -113,15 +116,31 @@
version = "v3.2.0"
[[projects]]
digest = "1:fb46255681497314debedde38b64be32a75bae50bad107586c22f1662bf2d352"
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "UT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:5abd6a22805b1919f6a6bca0ae58b13cef1f3412812f38569978f43ef02743d4"
name = "github.com/go-ini/ini"
packages = ["."]
pruneopts = "UT"
revision = "06f5f3d67269ccec1fe5fe4134ba6e982984f7f5"
version = "v1.37.0"
revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e"
version = "v1.38.2"
[[projects]]
digest = "1:f0e9573388aa6e99341cbea024d15775f8209896f9f7826cd92a3044f1f8ea39"
digest = "1:ad9585b1b4361cbe8e7d8cc31af82ef5f597b9243909daa16f2c225b8af68c46"
name = "github.com/go-redis/redis"
packages = [
".",
@@ -134,8 +153,8 @@
"internal/util",
]
pruneopts = "UT"
revision = "83fb42932f6145ce52df09860384a4653d2d332a"
version = "v6.12.0"
revision = "1614e579ed966441b8e0c3ccea1dd0fbbd93a6ae"
version = "v6.14.0"
[[projects]]
digest = "1:adea5a94903eb4384abef30f3d878dc9ff6b6b5b0722da25b82e5169216dfb61"
@@ -146,12 +165,12 @@
version = "v1.4.0"
[[projects]]
digest = "1:c4a2528ccbcabf90f9f3c464a5fc9e302d592861bbfd0b7135a7de8a943d0406"
digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d"
name = "github.com/go-stack/stack"
packages = ["."]
pruneopts = "UT"
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a"
version = "v1.8.0"
[[projects]]
branch = "master"
@@ -162,16 +181,16 @@
revision = "5bae204cdfb2d92dcc333d56014bae6a2f6c58b1"
[[projects]]
digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e"
digest = "1:cee8e8ac80df6373e7daa11baf1f98c1b6f7242c49ccae7e1ec34a971dc408d9"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
revision = "3ff3320c2a1756a3691521efc290b4701575147c"
version = "v1.3.0"
[[projects]]
branch = "master"
digest = "1:cfff48546cb55d9bfcb60078c1bb27b0cb8fbf403d8d481d7244a920359c8d7b"
digest = "1:4e08dc2383a46b3107f0b34ca338c4459e8fc8ee90e46a60e728aa8a2b21d558"
name = "github.com/gosuri/uitable"
packages = [
".",
@@ -181,6 +200,41 @@
pruneopts = "UT"
revision = "36ee7e946282a3fb1cfecd476ddc9b35d8847e42"
[[projects]]
digest = "1:77395dd3847dac9c45118c668f5dab85aedf0163dc3b38aea6578c5cf0d502f9"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "UT"
revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
version = "v1.0.0"
[[projects]]
digest = "1:c0d19ab64b32ce9fe5cf4ddceba78d5bc9807f0016db6b1183599da3dcc24d10"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "UT"
revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:0f8b63af5601a93b6b6a63a420c857819e98a252369262d8faf66f3566ba294e"
name = "github.com/hashicorp/uuid"
packages = ["."]
pruneopts = "UT"
revision = "ebb0a03e909c9c642a36d2527729104324c44fdb"
[[projects]]
branch = "master"
digest = "1:0778dc7fce1b4669a8bfa7ae506ec1f595b6ab0f8989c1c0d22a8ca1144e9972"
@@ -190,15 +244,15 @@
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
[[projects]]
digest = "1:dacb152d24b21683403e82e4e397e973cdc218befff03b0eba8f7fbffcef325c"
digest = "1:e96640e5b9ce93e2d7ee18f48048483080fd23e72e3c38bc17e9c8b77062031a"
name = "github.com/inconshreveable/log15"
packages = ["."]
pruneopts = "UT"
revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
version = "v2.13"
revision = "67afb5ed74ec82fd7ac8f49d27c509ac6f991970"
version = "v2.14"
[[projects]]
digest = "1:2e8f5374b87a5afd6529a07b4bcfffe5f44ddf005c8eb581ac2863875ece8bae"
digest = "1:8fe19266ce82209076d4a81007ff93f40dd349faca4a917aea59d33956bbd4fd"
name = "github.com/jinzhu/gorm"
packages = [
".",
@@ -241,6 +295,18 @@
revision = "027a6d1765d673d337e687394dbe780dd64e2a1e"
version = "v2.3.0"
[[projects]]
branch = "master"
digest = "1:bdf08c9b41c029c60ba5dc99443a3ce74eedad842cf2adf9c255513f432422e2"
name = "github.com/knqyf263/go-cpe"
packages = [
"common",
"matching",
"naming",
]
pruneopts = "UT"
revision = "659663f6eca2ff32258e282557e7808115ea498a"
[[projects]]
branch = "master"
digest = "1:a9955a589c7f6f28bd5a5f69da3f1e2cc857c23c7605c5fa7b605f065ba8f3fe"
@@ -258,20 +324,30 @@
revision = "74609b86c936dff800c69ec89fcf4bc52d5f13a4"
[[projects]]
branch = "master"
digest = "1:b6262669b4826e969fdba4f78d953c1008553e0f69a9ad3f1b2155db4108775a"
digest = "1:7f4a6b4726da539e615256d19381f7c7326255f80ec19cdbeedcc4d9d57e1831"
name = "github.com/knqyf263/gost"
packages = [
"config",
"db",
"models",
"util",
]
pruneopts = "UT"
revision = "e926a00c01bead2152ea43026159ec5cee7ca998"
version = "v0.1.0"
[[projects]]
digest = "1:a0936d2be9f1dfa483fb8c2251453a9202dca2a374b1e42c7d75036a87d1c69d"
name = "github.com/kotakanbe/go-cve-dictionary"
packages = [
"config",
"db",
"jvn",
"log",
"models",
"nvd",
"util",
]
pruneopts = "UT"
revision = "4ee71e8b928d387bdedfd8f1a0c394bbd84f273e"
revision = "01c566055f7231f55f8551a2ae69569e0a4b9641"
version = "v0.2.0"
[[projects]]
digest = "1:54d3c90db1164399906830313a6fce7770917d7e4a12da8f2d8693d18ff5ef27"
@@ -282,8 +358,7 @@
version = "v0.1.0"
[[projects]]
branch = "master"
digest = "1:ede6d917a203511d006d81c0f491687df3e3ad81a546215831cefa2f39373ba4"
digest = "1:564a03e039dfed4121709e3d76c05c08a9d4291335ca682b5065ae46285a688a"
name = "github.com/kotakanbe/goval-dictionary"
packages = [
"config",
@@ -292,7 +367,8 @@
"models",
]
pruneopts = "UT"
revision = "f3255b2e6a0a38b88eaa25b5696d7e73868e46dc"
revision = "818624daf2658cc177ea93100ff20c5caed064b6"
version = "v0.1.0"
[[projects]]
branch = "master"
@@ -303,8 +379,18 @@
revision = "928f7356cb964637e2489a6ef37eee55181676c5"
[[projects]]
branch = "master"
digest = "1:defb20f3cbe2ee567752a32f4f5a41ed22d648654545147fb5c94e28beec3fca"
digest = "1:faee5b9f53eb1ae4eb04708c040c8c4dd685ce46509671e57a08520a15c54368"
name = "github.com/labstack/gommon"
packages = [
"color",
"log",
]
pruneopts = "UT"
revision = "d6898124de917583f5ff5592ef931d1dfe0ddc05"
version = "0.2.6"
[[projects]]
digest = "1:b18ffc558326ebaed3b4a175617f1e12ed4e3f53d6ebfe5ba372a3de16d22278"
name = "github.com/lib/pq"
packages = [
".",
@@ -312,7 +398,16 @@
"oid",
]
pruneopts = "UT"
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
revision = "4ded0e9383f75c197b3a2aaa6d590ac52df6fd79"
version = "v1.0.0"
[[projects]]
digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "UT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:4e878df5f4e9fd625bf9c9aac77ef7cbfa4a74c01265505527c23470c0e40300"
@@ -339,12 +434,12 @@
version = "v0.0.3"
[[projects]]
digest = "1:e2d1d410fb367567c2b53ed9e2d719d3c1f0891397bb2fa49afd747cfbf1e8e4"
digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = "UT"
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3"
[[projects]]
digest = "1:3cafc6a5a1b8269605d9df4c6956d43d8011fc57f266ca6b9d04da6c09dee548"
@@ -363,12 +458,20 @@
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
branch = "master"
digest = "1:8eb17c2ec4df79193ae65b621cd1c0c4697db3bc317fe6afdc76d7f2746abd05"
digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4"
version = "v1.0.0"
[[projects]]
digest = "1:645110e089152bd0f4a011a2648fbb0e4df5977be73ca605781157ac297f50c4"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "fa473d140ef3c6adf42d6b391fe76707f1f243c8"
version = "v1.0.0"
[[projects]]
branch = "master"
@@ -379,20 +482,28 @@
revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d"
[[projects]]
digest = "1:20de7c621165002b38aa0378d26e4ad0470cf2c8ecc4934f7df50d19d6c0dab0"
digest = "1:ace662a36243b5cdc2f71e654175dc192f903fafbf3411a95bc910c1cad53ce7"
name = "github.com/nlopes/slack"
packages = ["."]
pruneopts = "UT"
revision = "8ab4d0b364ef1e9af5d102531da20d5ec902b6c4"
version = "v0.2.0"
revision = "0db1d5eae1116bf7c8ed96c6749acfbf4daaec3e"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:c9b6e36dbd23f8403a04493376916ca5dad8c01b2da5ae0a05e6a468eb0b6f24"
digest = "1:f335d800550786b6f51ddaedb9d1107a7a72f4a2195e5b039dd7c0e103e119bc"
name = "github.com/nsf/termbox-go"
packages = ["."]
pruneopts = "UT"
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
revision = "b66b20ab708e289ff1eb3e218478302e6aec28ce"
[[projects]]
branch = "master"
digest = "1:4daa045e1e1f3e23f4b07db6880cdf9f259dab65312dfe244a878e6070faaf77"
name = "github.com/olekukonko/tablewriter"
packages = ["."]
pruneopts = "UT"
revision = "d4647c9c7a84d847478d890b816b7d8b62b0b279"
[[projects]]
digest = "1:d776f3e95774a8719f2e57fabbbb33103035fe072dcf6f1864f33abd17b753e5"
@@ -402,6 +513,14 @@
revision = "a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3"
version = "v0.2.15"
[[projects]]
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "UT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
@@ -428,11 +547,70 @@
[[projects]]
branch = "master"
digest = "1:04330644a1b03ccdf9b31b2f98a0978e2697d967892c07c426f58676209428f6"
digest = "1:61ada1b10eccab5329199eaad8fc94048ed689969130010f592a6cc15f9afe39"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "e54a77765aca7bbdd8e56c1c54f60579968b2dc9"
revision = "49fbef4694fb220643e975c02c9547a1cda57c26"
[[projects]]
digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = "UT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "UT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:8a020f916b23ff574845789daee6818daf8d25a4852419aae3f0b12378ba432a"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "UT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:dab83a1bbc7ad3d7a6ba1a1cc1760f25ac38cdf7d96a5cdd55cd915a4f5ceaf9"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:4fc8a61287ccfb4286e1ca5ad2ce3b0b301d746053bf44ac38cf34e40ae10372"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "UT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59"
name = "github.com/valyala/bytebufferpool"
packages = ["."]
pruneopts = "UT"
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
[[projects]]
branch = "master"
digest = "1:268b8bce0064e8c057d7b913605459f9a26dcab864c0886a56d196540fbf003f"
name = "github.com/valyala/fasttemplate"
packages = ["."]
pruneopts = "UT"
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
[[projects]]
branch = "master"
@@ -444,7 +622,7 @@
[[projects]]
branch = "master"
digest = "1:aa95bbd7d9184ef9595b69cbd51b2a96f15627ae9d7d4f762c81860f7824dba6"
digest = "1:6019f7d49498f02cd589d41db388e11470de1f218a0a534c52353788684d8cd9"
name = "golang.org/x/crypto"
packages = [
"curve25519",
@@ -458,11 +636,11 @@
"ssh/terminal",
]
pruneopts = "UT"
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
revision = "614d502a4dac94afa3a6ce146bd1736da82514c6"
[[projects]]
branch = "master"
digest = "1:79d16bf6031c953123613b78dffd6590ef0927cc4b02f5f2ec8b42e8d44c0508"
digest = "1:357e8a9010dc590929a02bc5602c058acea216e4c46089755e618a5a59789604"
name = "golang.org/x/net"
packages = [
"context",
@@ -470,21 +648,21 @@
"publicsuffix",
]
pruneopts = "UT"
revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f"
revision = "8a410e7b638dca158bf9e766925842f6651ff828"
[[projects]]
branch = "master"
digest = "1:72d6244a51be9611f08994aca19677fcc31676b3e7b742c37e129e6ece4ad8fc"
digest = "1:0dafafed83f125cdc945a014b2dec15e5b5d8cd2d77a2d1e3763120b08ab381b"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "1b2967e3c290b7c545b3db0deeda16e9be4f98a2"
revision = "4910a1d54f876d7b22162a85f4d066d3ee649450"
[[projects]]
digest = "1:7509ba4347d1f8de6ae9be8818b0cd1abc3deeffe28aeaf4be6d4b6b5178d9ca"
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
name = "golang.org/x/text"
packages = [
"collate",
@@ -514,6 +692,70 @@
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
[[projects]]
digest = "1:e626376fab8608a972d47e91b3c1bbbddaecaf1d42b82be6dcc52d10a7557893"
name = "gopkg.in/VividCortex/ewma.v1"
packages = ["."]
pruneopts = "UT"
revision = "b24eb346a94c3ba12c1da1e564dbac1b498a77ce"
version = "v1.1.1"
[[projects]]
digest = "1:d8cd4f14785b5ae65100524a29ebba8b9dfc5401020fe7504f80b438bb8e8e0d"
name = "gopkg.in/cheggaaa/pb.v1"
packages = ["."]
pruneopts = "UT"
revision = "2af8bbdea9e99e83b3ac400d8f6b6d1b8cbbf338"
version = "v1.0.25"
[[projects]]
digest = "1:256938e7d43c73bd5e7bb97dd281d1ebe294b2928403ee1fbec96249915d1150"
name = "gopkg.in/cheggaaa/pb.v2"
packages = ["termutil"]
pruneopts = "UT"
revision = "c112833d014c77e8bde723fd0158e3156951639f"
version = "v2.0.6"
[[projects]]
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "gopkg.in/fatih/color.v1"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "gopkg.in/mattn/go-colorable.v0"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
name = "gopkg.in/mattn/go-isatty.v0"
packages = ["."]
pruneopts = "UT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033"
name = "gopkg.in/mattn/go-runewidth.v0"
packages = ["."]
pruneopts = "UT"
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
@@ -527,17 +769,22 @@
"github.com/aws/aws-sdk-go/aws/ec2metadata",
"github.com/aws/aws-sdk-go/aws/session",
"github.com/aws/aws-sdk-go/service/s3",
"github.com/aws/aws-sdk-go/service/sts",
"github.com/boltdb/bolt",
"github.com/cenkalti/backoff",
"github.com/google/subcommands",
"github.com/gosuri/uitable",
"github.com/hashicorp/uuid",
"github.com/howeyc/gopass",
"github.com/jroimartin/gocui",
"github.com/k0kubun/pp",
"github.com/knqyf263/go-cpe/naming",
"github.com/knqyf263/go-deb-version",
"github.com/knqyf263/go-rpm-version",
"github.com/kotakanbe/go-cve-dictionary/config",
"github.com/knqyf263/gost/db",
"github.com/knqyf263/gost/models",
"github.com/kotakanbe/go-cve-dictionary/db",
"github.com/kotakanbe/go-cve-dictionary/log",
"github.com/kotakanbe/go-cve-dictionary/models",
"github.com/kotakanbe/go-pingscanner",
"github.com/kotakanbe/goval-dictionary/db",
@@ -545,6 +792,7 @@
"github.com/kotakanbe/logrus-prefixed-formatter",
"github.com/mitchellh/go-homedir",
"github.com/nlopes/slack",
"github.com/olekukonko/tablewriter",
"github.com/parnurzeal/gorequest",
"github.com/pkg/errors",
"github.com/rifflock/lfshook",

View File

@@ -24,103 +24,6 @@
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/Azure/azure-sdk-for-go"
version = "18.0.0"
[[constraint]]
name = "github.com/BurntSushi/toml"
version = "0.3.0"
[[constraint]]
name = "github.com/asaskevich/govalidator"
version = "9.0.0"
[[constraint]]
name = "github.com/aws/aws-sdk-go"
version = "1.14.23"
[[constraint]]
name = "github.com/boltdb/bolt"
version = "1.3.1"
[[constraint]]
name = "github.com/cenkalti/backoff"
version = "2.0.0"
[[constraint]]
branch = "master"
name = "github.com/google/subcommands"
[[constraint]]
branch = "master"
name = "github.com/gosuri/uitable"
[[constraint]]
branch = "master"
name = "github.com/howeyc/gopass"
[[constraint]]
name = "github.com/jroimartin/gocui"
version = "0.3.0"
[[constraint]]
name = "github.com/k0kubun/pp"
version = "2.3.0"
[[constraint]]
branch = "master"
name = "github.com/knqyf263/go-deb-version"
[[constraint]]
branch = "master"
name = "github.com/knqyf263/go-rpm-version"
[[constraint]]
name = "github.com/kotakanbe/go-cve-dictionary"
branch = "master"
[[constraint]]
name = "github.com/kotakanbe/go-pingscanner"
version = "0.1.0"
[[constraint]]
branch = "master"
name = "github.com/kotakanbe/goval-dictionary"
[[constraint]]
branch = "master"
name = "github.com/kotakanbe/logrus-prefixed-formatter"
[[constraint]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
[[constraint]]
name = "github.com/nlopes/slack"
version = "0.2.0"
[[constraint]]
name = "github.com/parnurzeal/gorequest"
version = "0.2.15"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/rifflock/lfshook"
version = "2.3.0"
[[constraint]]
branch = "master"
name = "github.com/sirupsen/logrus"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[prune]
go-tests = true
unused-packages = true

View File

@@ -63,10 +63,11 @@ Vuls is a tool created to solve the problems listed above. It has the following
- Fast scan and Deep scan
- Fast Scan
- Scan without root privilege
- Scan with No internet access. (RedHat, CentOS, OracleLinux, Ubuntu, Debian)
- Scan with No internet access. (RedHat, CentOS, OracleLinux, Ubuntu and Debian)
- Almost no load on the scan target server
- Deep Scan
- Scan with root privilege
- Detect processes affected by update using yum-ps (RedHat, CentOS, OracleLinux and Amazon Linux)
- Parses the Changelog
Changelog has a history of version changes. When a security issue is fixed, the relevant CVE ID is listed.
By parsing the changelog and analysing the updates between the installed version of software on the server and the newest version of that software

5
cache/bolt.go vendored
View File

@@ -183,9 +183,6 @@ func (b Bolt) PutChangelog(servername, packName, changelog string) error {
if bkt == nil {
return fmt.Errorf("Failed to get Bucket: %s", servername)
}
if err := bkt.Put([]byte(packName), []byte(changelog)); err != nil {
return err
}
return nil
return bkt.Put([]byte(packName), []byte(changelog))
})
}

View File

@@ -33,20 +33,8 @@ import (
// ConfigtestCmd is Subcommand
type ConfigtestCmd struct {
configPath string
logDir string
askKeyPassword bool
containersOnly bool
sshNative bool
sshConfig bool
httpProxy string
timeoutSec int
fast bool
offline bool
deep bool
debug bool
vvv bool
}
// Name return subcommand name
@@ -59,9 +47,6 @@ func (*ConfigtestCmd) Synopsis() string { return "Test configuration" }
func (*ConfigtestCmd) Usage() string {
return `configtest:
configtest
[-fast]
[-offline]
[-deep]
[-config=/path/to/config.toml]
[-log-dir=/path/to/log]
[-ask-key-password]
@@ -83,66 +68,33 @@ func (p *ConfigtestCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
defaultLogDir := util.GetDefaultLogDir()
f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
f.BoolVar(&p.debug, "debug", false, "debug mode")
f.StringVar(&c.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log")
f.BoolVar(&c.Conf.Debug, "debug", false, "debug mode")
f.IntVar(&p.timeoutSec, "timeout", 5*60, "Timeout(Sec)")
f.BoolVar(
&p.askKeyPassword,
"ask-key-password",
false,
f.BoolVar(&p.askKeyPassword, "ask-key-password", false,
"Ask ssh privatekey password before scanning",
)
f.BoolVar(
&p.fast,
"fast",
false,
"Config test for online fast scan mode")
f.StringVar(&c.Conf.HTTPProxy, "http-proxy", "",
"http://proxy-url:port (default: empty)")
f.BoolVar(
&p.offline,
"offline",
false,
"Config test for offline scan mode")
f.BoolVar(&p.deep, "deep", false, "Config test for deep scan mode")
f.StringVar(
&p.httpProxy,
"http-proxy",
"",
"http://proxy-url:port (default: empty)",
)
f.BoolVar(
&p.sshNative,
"ssh-native-insecure",
false,
f.BoolVar(&c.Conf.SSHNative, "ssh-native-insecure", false,
"Use Native Go implementation of SSH. Default: Use the external command")
f.BoolVar(
&p.sshConfig,
"ssh-config",
false,
f.BoolVar(&c.Conf.SSHConfig, "ssh-config", false,
"Use SSH options specified in ssh_config preferentially")
f.BoolVar(
&p.containersOnly,
"containers-only",
false,
f.BoolVar(&c.Conf.ContainersOnly, "containers-only", false,
"Test containers only. Default: Test both of hosts and containers")
f.BoolVar(&p.vvv, "vvv", false, "ssh -vvv")
f.BoolVar(&c.Conf.Vvv, "vvv", false, "ssh -vvv")
}
// Execute execute
func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
// Setup Logger
c.Conf.Debug = p.debug
c.Conf.LogDir = p.logDir
util.Log = util.NewCustomLogger(c.ServerInfo{})
if err := mkdirDotVuls(); err != nil {
@@ -167,19 +119,6 @@ func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa
util.Log.Errorf("Please check README: https://github.com/future-architect/vuls#configuration")
return subcommands.ExitUsageError
}
c.Conf.SSHNative = p.sshNative
c.Conf.SSHConfig = p.sshConfig
c.Conf.HTTPProxy = p.httpProxy
c.Conf.ContainersOnly = p.containersOnly
c.Conf.Fast = p.fast
c.Conf.Offline = p.offline
c.Conf.Deep = p.deep
if !(c.Conf.Fast || c.Conf.Offline || c.Conf.Deep) {
c.Conf.Fast = true
}
c.Conf.Vvv = p.vvv
var servernames []string
if 0 < len(f.Args()) {
@@ -216,12 +155,22 @@ func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa
return subcommands.ExitFailure
}
util.Log.Info("Checking Scan Modes...")
if err := scan.CheckScanModes(); err != nil {
util.Log.Errorf("Fix config.toml: %s", err)
return subcommands.ExitFailure
}
util.Log.Info("Checking dependencies...")
scan.CheckDependencies(p.timeoutSec)
util.Log.Info("Checking sudo settings...")
scan.CheckIfSudoNoPasswd(p.timeoutSec)
scan.PrintSSHableServerNames()
return subcommands.ExitSuccess
util.Log.Info("It can be scanned with fast scan mode even if warn or err messages are displayed due to lack of dependent packages or sudo settings in fast-root or deep scan mode")
if scan.PrintSSHableServerNames() {
return subcommands.ExitSuccess
}
return subcommands.ExitFailure
}

View File

@@ -90,61 +90,130 @@ func (p *DiscoverCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
// Output the template of config.toml
func printConfigToml(ips []string) (err error) {
const tomlTemplate = `
[slack]
hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz"
#legacyToken = "xoxp-11111111111-222222222222-3333333333"
channel = "#channel-name"
#channel = "${servername}"
iconEmoji = ":ghost:"
authUser = "username"
notifyUsers = ["@username"]
[email]
smtpAddr = "smtp.example.com"
smtpPort = "587"
user = "username"
password = "password"
from = "from@example.com"
to = ["to@example.com"]
cc = ["cc@example.com"]
subjectPrefix = "[vuls]"
# TODO Doc Link
[cveDict]
type = "sqlite3"
sqlite3Path = "/path/to/cve.sqlite3"
#url = ""
# TODO Doc Link
[ovalDict]
type = "sqlite3"
sqlite3Path = "/path/to/oval.sqlite3"
#url = ""
# TODO Doc Link
[gost]
type = "sqlite3"
sqlite3Path = "/path/to/gost.sqlite3"
#url = ""
# https://vuls.io/docs/en/usage-settings.html#slack-section
#[slack]
#hookURL = "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz"
##legacyToken = "xoxp-11111111111-222222222222-3333333333"
#channel = "#channel-name"
##channel = "${servername}"
#iconEmoji = ":ghost:"
#authUser = "username"
#notifyUsers = ["@username"]
# https://vuls.io/docs/en/usage-settings.html#email-section
#[email]
#smtpAddr = "smtp.example.com"
#smtpPort = "587"
#user = "username"
#password = "password"
#from = "from@example.com"
#to = ["to@example.com"]
#cc = ["cc@example.com"]
#subjectPrefix = "[vuls]"
# https://vuls.io/docs/en/usage-settings.html#http-section
#[http]
#url = "http://localhost:11234"
# https://vuls.io/docs/en/usage-settings.html#syslog-section
#[syslog]
#protocol = "tcp"
#host = "localhost"
#port = "514"
#tag = "vuls"
#facility = "local0"
#severity = "alert"
#verbose = false
# https://vuls.io/docs/en/usage-report.html#example-put-results-in-s3-bucket
#[aws]
#profile = "default"
#region = "ap-northeast-1"
#s3Bucket = "vuls"
#s3ResultsDir = "/path/to/result"
#s3ServerSideEncryption = "AES256"
# https://vuls.io/docs/en/usage-report.html#example-put-results-in-azure-blob-storage<Paste>
#[azure]
#accountName = "default"
#accountKey = "xxxxxxxxxxxxxx"
#containerName = "vuls"
# https://vuls.io/docs/en/usage-settings.html#stride-section
#[stride]
#hookURL = "xxxxxxxxxxxxxxx"
#authToken = "xxxxxxxxxxxxxx"
# https://vuls.io/docs/en/usage-settings.html#hipchat-section
#[hipchat]
#room = "vuls"
#authToken = "xxxxxxxxxxxxxx"
# https://vuls.io/docs/en/usage-settings.html#chatwork-section
#[chatwork]
#room = "xxxxxxxxxxx"
#apiToken = "xxxxxxxxxxxxxxxxxx"
# https://vuls.io/docs/en/usage-settings.html#default-section
[default]
#port = "22"
#user = "username"
#keyPath = "/home/username/.ssh/id_rsa"
#port = "22"
#user = "username"
#keyPath = "/home/username/.ssh/id_rsa"
#scanMode = ["fast", "fast-root", "deep", "offline"]
#cpeNames = [
# "cpe:/a:rubyonrails:ruby_on_rails:4.2.1",
#]
#dependencyCheckXMLPath = "/tmp/dependency-check-report.xml"
#ignoreCves = ["CVE-2014-6271"]
#optional = [
# ["key", "value"],
#]
#containers = ["${running}"]
#owaspDCXMLPath = "/tmp/dependency-check-report.xml"
#ignoreCves = ["CVE-2014-6271"]
#containerType = "docker" #or "lxd" or "lxc" default: docker
#containersIncluded = ["${running}"]
#containersExcluded = ["container_name_a"]
# https://vuls.io/docs/en/usage-settings.html#servers-section
[servers]
{{- $names:= .Names}}
{{range $i, $ip := .IPs}}
[servers.{{index $names $i}}]
host = "{{$ip}}"
#port = "22"
#user = "root"
#keyPath = "/home/username/.ssh/id_rsa"
#type = "pseudo"
#cpeNames = [
# "cpe:/a:rubyonrails:ruby_on_rails:4.2.1",
#]
#dependencyCheckXMLPath = "/tmp/dependency-check-report.xml"
#ignoreCves = ["CVE-2014-0160"]
#optional = [
# ["key", "value"],
#]
#[servers.{{index $names $i}}.containers]
#type = "docker" #or "lxd" default: docker
#includes = ["${running}"]
#excludes = ["container_name_a", "4aa37a8b63b9"]
host = "{{$ip}}"
#port = "22"
#user = "root"
#keyPath = "/home/username/.ssh/id_rsa"
#scanMode = ["fast", "fast-root", "deep", "offline"]
#type = "pseudo"
#memo = "DB Server"
#cpeNames = [ "cpe:/a:rubyonrails:ruby_on_rails:4.2.1" ]
#owaspDCXMLPath = "/path/to/dependency-check-report.xml"
#ignoreCves = ["CVE-2014-0160"]
#containerType = "docker" #or "lxd" or "lxc" default: docker
#containersIncluded = ["${running}"]
#containersExcluded = ["container_name_a"]
#[servers.{{index $names $i}}.containers.container_name_a]
#cpeNames = [ "cpe:/a:rubyonrails:ruby_on_rails:4.2.1" ]
#owaspDCXMLPath = "/path/to/dependency-check-report.xml"
#ignoreCves = ["CVE-2014-0160"]
#[servers.{{index $names $i}}.optional]
#key = "value1"
{{end}}

View File

@@ -32,11 +32,7 @@ import (
)
// HistoryCmd is Subcommand of list scanned results
type HistoryCmd struct {
debug bool
debugSQL bool
resultsDir string
}
type HistoryCmd struct{}
// Name return subcommand name
func (*HistoryCmd) Name() string { return "history" }
@@ -56,19 +52,16 @@ func (*HistoryCmd) Usage() string {
// SetFlags set flag
func (p *HistoryCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
f.BoolVar(&c.Conf.DebugSQL, "debug-sql", false, "SQL debug mode")
wd, _ := os.Getwd()
defaultResultsDir := filepath.Join(wd, "results")
f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results")
f.StringVar(&c.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results")
}
// Execute execute
func (p *HistoryCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
c.Conf.DebugSQL = p.debugSQL
c.Conf.ResultsDir = p.resultsDir
dirs, err := report.ListValidJSONDirs()
if err != nil {
return subcommands.ExitFailure

View File

@@ -20,73 +20,27 @@ package commands
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/gost"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/oval"
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/util"
"github.com/google/subcommands"
"github.com/k0kubun/pp"
cvelog "github.com/kotakanbe/go-cve-dictionary/log"
)
// ReportCmd is subcommand for reporting
type ReportCmd struct {
lang string
debug bool
debugSQL bool
configPath string
resultsDir string
logDir string
refreshCve bool
cvssScoreOver float64
ignoreUnscoredCves bool
ignoreUnfixed bool
httpProxy string
cveDBType string
cveDBPath string
cveDBURL string
ovalDBType string
ovalDBPath string
ovalDBURL string
toSlack bool
toStride bool
toHipChat bool
toChatWork bool
toEMail bool
toSyslog bool
toLocalFile bool
toS3 bool
toAzureBlob bool
formatJSON bool
formatXML bool
formatOneEMail bool
formatOneLineText bool
formatShortText bool
formatFullText bool
gzip bool
awsProfile string
awsRegion string
awsS3Bucket string
awsS3ResultsDir string
awsS3ServerSideEncryption string
azureAccount string
azureKey string
azureContainer string
pipe bool
diff bool
cvelDict c.GoCveDictConf
ovalDict c.GovalDictConf
gostConf c.GostConf
httpConf c.HTTPConf
}
// Name return subcommand name
@@ -104,17 +58,12 @@ func (*ReportCmd) Usage() string {
[-results-dir=/path/to/results]
[-log-dir=/path/to/log]
[-refresh-cve]
[-cvedb-type=sqlite3|mysql|postgres]
[-cvedb-path=/path/to/cve.sqlite3]
[-cvedb-url=http://127.0.0.1:1323 or DB connection string]
[-ovaldb-type=sqlite3|mysql]
[-ovaldb-path=/path/to/oval.sqlite3]
[-ovaldb-url=http://127.0.0.1:1324 or DB connection string]
[-cvss-over=7]
[-diff]
[-ignore-unscored-cves]
[-ignore-unfixed]
[-to-email]
[-to-http]
[-to-slack]
[-to-stride]
[-to-hipchat]
@@ -122,25 +71,29 @@ func (*ReportCmd) Usage() string {
[-to-localfile]
[-to-s3]
[-to-azure-blob]
[-to-saas]
[-format-json]
[-format-xml]
[-format-one-email]
[-format-one-line-text]
[-format-short-text]
[-format-list]
[-format-full-text]
[-gzip]
[-aws-profile=default]
[-aws-region=us-west-2]
[-aws-s3-bucket=bucket_name]
[-aws-s3-results-dir=/bucket/path/to/results]
[-aws-s3-server-side-encryption=AES256]
[-azure-account=account]
[-azure-key=key]
[-azure-container=container]
[-uuid]
[-http-proxy=http://192.168.0.1:8080]
[-debug]
[-debug-sql]
[-pipe]
[-cvedb-type=sqlite3|mysql|postgres|redis]
[-cvedb-path=/path/to/cve.sqlite3]
[-cvedb-url=http://127.0.0.1:1323 or DB connection string]
[-ovaldb-type=sqlite3|mysql|redis]
[-ovaldb-path=/path/to/oval.sqlite3]
[-ovaldb-url=http://127.0.0.1:1324 or DB connection string]
[-gostdb-type=sqlite3|mysql|redis]
[-gostdb-path=/path/to/gost.sqlite3]
[-gostdb-url=http://127.0.0.1:1325 or DB connection string]
[-http="http://vuls-report-server"]
[RFC3339 datetime format under results dir]
`
@@ -148,218 +101,110 @@ func (*ReportCmd) Usage() string {
// SetFlags set flag
func (p *ReportCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.lang, "lang", "en", "[en|ja]")
f.BoolVar(&p.debug, "debug", false, "debug mode")
f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
f.StringVar(&c.Conf.Lang, "lang", "en", "[en|ja]")
f.BoolVar(&c.Conf.Debug, "debug", false, "debug mode")
f.BoolVar(&c.Conf.DebugSQL, "debug-sql", false, "SQL debug mode")
wd, _ := os.Getwd()
defaultConfPath := filepath.Join(wd, "config.toml")
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
defaultResultsDir := filepath.Join(wd, "results")
f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results")
f.StringVar(&c.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results")
defaultLogDir := util.GetDefaultLogDir()
f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
f.StringVar(&c.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log")
f.BoolVar(
&p.refreshCve,
"refresh-cve",
false,
f.BoolVar(&c.Conf.RefreshCve, "refresh-cve", false,
"Refresh CVE information in JSON file under results dir")
f.StringVar(
&p.cveDBType,
"cvedb-type",
"sqlite3",
"DB type for fetching CVE dictionary (sqlite3, mysql or postgres)")
defaultCveDBPath := filepath.Join(wd, "cve.sqlite3")
f.StringVar(
&p.cveDBPath,
"cvedb-path",
defaultCveDBPath,
"/path/to/sqlite3 (For get cve detail from cve.sqlite3)")
f.StringVar(
&p.cveDBURL,
"cvedb-url",
"",
"http://cve-dictionary.com:1323 or mysql connection string")
f.StringVar(
&p.ovalDBType,
"ovaldb-type",
"sqlite3",
"DB type for fetching OVAL dictionary (sqlite3 or mysql)")
defaultOvalDBPath := filepath.Join(wd, "oval.sqlite3")
f.StringVar(
&p.ovalDBPath,
"ovaldb-path",
defaultOvalDBPath,
"/path/to/sqlite3 (For get oval detail from oval.sqlite3)")
f.StringVar(
&p.ovalDBURL,
"ovaldb-url",
"",
"http://goval-dictionary.com:1324 or mysql connection string")
f.Float64Var(
&p.cvssScoreOver,
"cvss-over",
0,
f.Float64Var(&c.Conf.CvssScoreOver, "cvss-over", 0,
"-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))")
f.BoolVar(&p.diff,
"diff",
false,
fmt.Sprintf("Difference between previous result and current result "))
f.BoolVar(&c.Conf.Diff, "diff", false,
"Difference between previous result and current result ")
f.BoolVar(
&p.ignoreUnscoredCves,
"ignore-unscored-cves",
false,
f.BoolVar(&c.Conf.IgnoreUnscoredCves, "ignore-unscored-cves", false,
"Don't report the unscored CVEs")
f.BoolVar(
&p.ignoreUnfixed,
"ignore-unfixed",
false,
&c.Conf.IgnoreUnfixed, "ignore-unfixed", false,
"Don't report the unfixed CVEs")
f.StringVar(
&p.httpProxy,
"http-proxy",
"",
&c.Conf.HTTPProxy, "http-proxy", "",
"http://proxy-url:port (default: empty)")
f.BoolVar(&p.formatJSON,
"format-json",
false,
fmt.Sprintf("JSON format"))
f.BoolVar(&p.formatXML,
"format-xml",
false,
fmt.Sprintf("XML format"))
f.BoolVar(&p.formatOneEMail,
"format-one-email",
false,
f.BoolVar(&c.Conf.FormatJSON, "format-json", false, "JSON format")
f.BoolVar(&c.Conf.FormatXML, "format-xml", false, "XML format")
f.BoolVar(&c.Conf.FormatOneEMail, "format-one-email", false,
"Send all the host report via only one EMail (Specify with -to-email)")
f.BoolVar(&c.Conf.FormatOneLineText, "format-one-line-text", false,
"One line summary in plain text")
f.BoolVar(&c.Conf.FormatList, "format-list", false, "Display as list format")
f.BoolVar(&c.Conf.FormatFullText, "format-full-text", false,
"Detail report in plain text")
f.BoolVar(&p.formatOneLineText,
"format-one-line-text",
false,
fmt.Sprintf("One line summary in plain text"))
f.BoolVar(&p.formatShortText,
"format-short-text",
false,
fmt.Sprintf("Summary in plain text"))
f.BoolVar(&p.formatFullText,
"format-full-text",
false,
fmt.Sprintf("Detail report in plain text"))
f.BoolVar(&p.gzip, "gzip", false, "gzip compression")
f.BoolVar(&p.toSlack, "to-slack", false, "Send report via Slack")
f.BoolVar(&p.toStride, "to-stride", false, "Send report via Stride")
f.BoolVar(&p.toHipChat, "to-hipchat", false, "Send report via hipchat")
f.BoolVar(&p.toChatWork, "to-chatwork", false, "Send report via chatwork")
f.BoolVar(&p.toEMail, "to-email", false, "Send report via Email")
f.BoolVar(&p.toSyslog, "to-syslog", false, "Send report via Syslog")
f.BoolVar(&p.toLocalFile,
"to-localfile",
false,
fmt.Sprintf("Write report to localfile"))
f.BoolVar(&p.toS3,
"to-s3",
false,
f.BoolVar(&c.Conf.ToSlack, "to-slack", false, "Send report via Slack")
f.BoolVar(&c.Conf.ToStride, "to-stride", false, "Send report via Stride")
f.BoolVar(&c.Conf.ToHipChat, "to-hipchat", false, "Send report via hipchat")
f.BoolVar(&c.Conf.ToChatWork, "to-chatwork", false, "Send report via chatwork")
f.BoolVar(&c.Conf.ToEmail, "to-email", false, "Send report via Email")
f.BoolVar(&c.Conf.ToSyslog, "to-syslog", false, "Send report via Syslog")
f.BoolVar(&c.Conf.ToLocalFile, "to-localfile", false, "Write report to localfile")
f.BoolVar(&c.Conf.ToS3, "to-s3", false,
"Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/xml/txt)")
f.StringVar(&p.awsProfile, "aws-profile", "default", "AWS profile to use")
f.StringVar(&p.awsRegion, "aws-region", "us-east-1", "AWS region to use")
f.StringVar(&p.awsS3Bucket, "aws-s3-bucket", "", "S3 bucket name")
f.StringVar(&p.awsS3ResultsDir, "aws-s3-results-dir", "", "/bucket/path/to/results")
f.StringVar(&p.awsS3ServerSideEncryption, "aws-s3-server-side-encryption", "", "The Server-side encryption algorithm used when storing the reports in S3 (e.g., AES256, aws:kms).")
f.BoolVar(&p.toAzureBlob,
"to-azure-blob",
false,
f.BoolVar(&c.Conf.ToHTTP, "to-http", false, "Send report via HTTP POST")
f.BoolVar(&c.Conf.ToAzureBlob, "to-azure-blob", false,
"Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json/xml/txt)")
f.StringVar(&p.azureAccount,
"azure-account",
"",
"Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified")
f.StringVar(&p.azureKey,
"azure-key",
"",
"Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified")
f.StringVar(&p.azureContainer, "azure-container", "", "Azure storage container name")
f.BoolVar(&c.Conf.ToSaas, "to-saas", false,
"Upload report to Future Vuls(https://vuls.biz/) before report")
f.BoolVar(&c.Conf.GZIP, "gzip", false, "gzip compression")
f.BoolVar(&c.Conf.UUID, "uuid", false,
"Auto generate of scan target servers and then write to config.toml and scan result")
f.BoolVar(&c.Conf.Pipe, "pipe", false, "Use args passed via PIPE")
f.StringVar(&p.cvelDict.Type, "cvedb-type", "sqlite3",
"DB type of go-cve-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.cvelDict.SQLite3Path, "cvedb-sqlite3-path", "", "/path/to/sqlite3")
f.StringVar(&p.cvelDict.URL, "cvedb-url", "",
"http://go-cve-dictionary.com:1323 or DB connection string")
f.StringVar(&p.ovalDict.Type, "ovaldb-type", "",
"DB type of goval-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.ovalDict.SQLite3Path, "ovaldb-sqlite3-path", "", "/path/to/sqlite3")
f.StringVar(&p.ovalDict.URL, "ovaldb-url", "",
"http://goval-dictionary.com:1324 or DB connection string")
f.StringVar(&p.gostConf.Type, "gostdb-type", "",
"DB type of gost (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.gostConf.SQLite3Path, "gostdb-sqlite3-path", "", "/path/to/sqlite3")
f.StringVar(&p.gostConf.URL, "gostdb-url", "",
"http://gost.com:1325 or DB connection string")
f.StringVar(&p.httpConf.URL, "http", "", "-to-http http://vuls-report")
f.BoolVar(
&p.pipe,
"pipe",
false,
"Use args passed via PIPE")
}
// Execute execute
func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
c.Conf.Debug = p.debug
c.Conf.DebugSQL = p.debugSQL
c.Conf.LogDir = p.logDir
util.Log = util.NewCustomLogger(c.ServerInfo{})
cvelog.SetLogger(c.Conf.LogDir, false, c.Conf.Debug, false)
if err := c.Load(p.configPath, ""); err != nil {
util.Log.Errorf("Error loading %s, %s", p.configPath, err)
return subcommands.ExitUsageError
}
c.Conf.Lang = p.lang
c.Conf.ResultsDir = p.resultsDir
c.Conf.RefreshCve = p.refreshCve
c.Conf.CveDBType = p.cveDBType
c.Conf.CveDBPath = p.cveDBPath
c.Conf.CveDBURL = p.cveDBURL
c.Conf.OvalDBType = p.ovalDBType
c.Conf.OvalDBPath = p.ovalDBPath
c.Conf.OvalDBURL = p.ovalDBURL
c.Conf.CvssScoreOver = p.cvssScoreOver
c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves
c.Conf.IgnoreUnfixed = p.ignoreUnfixed
c.Conf.HTTPProxy = p.httpProxy
c.Conf.ToSlack = p.toSlack
c.Conf.ToStride = p.toStride
c.Conf.ToHipChat = p.toHipChat
c.Conf.ToChatWork = p.toChatWork
c.Conf.ToEmail = p.toEMail
c.Conf.ToSyslog = p.toSyslog
c.Conf.ToLocalFile = p.toLocalFile
c.Conf.ToS3 = p.toS3
c.Conf.ToAzureBlob = p.toAzureBlob
c.Conf.FormatXML = p.formatXML
c.Conf.FormatJSON = p.formatJSON
c.Conf.FormatOneEMail = p.formatOneEMail
c.Conf.FormatOneLineText = p.formatOneLineText
c.Conf.FormatShortText = p.formatShortText
c.Conf.FormatFullText = p.formatFullText
c.Conf.GZIP = p.gzip
c.Conf.Diff = p.diff
c.Conf.Pipe = p.pipe
c.Conf.CveDict.Overwrite(p.cvelDict)
c.Conf.OvalDict.Overwrite(p.ovalDict)
c.Conf.Gost.Overwrite(p.gostConf)
c.Conf.HTTP.Overwrite(p.httpConf)
var dir string
var err error
if p.diff {
if c.Conf.Diff {
dir, err = report.JSONDir([]string{})
} else {
dir, err = report.JSONDir(f.Args())
@@ -374,107 +219,87 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
report.StdoutWriter{},
}
if p.toSlack {
if c.Conf.ToSlack {
reports = append(reports, report.SlackWriter{})
}
if p.toStride {
if c.Conf.ToStride {
reports = append(reports, report.StrideWriter{})
}
if p.toHipChat {
if c.Conf.ToHipChat {
reports = append(reports, report.HipChatWriter{})
}
if p.toChatWork {
if c.Conf.ToChatWork {
reports = append(reports, report.ChatWorkWriter{})
}
if p.toEMail {
if c.Conf.ToEmail {
reports = append(reports, report.EMailWriter{})
}
if p.toSyslog {
if c.Conf.ToSyslog {
reports = append(reports, report.SyslogWriter{})
}
if p.toLocalFile {
if c.Conf.ToHTTP {
reports = append(reports, report.HTTPRequestWriter{})
}
if c.Conf.ToLocalFile {
reports = append(reports, report.LocalFileWriter{
CurrentDir: dir,
})
}
if p.toS3 {
c.Conf.AwsRegion = p.awsRegion
c.Conf.AwsProfile = p.awsProfile
c.Conf.S3Bucket = p.awsS3Bucket
c.Conf.S3ResultsDir = p.awsS3ResultsDir
c.Conf.S3ServerSideEncryption = p.awsS3ServerSideEncryption
if c.Conf.ToS3 {
if err := report.CheckIfBucketExists(); err != nil {
util.Log.Errorf("Check if there is a bucket beforehand: %s, err: %s", c.Conf.S3Bucket, err)
util.Log.Errorf("Check if there is a bucket beforehand: %s, err: %s",
c.Conf.AWS.S3Bucket, err)
return subcommands.ExitUsageError
}
reports = append(reports, report.S3Writer{})
}
if p.toAzureBlob {
c.Conf.AzureAccount = p.azureAccount
if len(c.Conf.AzureAccount) == 0 {
c.Conf.AzureAccount = os.Getenv("AZURE_STORAGE_ACCOUNT")
if c.Conf.ToAzureBlob {
if len(c.Conf.Azure.AccountName) == 0 {
c.Conf.Azure.AccountName = os.Getenv("AZURE_STORAGE_ACCOUNT")
}
c.Conf.AzureKey = p.azureKey
if len(c.Conf.AzureKey) == 0 {
c.Conf.AzureKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
if len(c.Conf.Azure.AccountKey) == 0 {
c.Conf.Azure.AccountKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
}
c.Conf.AzureContainer = p.azureContainer
if len(c.Conf.AzureContainer) == 0 {
if len(c.Conf.Azure.ContainerName) == 0 {
util.Log.Error("Azure storage container name is required with -azure-container option")
return subcommands.ExitUsageError
}
if err := report.CheckIfAzureContainerExists(); err != nil {
util.Log.Errorf("Check if there is a container beforehand: %s, err: %s", c.Conf.AzureContainer, err)
util.Log.Errorf("Check if there is a container beforehand: %s, err: %s",
c.Conf.Azure.ContainerName, err)
return subcommands.ExitUsageError
}
reports = append(reports, report.AzureBlobWriter{})
}
if !(p.formatJSON || p.formatOneLineText ||
p.formatShortText || p.formatFullText || p.formatXML) {
c.Conf.FormatShortText = true
if c.Conf.ToSaas {
if !c.Conf.UUID {
util.Log.Errorf("If you use the -to-saas option, you need to enable the uuid option")
return subcommands.ExitUsageError
}
reports = append(reports, report.SaasWriter{})
}
if !(c.Conf.FormatJSON || c.Conf.FormatOneLineText ||
c.Conf.FormatList || c.Conf.FormatFullText || c.Conf.FormatXML) {
c.Conf.FormatList = true
}
util.Log.Info("Validating config...")
if !c.Conf.ValidateOnReport() {
return subcommands.ExitUsageError
}
if err := report.CveClient.CheckHealth(); err != nil {
util.Log.Errorf("CVE HTTP server is not running. err: %s", err)
util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with -cvedb-path option")
return subcommands.ExitFailure
}
if c.Conf.CveDBURL != "" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDBURL)
} else {
if c.Conf.CveDBType == "sqlite3" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDBPath)
}
}
if c.Conf.OvalDBURL != "" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDBURL)
err := oval.Base{}.CheckHTTPHealth()
if err != nil {
util.Log.Errorf("OVAL HTTP server is not running. err: %s", err)
util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with -ovaldb-path option")
return subcommands.ExitFailure
}
} else {
if c.Conf.OvalDBType == "sqlite3" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDBPath)
}
}
var loaded models.ScanResults
if loaded, err = report.LoadScanResults(dir); err != nil {
@@ -493,9 +318,86 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
}
}
if res, err = report.FillCveInfos(res, dir); err != nil {
util.Log.Error(err)
return subcommands.ExitFailure
for _, r := range res {
util.Log.Debugf("%s: %s",
r.ServerInfo(),
pp.Sprintf("%s", c.Conf.Servers[r.ServerName]))
}
if c.Conf.UUID {
// Ensure UUIDs of scan target servers in config.toml
if err := report.EnsureUUIDs(p.configPath, res); err != nil {
util.Log.Errorf("Failed to ensure UUIDs: %s", err)
return subcommands.ExitFailure
}
}
if !c.Conf.ToSaas {
util.Log.Info("Validating db config...")
if !c.Conf.ValidateOnReportDB() {
return subcommands.ExitUsageError
}
if err := report.CveClient.CheckHealth(); err != nil {
util.Log.Errorf("CVE HTTP server is not running. err: %s", err)
util.Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with -cvedb-path option instead of -cvedb-url")
return subcommands.ExitFailure
}
if c.Conf.CveDict.URL != "" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDict.URL)
} else {
if c.Conf.CveDict.Type == "sqlite3" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDict.SQLite3Path)
}
}
if c.Conf.OvalDict.URL != "" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDict.URL)
err := oval.Base{}.CheckHTTPHealth()
if err != nil {
util.Log.Errorf("OVAL HTTP server is not running. err: %s", err)
util.Log.Errorf("Run goval-dictionary as server mode before reporting or run with -ovaldb-path option instead of -ovaldb-url")
return subcommands.ExitFailure
}
} else {
if c.Conf.OvalDict.Type == "sqlite3" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDict.SQLite3Path)
}
}
if c.Conf.Gost.URL != "" {
util.Log.Infof("gost: %s", c.Conf.Gost.URL)
err := gost.Base{}.CheckHTTPHealth()
if err != nil {
util.Log.Errorf("gost HTTP server is not running. err: %s", err)
util.Log.Errorf("Run gost as server mode before reporting or run with -gostdb-path option instead of -gostdb-url")
return subcommands.ExitFailure
}
} else {
if c.Conf.Gost.Type == "sqlite3" {
util.Log.Infof("gost: %s", c.Conf.Gost.SQLite3Path)
}
}
dbclient, locked, err := report.NewDBClient(report.DBClientConf{
CveDictCnf: c.Conf.CveDict,
OvalDictCnf: c.Conf.OvalDict,
GostCnf: c.Conf.Gost,
DebugSQL: c.Conf.DebugSQL,
})
if locked {
util.Log.Errorf("SQLite3 is locked. Close other DB connections and try again: %s", err)
return subcommands.ExitFailure
}
if err != nil {
util.Log.Errorf("Failed to init DB Clients: %s", err)
return subcommands.ExitFailure
}
defer dbclient.CloseDB()
if res, err = report.FillCveInfos(*dbclient, res, dir); err != nil {
util.Log.Error(err)
return subcommands.ExitFailure
}
}
for _, w := range reports {
@@ -504,5 +406,6 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}

View File

@@ -35,22 +35,8 @@ import (
// ScanCmd is Subcommand of host discovery mode
type ScanCmd struct {
debug bool
configPath string
resultsDir string
logDir string
cacheDBPath string
httpProxy string
askKeyPassword bool
containersOnly bool
fast bool
offline bool
deep bool
skipBroken bool
sshNative bool
sshConfig bool
pipe bool
vvv bool
timeoutSec int
scanTimeoutSec int
}
@@ -65,9 +51,6 @@ func (*ScanCmd) Synopsis() string { return "Scan vulnerabilities" }
func (*ScanCmd) Usage() string {
return `scan:
scan
[-fast]
[-offline]
[-deep]
[-config=/path/to/config.toml]
[-results-dir=/path/to/results]
[-log-dir=/path/to/log]
@@ -90,101 +73,49 @@ func (*ScanCmd) Usage() string {
// SetFlags set flag
func (p *ScanCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.debug, "debug", false, "debug mode")
f.BoolVar(&c.Conf.Debug, "debug", false, "debug mode")
wd, _ := os.Getwd()
defaultConfPath := filepath.Join(wd, "config.toml")
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
defaultResultsDir := filepath.Join(wd, "results")
f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results")
f.StringVar(&c.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results")
defaultLogDir := util.GetDefaultLogDir()
f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
f.StringVar(&c.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log")
defaultCacheDBPath := filepath.Join(wd, "cache.db")
f.StringVar(
&p.cacheDBPath,
"cachedb-path",
defaultCacheDBPath,
f.StringVar(&c.Conf.CacheDBPath, "cachedb-path", defaultCacheDBPath,
"/path/to/cache.db (local cache of changelog for Ubuntu/Debian)")
f.BoolVar(
&p.sshNative,
"ssh-native-insecure",
false,
f.BoolVar(&c.Conf.SSHNative, "ssh-native-insecure", false,
"Use Native Go implementation of SSH. Default: Use the external command")
f.BoolVar(
&p.sshConfig,
"ssh-config",
false,
f.BoolVar(&c.Conf.SSHConfig, "ssh-config", false,
"Use SSH options specified in ssh_config preferentially")
f.BoolVar(
&p.containersOnly,
"containers-only",
false,
f.BoolVar(&c.Conf.ContainersOnly, "containers-only", false,
"Scan containers only. Default: Scan both of hosts and containers")
f.BoolVar(
&p.skipBroken,
"skip-broken",
false,
f.BoolVar(&c.Conf.SkipBroken, "skip-broken", false,
"[For CentOS] yum update changelog with --skip-broken option")
f.StringVar(
&p.httpProxy,
"http-proxy",
"",
"http://proxy-url:port (default: empty)",
)
f.StringVar(&c.Conf.HTTPProxy, "http-proxy", "",
"http://proxy-url:port (default: empty)")
f.BoolVar(
&p.askKeyPassword,
"ask-key-password",
false,
f.BoolVar(&p.askKeyPassword, "ask-key-password", false,
"Ask ssh privatekey password before scanning",
)
f.BoolVar(
&p.fast,
"fast",
false,
"Online fast scan mode.")
f.BoolVar(&c.Conf.Pipe, "pipe", false, "Use stdin via PIPE")
f.BoolVar(&c.Conf.Vvv, "vvv", false, "ssh -vvv")
f.BoolVar(
&p.offline,
"offline",
false,
"Offline scan mode. Unable to get updatable packages information.")
f.BoolVar(
&p.deep,
"deep",
false,
"Deep scan mode. Scan accuracy improves and scanned information becomes richer. Since analysis of changelog, issue commands requiring sudo, but it may be slower and high load on the target server")
f.BoolVar(
&p.pipe,
"pipe",
false,
"Use stdin via PIPE")
f.BoolVar(&p.vvv, "vvv", false, "ssh -vvv")
f.IntVar(
&p.timeoutSec,
"timeout",
5*60,
f.IntVar(&p.timeoutSec, "timeout", 5*60,
"Number of seconds for processing other than scan",
)
f.IntVar(
&p.scanTimeoutSec,
"timeout-scan",
120*60,
f.IntVar(&p.scanTimeoutSec, "timeout-scan", 120*60,
"Number of seconds for scanning vulnerabilities for all servers",
)
}
@@ -192,8 +123,6 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) {
// Execute execute
func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
// Setup Logger
c.Conf.Debug = p.debug
c.Conf.LogDir = p.logDir
util.Log = util.NewCustomLogger(c.ServerInfo{})
if err := mkdirDotVuls(); err != nil {
@@ -222,8 +151,6 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
util.Log.Info("Start scanning")
util.Log.Infof("config: %s", p.configPath)
c.Conf.Pipe = p.pipe
c.Conf.Vvv = p.vvv
var servernames []string
if 0 < len(f.Args()) {
servernames = f.Args()
@@ -259,21 +186,6 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
}
util.Log.Debugf("%s", pp.Sprintf("%v", target))
c.Conf.ResultsDir = p.resultsDir
c.Conf.CacheDBPath = p.cacheDBPath
c.Conf.SSHNative = p.sshNative
c.Conf.SSHConfig = p.sshConfig
c.Conf.HTTPProxy = p.httpProxy
c.Conf.ContainersOnly = p.containersOnly
c.Conf.SkipBroken = p.skipBroken
c.Conf.Fast = p.fast
c.Conf.Offline = p.offline
c.Conf.Deep = p.deep
if !(c.Conf.Fast || c.Conf.Offline || c.Conf.Deep) {
c.Conf.Fast = true
}
util.Log.Info("Validating config...")
if !c.Conf.ValidateOnScan() {
return subcommands.ExitUsageError
@@ -285,6 +197,12 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitFailure
}
util.Log.Info("Checking Scan Modes... ")
if err := scan.CheckScanModes(); err != nil {
util.Log.Errorf("Fix config.toml: %s", err)
return subcommands.ExitFailure
}
util.Log.Info("Detecting Platforms... ")
scan.DetectPlatforms(p.timeoutSec)

219
commands/server.go Normal file
View File

@@ -0,0 +1,219 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package commands
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
// "github.com/future-architect/vuls/Server"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/oval"
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/server"
"github.com/future-architect/vuls/util"
"github.com/google/subcommands"
cvelog "github.com/kotakanbe/go-cve-dictionary/log"
)
// ServerCmd is subcommand for server
type ServerCmd struct {
configPath string
listen string
cvelDict c.GoCveDictConf
ovalDict c.GovalDictConf
gostConf c.GostConf
}
// Name return subcommand name
func (*ServerCmd) Name() string { return "server" }
// Synopsis return synopsis
func (*ServerCmd) Synopsis() string { return "Server" }
// Usage return usage
func (*ServerCmd) Usage() string {
return `Server:
Server
[-lang=en|ja]
[-config=/path/to/config.toml]
[-log-dir=/path/to/log]
[-cvss-over=7]
[-diff]
[-ignore-unscored-cves]
[-ignore-unfixed]
[-to-email]
[-to-slack]
[-to-stride]
[-to-hipchat]
[-to-chatwork]
[-to-localfile]
[-to-s3]
[-to-azure-blob]
[-format-json]
[-format-xml]
[-format-one-email]
[-format-one-line-text]
[-format-list]
[-format-full-text]
[-http-proxy=http://192.168.0.1:8080]
[-debug]
[-debug-sql]
[-listen=localhost:5515]
[-cvedb-type=sqlite3|mysql|postgres|redis]
[-cvedb-path=/path/to/cve.sqlite3]
[-cvedb-url=http://127.0.0.1:1323 or DB connection string]
[-ovaldb-type=sqlite3|mysql|redis]
[-ovaldb-path=/path/to/oval.sqlite3]
[-ovaldb-url=http://127.0.0.1:1324 or DB connection string]
[-gostdb-type=sqlite3|mysql|redis]
[-gostdb-path=/path/to/gost.sqlite3]
[-gostdb-url=http://127.0.0.1:1325 or DB connection string]
[RFC3339 datetime format under results dir]
`
}
// SetFlags set flag
func (p *ServerCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.Conf.Lang, "lang", "en", "[en|ja]")
f.BoolVar(&c.Conf.Debug, "debug", false, "debug mode")
f.BoolVar(&c.Conf.DebugSQL, "debug-sql", false, "SQL debug mode")
wd, _ := os.Getwd()
defaultConfPath := filepath.Join(wd, "config.toml")
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
defaultResultsDir := filepath.Join(wd, "results")
f.StringVar(&c.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results")
defaultLogDir := util.GetDefaultLogDir()
f.StringVar(&c.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log")
f.Float64Var(&c.Conf.CvssScoreOver, "cvss-over", 0,
"-cvss-over=6.5 means Servering CVSS Score 6.5 and over (default: 0 (means Server all))")
f.BoolVar(&c.Conf.IgnoreUnscoredCves, "ignore-unscored-cves", false,
"Don't Server the unscored CVEs")
f.BoolVar(&c.Conf.IgnoreUnfixed, "ignore-unfixed", false,
"Don't Server the unfixed CVEs")
f.StringVar(&c.Conf.HTTPProxy, "http-proxy", "",
"http://proxy-url:port (default: empty)")
f.BoolVar(&c.Conf.FormatJSON, "format-json", false, "JSON format")
f.BoolVar(&c.Conf.ToLocalFile, "to-localfile", false, "Write report to localfile")
f.StringVar(&p.listen, "listen", "localhost:5515",
"host:port (default: localhost:5515)")
f.StringVar(&p.cvelDict.Type, "cvedb-type", "sqlite3",
"DB type of go-cve-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.cvelDict.SQLite3Path, "cvedb-path", "", "/path/to/sqlite3")
f.StringVar(&p.cvelDict.URL, "cvedb-url", "",
"http://go-cve-dictionary.com:1323 or DB connection string")
f.StringVar(&p.ovalDict.Type, "ovaldb-type", "",
"DB type of goval-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.ovalDict.SQLite3Path, "ovaldb-path", "", "/path/to/sqlite3")
f.StringVar(&p.ovalDict.URL, "ovaldb-url", "",
"http://goval-dictionary.com:1324 or DB connection string")
f.StringVar(&p.gostConf.Type, "gostdb-type", "",
"DB type of gost (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.gostConf.SQLite3Path, "gostdb-path", "", "/path/to/sqlite3")
f.StringVar(&p.gostConf.URL, "gostdb-url", "",
"http://gost.com:1325 or DB connection string")
}
// Execute execute
func (p *ServerCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
util.Log = util.NewCustomLogger(c.ServerInfo{})
cvelog.SetLogger(c.Conf.LogDir, false, c.Conf.Debug, false)
c.Conf.CveDict.Overwrite(p.cvelDict)
c.Conf.OvalDict.Overwrite(p.ovalDict)
c.Conf.Gost.Overwrite(p.gostConf)
util.Log.Info("Validating config...")
if !c.Conf.ValidateOnReport() {
return subcommands.ExitUsageError
}
if err := report.CveClient.CheckHealth(); err != nil {
util.Log.Errorf("CVE HTTP server is not running. err: %s", err)
util.Log.Errorf("Run go-cve-dictionary as server mode before Servering or run with -cvedb-path option")
return subcommands.ExitFailure
}
if c.Conf.CveDict.URL != "" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDict.URL)
} else {
if c.Conf.CveDict.Type == "sqlite3" {
util.Log.Infof("cve-dictionary: %s", c.Conf.CveDict.SQLite3Path)
}
}
if c.Conf.OvalDict.URL != "" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDict.URL)
err := oval.Base{}.CheckHTTPHealth()
if err != nil {
util.Log.Errorf("OVAL HTTP server is not running. err: %s", err)
util.Log.Errorf("Run goval-dictionary as server mode before Servering or run with -ovaldb-path option")
return subcommands.ExitFailure
}
} else {
if c.Conf.OvalDict.Type == "sqlite3" {
util.Log.Infof("oval-dictionary: %s", c.Conf.OvalDict.SQLite3Path)
}
}
dbclient, locked, err := report.NewDBClient(report.DBClientConf{
CveDictCnf: c.Conf.CveDict,
OvalDictCnf: c.Conf.OvalDict,
GostCnf: c.Conf.Gost,
DebugSQL: c.Conf.DebugSQL,
})
if locked {
util.Log.Errorf("SQLite3 is locked. Close other DB connections and try again: %s", err)
return subcommands.ExitFailure
}
if err != nil {
util.Log.Errorf("Failed to init DB Clients: %s", err)
return subcommands.ExitFailure
}
defer dbclient.CloseDB()
http.Handle("/vuls", server.VulsHandler{DBclient: *dbclient})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
})
util.Log.Infof("Listening on %s", p.listen)
if err := http.ListenAndServe(p.listen, nil); err != nil {
util.Log.Errorf("Failed to start server: %s", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}

View File

@@ -20,7 +20,6 @@ package commands
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
@@ -29,33 +28,15 @@ import (
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/util"
"github.com/google/subcommands"
cvelog "github.com/kotakanbe/go-cve-dictionary/log"
)
// TuiCmd is Subcommand of host discovery mode
type TuiCmd struct {
lang string
debugSQL bool
debug bool
configPath string
logDir string
resultsDir string
refreshCve bool
cvedbtype string
cvedbpath string
cveDictionaryURL string
ovalDBType string
ovalDBPath string
ovalDBURL string
cvssScoreOver float64
ignoreUnscoredCves bool
ignoreUnfixed bool
pipe bool
diff bool
cvelDict c.GoCveDictConf
ovalDict c.GovalDictConf
gostConf c.GostConf
}
// Name return subcommand name
@@ -70,12 +51,6 @@ func (*TuiCmd) Usage() string {
tui
[-refresh-cve]
[-config=/path/to/config.toml]
[-cvedb-type=sqlite3|mysql|postgres]
[-cvedb-path=/path/to/cve.sqlite3]
[-cvedb-url=http://127.0.0.1:1323 or DB connection string]
[-ovaldb-type=sqlite3|mysql]
[-ovaldb-path=/path/to/oval.sqlite3]
[-ovaldb-url=http://127.0.0.1:1324 or DB connection string]
[-cvss-over=7]
[-diff]
[-ignore-unscored-cves]
@@ -85,6 +60,15 @@ func (*TuiCmd) Usage() string {
[-debug]
[-debug-sql]
[-pipe]
[-cvedb-type=sqlite3|mysql|postgres|redis]
[-cvedb-path=/path/to/cve.sqlite3]
[-cvedb-url=http://127.0.0.1:1323 or DB connection string]
[-ovaldb-type=sqlite3|mysql|redis]
[-ovaldb-path=/path/to/oval.sqlite3]
[-ovaldb-url=http://127.0.0.1:1324 or DB connection string]
[-gostdb-type=sqlite3|mysql|redis]
[-gostdb-path=/path/to/gost.sqlite3]
[-gostdb-url=http://127.0.0.1:1325 or DB connection string]
`
}
@@ -92,91 +76,54 @@ func (*TuiCmd) Usage() string {
// SetFlags set flag
func (p *TuiCmd) SetFlags(f *flag.FlagSet) {
// f.StringVar(&p.lang, "lang", "en", "[en|ja]")
f.BoolVar(&p.debugSQL, "debug-sql", false, "debug SQL")
f.BoolVar(&p.debug, "debug", false, "debug mode")
f.BoolVar(&c.Conf.DebugSQL, "debug-sql", false, "debug SQL")
f.BoolVar(&c.Conf.Debug, "debug", false, "debug mode")
defaultLogDir := util.GetDefaultLogDir()
f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
f.StringVar(&c.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log")
wd, _ := os.Getwd()
defaultResultsDir := filepath.Join(wd, "results")
f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results")
f.StringVar(&c.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results")
defaultConfPath := filepath.Join(wd, "config.toml")
f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml")
f.BoolVar(
&p.refreshCve,
"refresh-cve",
false,
f.BoolVar(&c.Conf.RefreshCve, "refresh-cve", false,
"Refresh CVE information in JSON file under results dir")
f.StringVar(
&p.cvedbtype,
"cvedb-type",
"sqlite3",
"DB type for fetching CVE dictionary (sqlite3, mysql or postgres)")
defaultCveDBPath := filepath.Join(wd, "cve.sqlite3")
f.StringVar(
&p.cvedbpath,
"cvedb-path",
defaultCveDBPath,
"/path/to/sqlite3 (For get cve detail from cve.sqlite3)")
f.StringVar(
&p.cveDictionaryURL,
"cvedb-url",
"",
"http://cve-dictionary.example.com:1323 or mysql connection string")
f.StringVar(
&p.ovalDBType,
"ovaldb-type",
"sqlite3",
"DB type for fetching OVAL dictionary (sqlite3 or mysql)")
defaultOvalDBPath := filepath.Join(wd, "oval.sqlite3")
f.StringVar(
&p.ovalDBPath,
"ovaldb-path",
defaultOvalDBPath,
"/path/to/sqlite3 (For get oval detail from oval.sqlite3)")
f.StringVar(
&p.ovalDBURL,
"ovaldb-url",
"",
"http://goval-dictionary.example.com:1324 or mysql connection string")
f.Float64Var(
&p.cvssScoreOver,
"cvss-over",
0,
f.Float64Var(&c.Conf.CvssScoreOver, "cvss-over", 0,
"-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))")
f.BoolVar(&p.diff,
"diff",
false,
fmt.Sprintf("Difference between previous result and current result "))
f.BoolVar(&c.Conf.Diff, "diff", false,
"Difference between previous result and current result ")
f.BoolVar(
&p.ignoreUnscoredCves,
"ignore-unscored-cves",
false,
&c.Conf.IgnoreUnscoredCves, "ignore-unscored-cves", false,
"Don't report the unscored CVEs")
f.BoolVar(
&p.ignoreUnfixed,
"ignore-unfixed",
false,
f.BoolVar(&c.Conf.IgnoreUnfixed, "ignore-unfixed", false,
"Don't report the unfixed CVEs")
f.BoolVar(
&p.pipe,
"pipe",
false,
"Use stdin via PIPE")
f.BoolVar(&c.Conf.Pipe, "pipe", false, "Use stdin via PIPE")
f.StringVar(&p.cvelDict.Type, "cvedb-type", "sqlite3",
"DB type of go-cve-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.cvelDict.SQLite3Path, "cvedb-path", "", "/path/to/sqlite3")
f.StringVar(&p.cvelDict.URL, "cvedb-url", "",
"http://go-cve-dictionary.com:1323 or DB connection string")
f.StringVar(&p.ovalDict.Type, "ovaldb-type", "",
"DB type of goval-dictionary (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.ovalDict.SQLite3Path, "ovaldb-path", "", "/path/to/sqlite3")
f.StringVar(&p.ovalDict.URL, "ovaldb-url", "",
"http://goval-dictionary.com:1324 or DB connection string")
f.StringVar(&p.gostConf.Type, "gostdb-type", "",
"DB type of gost (sqlite3, mysql, postgres or redis)")
f.StringVar(&p.gostConf.SQLite3Path, "gostdb-path", "", "/path/to/sqlite3")
f.StringVar(&p.gostConf.URL, "gostdb-url", "",
"http://gost.com:1325 or DB connection string")
}
// Execute execute
@@ -184,40 +131,26 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
c.Conf.Lang = "en"
// Setup Logger
c.Conf.Debug = p.debug
c.Conf.DebugSQL = p.debugSQL
c.Conf.LogDir = p.logDir
util.Log = util.NewCustomLogger(c.ServerInfo{})
log := util.Log
cvelog.SetLogger(c.Conf.LogDir, false, c.Conf.Debug, false)
if err := c.Load(p.configPath, ""); err != nil {
util.Log.Errorf("Error loading %s, %s", p.configPath, err)
return subcommands.ExitUsageError
}
c.Conf.ResultsDir = p.resultsDir
c.Conf.CveDBType = p.cvedbtype
c.Conf.CveDBPath = p.cvedbpath
c.Conf.CveDBURL = p.cveDictionaryURL
c.Conf.OvalDBType = p.ovalDBType
c.Conf.OvalDBPath = p.ovalDBPath
c.Conf.OvalDBURL = p.ovalDBURL
c.Conf.CvssScoreOver = p.cvssScoreOver
c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves
c.Conf.IgnoreUnfixed = p.ignoreUnfixed
c.Conf.RefreshCve = p.refreshCve
c.Conf.CveDict.Overwrite(p.cvelDict)
c.Conf.OvalDict.Overwrite(p.ovalDict)
c.Conf.Gost.Overwrite(p.gostConf)
log.Info("Validating config...")
util.Log.Info("Validating config...")
if !c.Conf.ValidateOnTui() {
return subcommands.ExitUsageError
}
c.Conf.Pipe = p.pipe
c.Conf.Diff = p.diff
var dir string
var err error
if p.diff {
if c.Conf.Diff {
dir, err = report.JSONDir([]string{})
} else {
dir, err = report.JSONDir(f.Args())
@@ -233,7 +166,25 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
}
util.Log.Infof("Loaded: %s", dir)
if res, err = report.FillCveInfos(res, dir); err != nil {
dbclient, locked, err := report.NewDBClient(report.DBClientConf{
CveDictCnf: c.Conf.CveDict,
OvalDictCnf: c.Conf.OvalDict,
GostCnf: c.Conf.Gost,
DebugSQL: c.Conf.DebugSQL,
})
if locked {
util.Log.Errorf("SQLite3 is locked. Close other DB connections and try again: %s", err)
return subcommands.ExitFailure
}
if err != nil {
util.Log.Errorf("Failed to init DB Clients: %s", err)
return subcommands.ExitFailure
}
defer dbclient.CloseDB()
if res, err = report.FillCveInfos(*dbclient, res, dir); err != nil {
util.Log.Error(err)
return subcommands.ExitFailure
}

View File

@@ -22,6 +22,7 @@ import (
"fmt"
"log/syslog"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
@@ -30,6 +31,12 @@ import (
log "github.com/sirupsen/logrus"
)
// Version of Vuls
var Version = "0.5.0"
// Revision of Git
var Revision string
// Conf has Configuration
var Conf Config
@@ -90,80 +97,62 @@ const (
//Config is struct of Configuration
type Config struct {
Debug bool
DebugSQL bool
Lang string
Debug bool `json:"debug"`
DebugSQL bool `json:"debugSQL"`
Lang string `json:"lang"`
HTTPProxy string `valid:"url" json:"httpProxy"`
LogDir string `json:"logDir"`
ResultsDir string `json:"resultsDir"`
Pipe bool `json:"pipe"`
EMail SMTPConf
Slack SlackConf
Stride StrideConf
HipChat HipChatConf
ChatWork ChatWorkConf
Syslog SyslogConf
Default ServerInfo
Servers map[string]ServerInfo
Default ServerInfo `json:"default"`
Servers map[string]ServerInfo `json:"servers"`
CvssScoreOver float64 `json:"cvssScoreOver"`
IgnoreUnscoredCves bool `json:"ignoreUnscoredCves"`
IgnoreUnfixed bool `json:"ignoreUnfixed"`
SSHNative bool `json:"sshNative"`
SSHConfig bool `json:"sshConfig"`
ContainersOnly bool `json:"containersOnly"`
SkipBroken bool `json:"skipBroken"`
CacheDBPath string `json:"cacheDBPath"`
Vvv bool `json:"vvv"`
UUID bool `json:"uuid"`
CvssScoreOver float64
IgnoreUnscoredCves bool
IgnoreUnfixed bool
CveDict GoCveDictConf `json:"cveDict"`
OvalDict GovalDictConf `json:"ovalDict"`
Gost GostConf `json:"gost"`
SSHNative bool
SSHConfig bool
Slack SlackConf `json:"-"`
EMail SMTPConf `json:"-"`
HTTP HTTPConf `json:"-"`
Syslog SyslogConf `json:"-"`
AWS AWS `json:"-"`
Azure Azure `json:"-"`
Stride StrideConf `json:"-"`
HipChat HipChatConf `json:"-"`
ChatWork ChatWorkConf `json:"-"`
Saas SaasConf `json:"-"`
ContainersOnly bool
Fast bool
Offline bool
Deep bool
SkipBroken bool
HTTPProxy string `valid:"url"`
LogDir string
ResultsDir string
CveDBType string
CveDBPath string
CveDBURL string
OvalDBType string
OvalDBPath string
OvalDBURL string
CacheDBPath string
RefreshCve bool
ToSlack bool
ToStride bool
ToHipChat bool
ToChatWork bool
ToEmail bool
ToSyslog bool
ToLocalFile bool
ToS3 bool
ToAzureBlob bool
FormatXML bool
FormatJSON bool
FormatOneEMail bool
FormatOneLineText bool
FormatShortText bool
FormatFullText bool
GZIP bool
AwsProfile string
AwsRegion string
S3Bucket string
S3ResultsDir string
S3ServerSideEncryption string
AzureAccount string
AzureKey string `json:"-"`
AzureContainer string
Pipe bool
Vvv bool
Diff bool
RefreshCve bool `json:"refreshCve"`
ToSlack bool `json:"toSlack"`
ToStride bool `json:"toStride"`
ToHipChat bool `json:"toHipChat"`
ToChatWork bool `json:"toChatWork"`
ToEmail bool `json:"toEmail"`
ToSyslog bool `json:"toSyslog"`
ToLocalFile bool `json:"toLocalFile"`
ToS3 bool `json:"toS3"`
ToAzureBlob bool `json:"toAzureBlob"`
ToSaas bool `json:"toSaas"`
ToHTTP bool `json:"toHTTP"`
FormatXML bool `json:"formatXML"`
FormatJSON bool `json:"formatJSON"`
FormatOneEMail bool `json:"formatOneEMail"`
FormatOneLineText bool `json:"formatOneLineText"`
FormatList bool `json:"formatList"`
FormatFullText bool `json:"formatFullText"`
GZIP bool `json:"gzip"`
Diff bool `json:"diff"`
}
// ValidateOnConfigtest validates
@@ -186,11 +175,6 @@ func (c Config) ValidateOnConfigtest() bool {
return len(errs) == 0
}
// ValidateOnPrepare validates configuration
func (c Config) ValidateOnPrepare() bool {
return c.ValidateOnConfigtest()
}
// ValidateOnScan validates configuration
func (c Config) ValidateOnScan() bool {
errs := []error{}
@@ -216,20 +200,11 @@ func (c Config) ValidateOnScan() bool {
if len(c.CacheDBPath) != 0 {
if ok, _ := valid.IsFilePath(c.CacheDBPath); !ok {
errs = append(errs, fmt.Errorf(
"Cache DB path must be a *Absolute* file path. -cache-dbpath: %s", c.CacheDBPath))
"Cache DB path must be a *Absolute* file path. -cache-dbpath: %s",
c.CacheDBPath))
}
}
numTrue := 0
for _, b := range []bool{c.Fast, c.Offline, c.Deep} {
if b {
numTrue++
}
}
if numTrue != 1 {
errs = append(errs, fmt.Errorf("Specify only one of -fast, -fast-offline, -deep"))
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
@@ -242,6 +217,30 @@ func (c Config) ValidateOnScan() bool {
return len(errs) == 0
}
// 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 c.CveDict.Type == "sqlite3" {
if _, err := os.Stat(c.CveDict.SQLite3Path); os.IsNotExist(err) {
errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDict.SQLite3Path))
}
}
if err := validateDB("ovaldb", c.OvalDict.Type, c.OvalDict.SQLite3Path, c.OvalDict.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 {
errs := []error{}
@@ -253,19 +252,6 @@ func (c Config) ValidateOnReport() bool {
}
}
if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil {
errs = append(errs, err)
}
if c.CveDBType == "sqlite3" {
if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) {
errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath))
}
}
if err := validateDB("ovaldb", c.OvalDBType, c.OvalDBPath, c.OvalDBURL); err != nil {
errs = append(errs, err)
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
@@ -291,10 +277,18 @@ func (c Config) ValidateOnReport() bool {
errs = append(errs, strideerrs...)
}
if saaserrs := c.Saas.Validate(); 0 < len(saaserrs) {
errs = append(errs, saaserrs...)
}
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)
}
@@ -313,12 +307,12 @@ func (c Config) ValidateOnTui() bool {
}
}
if err := validateDB("cvedb", c.CveDBType, c.CveDBPath, c.CveDBURL); err != nil {
if err := validateDB("cvedb", c.CveDict.Type, c.CveDict.SQLite3Path, c.CveDict.URL); err != nil {
errs = append(errs, err)
}
if c.CveDBType == "sqlite3" {
if _, err := os.Stat(c.CveDBPath); os.IsNotExist(err) {
errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDBPath))
if c.CveDict.Type == "sqlite3" {
if _, err := os.Stat(c.CveDict.SQLite3Path); os.IsNotExist(err) {
errs = append(errs, fmt.Errorf("SQLite3 DB path (%s) is not exist: %s", "cvedb", c.CveDict.SQLite3Path))
}
}
@@ -371,15 +365,14 @@ func validateDB(dictionaryDBName, dbType, dbPath, dbURL string) error {
// SMTPConf is smtp config
type SMTPConf struct {
SMTPAddr string
SMTPPort string `valid:"port"`
User string
Password string `json:"-"`
From string
To []string
Cc []string
SubjectPrefix string
SMTPAddr string `toml:"smtpAddr,omitempty" json:"-"`
SMTPPort string `toml:"smtpPort,omitempty" valid:"port" json:"-"`
User string `toml:"user,omitempty" json:"-"`
Password string `toml:"password,omitempty" json:"-"`
From string `toml:"from,omitempty" json:"-"`
To []string `toml:"to,omitempty" json:"-"`
Cc []string `toml:"cc,omitempty" json:"-"`
SubjectPrefix string `toml:"subjectPrefix,omitempty" json:"-"`
}
func checkEmails(emails []string) (errs []error) {
@@ -399,7 +392,6 @@ func (c *SMTPConf) Validate() (errs []error) {
if !Conf.ToEmail {
return
}
// Check Emails fromat
emails := []string{}
emails = append(emails, c.From)
@@ -432,8 +424,8 @@ func (c *SMTPConf) Validate() (errs []error) {
// StrideConf is stride config
type StrideConf struct {
HookURL string `json:"hook_url"`
AuthToken string `json:"AuthToken"`
HookURL string `json:"-"`
AuthToken string `json:"-"`
}
// Validate validates configuration
@@ -459,14 +451,13 @@ func (c *StrideConf) Validate() (errs []error) {
// SlackConf is slack config
type SlackConf struct {
HookURL string `valid:"url" json:"-"`
LegacyToken string `json:"token" toml:"legacyToken,omitempty"`
Channel string `json:"channel"`
IconEmoji string `json:"icon_emoji"`
AuthUser string `json:"username"`
NotifyUsers []string
Text string `json:"text"`
HookURL string `valid:"url" json:"-" toml:"hookURL,omitempty"`
LegacyToken string `json:"-" toml:"legacyToken,omitempty"`
Channel string `json:"-" toml:"channel,omitempty"`
IconEmoji string `json:"-" toml:"iconEmoji,omitempty"`
AuthUser string `json:"-" toml:"authUser,omitempty"`
NotifyUsers []string `toml:"notifyUsers,omitempty" json:"-"`
Text string `json:"-"`
}
// Validate validates configuration
@@ -503,8 +494,8 @@ func (c *SlackConf) Validate() (errs []error) {
// HipChatConf is HipChat config
type HipChatConf struct {
AuthToken string `json:"AuthToken"`
Room string `json:"Room"`
AuthToken string `json:"-"`
Room string `json:"-"`
}
// Validate validates configuration
@@ -529,8 +520,8 @@ func (c *HipChatConf) Validate() (errs []error) {
// ChatWorkConf is ChatWork config
type ChatWorkConf struct {
APIToken string `json:"ApiToken"`
Room string `json:"Room"`
APIToken string `json:"-"`
Room string `json:"-"`
}
// Validate validates configuration
@@ -553,16 +544,47 @@ func (c *ChatWorkConf) Validate() (errs []error) {
return
}
// SaasConf is stride config
type SaasConf struct {
GroupID int `json:"-"`
Token string `json:"-"`
URL string `json:"-"`
}
// Validate validates configuration
func (c *SaasConf) Validate() (errs []error) {
if !Conf.ToSaas {
return
}
if c.GroupID == 0 {
errs = append(errs, fmt.Errorf("saas.GroupID must not be empty"))
}
if len(c.Token) == 0 {
errs = append(errs, fmt.Errorf("saas.Token must not be empty"))
}
if len(c.URL) == 0 {
errs = append(errs, fmt.Errorf("saas.URL must not be empty"))
}
_, err := valid.ValidateStruct(c)
if err != nil {
errs = append(errs, err)
}
return
}
// SyslogConf is syslog config
type SyslogConf struct {
Protocol string
Host string `valid:"host"`
Port string `valid:"port"`
Severity string
Facility string
Tag string
Verbose bool
Protocol string `json:"-"`
Host string `valid:"host" json:"-"`
Port string `valid:"port" json:"-"`
Severity string `json:"-"`
Facility string `json:"-"`
Tag string `json:"-"`
Verbose bool `json:"-"`
}
// Validate validates configuration
@@ -674,42 +696,342 @@ func (c *SyslogConf) GetFacility() (syslog.Priority, error) {
}
}
// ServerInfo has SSH Info, additional CPE packages to scan.
type ServerInfo struct {
ServerName string
User string
Host string
Port string
KeyPath string
KeyPassword string `json:"-"`
// HTTPConf is HTTP config
type HTTPConf struct {
URL string `valid:"url" json:"-"`
}
CpeNames []string
DependencyCheckXMLPath string
// Validate validates configuration
func (c *HTTPConf) Validate() (errs []error) {
if !Conf.ToHTTP {
return nil
}
// Container Names or IDs
Containers Containers
if _, err := valid.ValidateStruct(c); err != nil {
errs = append(errs, err)
}
return errs
}
IgnoreCves []string
const httpKey = "VULS_HTTP_URL"
// Optional key-value set that will be outputted to JSON
Optional [][]interface{}
// Overwrite set options with the following priority.
// 1. Command line option
// 2. Environment variable
// 3. config.toml
func (c *HTTPConf) Overwrite(cmdOpt HTTPConf) {
if os.Getenv(httpKey) != "" {
c.URL = os.Getenv(httpKey)
}
if cmdOpt.URL != "" {
c.URL = cmdOpt.URL
}
}
// For CentOS, RHEL, Amazon
Enablerepo []string
// "pseudo" or ""
// GoCveDictConf is go-cve-dictionary config
type GoCveDictConf struct {
// DB type of CVE dictionary (sqlite3, mysql, postgres or redis)
Type string
// used internal
LogMsgAnsiColor string // DebugLog Color
Container Container
Distro Distro
// http://cve-dictionary.com:1323 or DB connection string
URL string `valid:"url" json:"-"`
// IP addresses
IPv4Addrs []string
IPv6Addrs []string
// /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"
// Overwrite set options with the following priority.
// 1. Command line option
// 2. Environment variable
// 3. config.toml
func (cnf *GoCveDictConf) Overwrite(cmdOpt GoCveDictConf) {
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)
}
if cmdOpt.Type != "" {
cnf.Type = cmdOpt.Type
}
if cmdOpt.URL != "" {
cnf.URL = cmdOpt.URL
}
if cmdOpt.SQLite3Path != "" {
cnf.SQLite3Path = cmdOpt.SQLite3Path
}
cnf.setDefault()
}
// 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 `valid:"url" 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"
// Overwrite set options with the following priority.
// 1. Command line option
// 2. Environment variable
// 3. config.toml
func (cnf *GovalDictConf) Overwrite(cmdOpt GovalDictConf) {
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)
}
if cmdOpt.Type != "" {
cnf.Type = cmdOpt.Type
}
if cmdOpt.URL != "" {
cnf.URL = cmdOpt.URL
}
if cmdOpt.SQLite3Path != "" {
cnf.SQLite3Path = cmdOpt.SQLite3Path
}
cnf.setDefault()
}
// 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 `valid:"url" 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"
// Overwrite set options with the following priority.
// 1. Command line option
// 2. Environment variable
// 3. config.toml
func (cnf *GostConf) Overwrite(cmdOpt GostConf) {
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)
}
if cmdOpt.Type != "" {
cnf.Type = cmdOpt.Type
}
if cmdOpt.URL != "" {
cnf.URL = cmdOpt.URL
}
if cmdOpt.SQLite3Path != "" {
cnf.SQLite3Path = cmdOpt.SQLite3Path
}
cnf.setDefault()
}
// AWS is aws config
type AWS 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"`
}
// Azure is azure config
type Azure 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"`
}
// ServerInfo has SSH Info, additional CPE packages to scan.
type ServerInfo struct {
ServerName string `toml:"-" json:"serverName"`
User string `toml:"user,omitempty" json:"user"`
Host string `toml:"host,omitempty" json:"host"`
Port string `toml:"port,omitempty" json:"port"`
KeyPath string `toml:"keyPath,omitempty" json:"keyPath"`
KeyPassword string `json:"-" toml:"-"`
CpeNames []string `toml:"cpeNames,omitempty" json:"cpeNames,omitempty"`
ScanMode []string `toml:"scanMode,omitempty" json:"scanMode,omitempty"`
DependencyCheckXMLPath string `toml:"dependencyCheckXMLPath,omitempty" json:"-"` // TODO Deprecated remove in near future
OwaspDCXMLPath string `toml:"owaspDCXMLPath,omitempty" json:"owaspDCXMLPath"`
ContainersIncluded []string `toml:"containersIncluded,omitempty" json:"containersIncluded,omitempty"`
ContainersExcluded []string `toml:"containersExcluded,omitempty" json:"containersExcluded,omitempty"`
ContainerType string `toml:"containerType,omitempty" json:"containerType,omitempty"`
Containers map[string]ContainerSetting `toml:"containers" json:"containers,omitempty"`
IgnoreCves []string `toml:"ignoreCves,omitempty" json:"ignoreCves,omitempty"`
IgnorePkgsRegexp []string `toml:"ignorePkgsRegexp,omitempty" json:"ignorePkgsRegexp,omitempty"`
UUIDs map[string]string `toml:"uuids,omitempty" json:"uuids,omitempty"`
Memo string `toml:"memo,omitempty" json:"memo"`
Enablerepo []string `toml:"enablerepo,omitempty" json:"enablerepo,omitempty"` // For CentOS, RHEL, Amazon
Optional map[string]interface{} `toml:"optional,omitempty" json:"optional,omitempty"` // Optional key-value set that will be outputted to JSON
Type string `toml:"type,omitempty" json:"type"` // "pseudo" or ""
IPv4Addrs []string `toml:"-" json:"ipv4Addrs,omitempty"`
IPv6Addrs []string `toml:"-" json:"ipv6Addrs,omitempty"`
// used internal
LogMsgAnsiColor string `toml:"-" json:"-"` // DebugLog Color
Container Container `toml:"-" json:"-"`
Distro Distro `toml:"-" json:"-"`
Mode ScanMode `toml:"-" json:"-"`
}
// ContainerSetting is used for loading container setting in config.toml
type ContainerSetting struct {
Cpes []string `json:"cpes,omitempty"`
OwaspDCXMLPath string `json:"owaspDCXMLPath"`
IgnorePkgsRegexp []string `json:"ignorePkgsRegexp,omitempty"`
IgnoreCves []string `json:"ignoreCves,omitempty"`
}
// ScanMode has a type of scan mode. fast, fast-root, deep and offline
type ScanMode struct {
flag byte
}
// Set mode
func (s *ScanMode) Set(f byte) {
s.flag |= f
}
// IsFast return whether scan mode is fast
func (s ScanMode) IsFast() bool {
return s.flag&Fast == Fast
}
// IsFastRoot return whether scan mode is fastroot
func (s ScanMode) IsFastRoot() bool {
return s.flag&FastRoot == FastRoot
}
// IsDeep return whether scan mode is deep
func (s ScanMode) IsDeep() bool {
return s.flag&Deep == Deep
}
// IsOffline return whether scan mode is offline
func (s ScanMode) IsOffline() bool {
return s.flag&Offline == Offline
}
func (s ScanMode) validate() error {
numTrue := 0
for _, b := range []bool{s.IsFast(), s.IsFastRoot(), s.IsDeep()} {
if b {
numTrue++
}
}
if numTrue == 0 {
s.Set(Fast)
} else if s.IsDeep() && s.IsOffline() {
return fmt.Errorf("Don't specify both of -deep and offline")
} else if numTrue != 1 {
return fmt.Errorf("Specify only one of -fast, -fast-root or -deep")
}
return nil
}
func (s ScanMode) String() string {
ss := ""
if s.IsFast() {
ss = "fast"
} else if s.IsFastRoot() {
ss = "fast-root"
} else if s.IsDeep() {
ss = "deep"
}
if s.IsOffline() {
ss += " offline"
}
return ss + " mode"
}
const (
// Fast is fast scan mode
Fast = byte(1 << iota)
// FastRoot is fast-root scan mode
FastRoot
// Deep is deep scan mode
Deep
// Offline is offline scan mode
Offline
)
// GetServerName returns ServerName if this serverInfo is about host.
// If this serverInfo is abount a container, returns containerID@ServerName
func (s ServerInfo) GetServerName() string {
@@ -757,13 +1079,6 @@ func (s *ServerInfo) SetContainer(d Container) {
s.Container = d
}
// Containers has Containers information.
type Containers struct {
Type string
Includes []string
Excludes []string
}
// Container has Container information.
type Container struct {
ContainerID string

View File

@@ -20,10 +20,11 @@ package config
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/BurntSushi/toml"
"github.com/future-architect/vuls/contrib/owasp-dependency-check/parser"
log "github.com/sirupsen/logrus"
"github.com/knqyf263/go-cpe/naming"
)
// TOMLLoader loads config
@@ -32,22 +33,24 @@ type TOMLLoader struct {
// Load load the configuration TOML file specified by path arg.
func (c TOMLLoader) Load(pathToToml, keyPass string) error {
if Conf.Debug {
log.SetLevel(log.DebugLevel)
}
var conf Config
if _, err := toml.DecodeFile(pathToToml, &conf); err != nil {
log.Error("Load config failed", err)
return err
}
Conf.EMail = conf.EMail
Conf.Slack = conf.Slack
Conf.Stride = conf.Stride
Conf.HipChat = conf.HipChat
Conf.ChatWork = conf.ChatWork
Conf.Saas = conf.Saas
Conf.Syslog = conf.Syslog
Conf.HTTP = conf.HTTP
Conf.AWS = conf.AWS
Conf.Azure = conf.Azure
Conf.CveDict = conf.CveDict
Conf.OvalDict = conf.OvalDict
Conf.Gost = conf.Gost
d := conf.Default
Conf.Default = d
@@ -58,17 +61,16 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
}
i := 0
for name, v := range conf.Servers {
for serverName, v := range conf.Servers {
if 0 < len(v.KeyPassword) {
log.Warn("[Deprecated] KEYPASSWORD IN CONFIG FILE ARE UNSECURE. REMOVE THEM IMMEDIATELY FOR A SECURITY REASONS. THEY WILL BE REMOVED IN A FUTURE RELEASE.")
return fmt.Errorf("[Deprecated] KEYPASSWORD IN CONFIG FILE ARE UNSECURE. REMOVE THEM IMMEDIATELY FOR A SECURITY REASONS. THEY WILL BE REMOVED IN A FUTURE RELEASE: %s", serverName)
}
s := ServerInfo{ServerName: name}
s := ServerInfo{ServerName: serverName}
if v.Type != ServerTypePseudo {
s.Host = v.Host
if len(s.Host) == 0 {
return fmt.Errorf("%s is invalid. host is empty", name)
return fmt.Errorf("%s is invalid. host is empty", serverName)
}
switch {
@@ -87,7 +89,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
s.User = d.User
default:
if s.Port != "local" {
return fmt.Errorf("%s is invalid. User is empty", name)
return fmt.Errorf("%s is invalid. User is empty", serverName)
}
}
@@ -98,7 +100,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
if s.KeyPath != "" {
if _, err := os.Stat(s.KeyPath); err != nil {
return fmt.Errorf(
"%s is invalid. keypath: %s not exists", name, s.KeyPath)
"%s is invalid. keypath: %s not exists", serverName, s.KeyPath)
}
}
@@ -109,31 +111,77 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
}
}
s.ScanMode = v.ScanMode
if len(s.ScanMode) == 0 {
s.ScanMode = d.ScanMode
if len(s.ScanMode) == 0 {
s.ScanMode = []string{"fast"}
}
}
for _, m := range s.ScanMode {
switch m {
case "fast":
s.Mode.Set(Fast)
case "fast-root":
s.Mode.Set(FastRoot)
case "deep":
s.Mode.Set(Deep)
case "offline":
s.Mode.Set(Offline)
default:
return fmt.Errorf("scanMode: %s of %s is invalie. Specify -fast, -fast-root, -deep or offline", m, serverName)
}
}
if err := s.Mode.validate(); err != nil {
return fmt.Errorf("%s in %s", err, serverName)
}
s.CpeNames = v.CpeNames
if len(s.CpeNames) == 0 {
s.CpeNames = d.CpeNames
}
s.DependencyCheckXMLPath = v.DependencyCheckXMLPath
if len(s.DependencyCheckXMLPath) == 0 {
s.DependencyCheckXMLPath = d.DependencyCheckXMLPath
for i, n := range s.CpeNames {
uri, err := toCpeURI(n)
if err != nil {
return fmt.Errorf("Failed to parse CPENames %s in %s: %s", n, serverName, err)
}
s.CpeNames[i] = uri
}
// Load CPEs from OWASP Dependency Check XML
if len(s.DependencyCheckXMLPath) != 0 {
cpes, err := parser.Parse(s.DependencyCheckXMLPath)
if err != nil {
return fmt.Errorf(
"Failed to read OWASP Dependency Check XML: %s", err)
}
log.Debugf("Loaded from OWASP Dependency Check XML: %s",
s.ServerName)
s.CpeNames = append(s.CpeNames, cpes...)
s.ContainersIncluded = v.ContainersIncluded
if len(s.ContainersIncluded) == 0 {
s.ContainersIncluded = d.ContainersIncluded
}
s.ContainersExcluded = v.ContainersExcluded
if len(s.ContainersExcluded) == 0 {
s.ContainersExcluded = d.ContainersExcluded
}
s.ContainerType = v.ContainerType
if len(s.ContainerType) == 0 {
s.ContainerType = d.ContainerType
}
s.Containers = v.Containers
if len(s.Containers.Includes) == 0 {
s.Containers = d.Containers
for contName, cont := range s.Containers {
cont.IgnoreCves = append(cont.IgnoreCves, d.IgnoreCves...)
s.Containers[contName] = cont
}
if len(v.DependencyCheckXMLPath) != 0 || len(d.DependencyCheckXMLPath) != 0 {
return fmt.Errorf("[DEPRECATED] dependencyCheckXMLPath IS DEPRECATED. USE owaspDCXMLPath INSTEAD: %s", serverName)
}
s.OwaspDCXMLPath = v.OwaspDCXMLPath
if len(s.OwaspDCXMLPath) == 0 {
s.OwaspDCXMLPath = d.OwaspDCXMLPath
}
s.Memo = v.Memo
if s.Memo == "" {
s.Memo = d.Memo
}
s.IgnoreCves = v.IgnoreCves
@@ -150,19 +198,43 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
}
}
s.Optional = v.Optional
for _, dkv := range d.Optional {
s.IgnorePkgsRegexp = v.IgnorePkgsRegexp
for _, pkg := range d.IgnorePkgsRegexp {
found := false
for _, kv := range s.Optional {
if dkv[0] == kv[0] {
for _, p := range s.IgnorePkgsRegexp {
if pkg == p {
found = true
break
}
}
if !found {
s.Optional = append(s.Optional, dkv)
s.IgnorePkgsRegexp = append(s.IgnorePkgsRegexp, pkg)
}
}
for _, reg := range s.IgnorePkgsRegexp {
_, err := regexp.Compile(reg)
if err != nil {
return fmt.Errorf("Faild to parse %s in %s. err: %s", reg, serverName, err)
}
}
for contName, cont := range s.Containers {
for _, reg := range cont.IgnorePkgsRegexp {
_, err := regexp.Compile(reg)
if err != nil {
return fmt.Errorf("Faild to parse %s in %s@%s. err: %s",
reg, contName, serverName, err)
}
}
}
opt := map[string]interface{}{}
for k, v := range d.Optional {
opt[k] = v
}
for k, v := range v.Optional {
opt[k] = v
}
s.Optional = opt
s.Enablerepo = v.Enablerepo
if len(s.Enablerepo) == 0 {
@@ -176,18 +248,36 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error {
default:
return fmt.Errorf(
"For now, enablerepo have to be base or updates: %s, servername: %s",
s.Enablerepo, name)
s.Enablerepo, serverName)
}
}
}
s.UUIDs = v.UUIDs
s.Type = v.Type
s.LogMsgAnsiColor = Colors[i%len(Colors)]
i++
servers[name] = s
servers[serverName] = s
}
Conf.Servers = servers
return nil
}
func toCpeURI(cpename string) (string, error) {
if strings.HasPrefix(cpename, "cpe:2.3:") {
wfn, err := naming.UnbindFS(cpename)
if err != nil {
return "", err
}
return naming.BindToURI(wfn), nil
} else if strings.HasPrefix(cpename, "cpe:/") {
wfn, err := naming.UnbindURI(cpename)
if err != nil {
return "", err
}
return naming.BindToURI(wfn), nil
}
return "", fmt.Errorf("Unknow CPE format: %s", cpename)
}

44
config/tomlloader_test.go Normal file
View File

@@ -0,0 +1,44 @@
package config
import (
"testing"
)
func TestToCpeURI(t *testing.T) {
var tests = []struct {
in string
expected string
err bool
}{
{
in: "",
expected: "",
err: true,
},
{
in: "cpe:/a:microsoft:internet_explorer:10",
expected: "cpe:/a:microsoft:internet_explorer:10",
err: false,
},
{
in: "cpe:2.3:a:microsoft:internet_explorer:10:*:*:*:*:*:*:*",
expected: "cpe:/a:microsoft:internet_explorer:10",
err: false,
},
}
for i, tt := range tests {
actual, err := toCpeURI(tt.in)
if err != nil && !tt.err {
t.Errorf("[%d] unexpected error occurred, in: %s act: %s, exp: %s",
i, tt.in, actual, tt.expected)
} else if err == nil && tt.err {
t.Errorf("[%d] expected error is not occurred, in: %s act: %s, exp: %s",
i, tt.in, actual, tt.expected)
}
if actual != tt.expected {
t.Errorf("[%d] in: %s, actual: %s, expected: %s",
i, tt.in, actual, tt.expected)
}
}
}

View File

@@ -32,7 +32,7 @@ func appendIfMissing(slice []string, str string) []string {
return append(slice, str)
}
// Parse parses XML and collect list of cpe
// Parse parses OWASP dependency check XML and collect list of cpe
func Parse(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {

5145
cwe/en.go Normal file

File diff suppressed because it is too large Load Diff

1027
cwe/ja.go Normal file

File diff suppressed because it is too large Load Diff

65
cwe/owasp.go Normal file
View File

@@ -0,0 +1,65 @@
package cwe
// OwaspTopTen2017 has CWE-ID in OWSP Top 10
var OwaspTopTen2017 = map[string]string{
"77": "1",
"89": "1",
"564": "1",
"917": "1",
"287": "2",
"384": "2",
"220": "3",
"310": "3",
"312": "3",
"319": "3",
"326": "3",
"359": "3",
"611": "4",
"22": "5",
"284": "5",
"285": "5",
"639": "5",
"2": "6",
"16": "6",
"388": "6",
"79": "7",
"502": "8",
"223": "10",
"778": "10",
}
// OwaspTopTen2017GitHubURLEn has GitHub links
var OwaspTopTen2017GitHubURLEn = map[string]string{
"1": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa1-injection.md",
"2": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa2-broken-authentication.md",
"3": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa3-sensitive-data-disclosure.md",
"4": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa4-xxe.md",
"5": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa5-broken-access-control.md",
"6": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa6-security-misconfiguration.md",
"7": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa7-xss.md",
"8": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa8-insecure-deserialization.md",
"9": "https://github.com/OWASP/Top10/blob/master/2017/en/0xa9-known-vulns.md<Paste>",
"10": "https://github.com/OWASP/Top10/blob/master/2017/en/0xaa-logging-detection-response.md",
}
// OwaspTopTen2017GitHubURLJa has GitHub links
var OwaspTopTen2017GitHubURLJa = map[string]string{
"1": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa1-injection.md",
"2": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa2-broken-authentication.md",
"3": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa3-sensitive-data-disclosure.md",
"4": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa4-xxe.md",
"5": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa5-broken-access-control.md",
"6": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa6-security-misconfiguration.md",
"7": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa7-xss.md",
"8": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa8-insecure-deserialization.md",
"9": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xa9-known-vulns.md<Paste>",
"10": "https://github.com/OWASP/Top10/blob/master/2017/ja/0xaa-logging-detection-response.md",
}

178
gost/debian.go Normal file
View File

@@ -0,0 +1,178 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gost
import (
"encoding/json"
"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"
)
// Debian is Gost client for Debian GNU/Linux
type Debian struct {
Base
}
type packCves struct {
packName string
isSrcPack bool
cves []models.CveContent
}
// FillWithGost fills cve information that has in Gost
func (deb Debian) FillWithGost(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
linuxImage := "linux-image-" + r.RunningKernel.Release
// Add linux and set the version of running kernel to search OVAL.
if r.Container.ContainerID == "" {
newVer := ""
if p, ok := r.Packages[linuxImage]; ok {
newVer = p.NewVersion
}
r.Packages["linux"] = models.Package{
Name: "linux",
Version: r.RunningKernel.Version,
NewVersion: newVer,
}
}
packCvesList := []packCves{}
if deb.isFetchViaHTTP() {
url, _ := util.URLPathJoin(config.Conf.Gost.URL, "debian", major(r.Release), "pkgs")
responses, err := getAllUnfixedCvesViaHTTP(r, url)
if err != nil {
return 0, err
}
for _, res := range responses {
debCves := map[string]gostmodels.DebianCVE{}
if err := json.Unmarshal([]byte(res.json), &debCves); err != nil {
return 0, err
}
cves := []models.CveContent{}
for _, debcve := range debCves {
cves = append(cves, *deb.ConvertToModel(&debcve))
}
packCvesList = append(packCvesList, packCves{
packName: res.request.packName,
isSrcPack: res.request.isSrcPack,
cves: cves,
})
}
} else {
if driver == nil {
return 0, nil
}
for _, pack := range r.Packages {
cveDebs := driver.GetUnfixedCvesDebian(major(r.Release), pack.Name)
cves := []models.CveContent{}
for _, cveDeb := range cveDebs {
cves = append(cves, *deb.ConvertToModel(&cveDeb))
}
packCvesList = append(packCvesList, packCves{
packName: pack.Name,
isSrcPack: false,
cves: cves,
})
}
// SrcPack
for _, pack := range r.SrcPackages {
cveDebs := driver.GetUnfixedCvesDebian(major(r.Release), pack.Name)
cves := []models.CveContent{}
for _, cveDeb := range cveDebs {
cves = append(cves, *deb.ConvertToModel(&cveDeb))
}
packCvesList = append(packCvesList, packCves{
packName: pack.Name,
isSrcPack: true,
cves: cves,
})
}
}
delete(r.Packages, "linux")
for _, p := range packCvesList {
for _, cve := range p.cves {
v, ok := r.ScannedCves[cve.CveID]
if ok {
v.CveContents[models.DebianSecurityTracker] = cve
} else {
v = models.VulnInfo{
CveID: cve.CveID,
CveContents: models.NewCveContents(cve),
Confidences: models.Confidences{models.DebianSecurityTrackerMatch},
}
nCVEs++
}
names := []string{}
if p.isSrcPack {
if srcPack, ok := r.SrcPackages[p.packName]; ok {
for _, binName := range srcPack.BinaryNames {
if _, ok := r.Packages[binName]; ok {
names = append(names, binName)
}
}
}
} else {
if p.packName == "linux" {
names = append(names, linuxImage)
} else {
names = append(names, p.packName)
}
}
for _, name := range names {
v.AffectedPackages = v.AffectedPackages.Store(models.PackageStatus{
Name: name,
FixState: "open",
NotFixedYet: true,
})
}
r.ScannedCves[cve.CveID] = v
}
}
return nCVEs, nil
}
// ConvertToModel converts gost model to vuls model
func (deb Debian) ConvertToModel(cve *gostmodels.DebianCVE) *models.CveContent {
severity := ""
for _, p := range cve.Package {
for _, r := range p.Release {
severity = r.Urgency
break
}
}
return &models.CveContent{
Type: models.DebianSecurityTracker,
CveID: cve.CveID,
Summary: cve.Description,
Cvss2Severity: severity,
Cvss3Severity: severity,
SourceLink: "https://security-tracker.debian.org/tracker/" + cve.CveID,
Optional: map[string]string{
"attack range": cve.Scope,
},
}
}

107
gost/gost.go Normal file
View File

@@ -0,0 +1,107 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gost
import (
"fmt"
"net/http"
"strings"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/knqyf263/gost/db"
"github.com/parnurzeal/gorequest"
)
// Client is the interface of OVAL client.
type Client interface {
FillWithGost(db.DB, *models.ScanResult) (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)
}
// NewClient make Client by family
func NewClient(family string) Client {
switch family {
case cnf.RedHat, cnf.CentOS:
return RedHat{}
case cnf.Debian:
return Debian{}
default:
return Pseudo{}
}
}
// Base is a base struct
type Base struct {
family string
}
// CheckHTTPHealth do health check
func (b Base) CheckHTTPHealth() error {
if !b.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 fmt.Errorf("Failed to connect to gost server. url: %s, errs: %v",
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
}
func (b Base) isFetchViaHTTP() bool {
// Default value of OvalDBType is sqlite3
return cnf.Conf.Gost.URL != "" && cnf.Conf.Gost.Type == "sqlite3"
}
// Pseudo is Gost client except for RedHat family and Debian
type Pseudo struct {
Base
}
// FillWithGost fills cve information that has in Gost
func (pse Pseudo) FillWithGost(driver db.DB, r *models.ScanResult) (int, error) {
return 0, nil
}
func major(osVer string) (majorVersion string) {
return strings.Split(osVer, ".")[0]
}

129
gost/gost_test.go Normal file
View File

@@ -0,0 +1,129 @@
package gost
import (
"reflect"
"testing"
"github.com/future-architect/vuls/models"
gostmodels "github.com/knqyf263/gost/models"
)
func TestSetPackageStates(t *testing.T) {
var tests = []struct {
pkgstats []gostmodels.RedhatPackageState
installed models.Packages
release string
in models.VulnInfo
out models.PackageStatuses
}{
//0 one
{
pkgstats: []gostmodels.RedhatPackageState{
{
FixState: "Will not fix",
PackageName: "bouncycastle",
Cpe: "cpe:/o:redhat:enterprise_linux:7",
},
},
installed: models.Packages{
"bouncycastle": models.Package{},
},
release: "7",
in: models.VulnInfo{},
out: []models.PackageStatus{
{
Name: "bouncycastle",
FixState: "Will not fix",
NotFixedYet: true,
},
},
},
//1 two
{
pkgstats: []gostmodels.RedhatPackageState{
{
FixState: "Will not fix",
PackageName: "bouncycastle",
Cpe: "cpe:/o:redhat:enterprise_linux:7",
},
{
FixState: "Fix deferred",
PackageName: "pack_a",
Cpe: "cpe:/o:redhat:enterprise_linux:7",
},
// ignore not-installed-package
{
FixState: "Fix deferred",
PackageName: "pack_b",
Cpe: "cpe:/o:redhat:enterprise_linux:7",
},
},
installed: models.Packages{
"bouncycastle": models.Package{},
"pack_a": models.Package{},
},
release: "7",
in: models.VulnInfo{},
out: []models.PackageStatus{
{
Name: "bouncycastle",
FixState: "Will not fix",
NotFixedYet: true,
},
{
Name: "pack_a",
FixState: "Fix deferred",
NotFixedYet: true,
},
},
},
//2 ignore affected
{
pkgstats: []gostmodels.RedhatPackageState{
{
FixState: "affected",
PackageName: "bouncycastle",
Cpe: "cpe:/o:redhat:enterprise_linux:7",
},
},
installed: models.Packages{
"bouncycastle": models.Package{},
},
release: "7",
in: models.VulnInfo{
AffectedPackages: models.PackageStatuses{},
},
out: models.PackageStatuses{},
},
//3 look only the same os release.
{
pkgstats: []gostmodels.RedhatPackageState{
{
FixState: "Will not fix",
PackageName: "bouncycastle",
Cpe: "cpe:/o:redhat:enterprise_linux:6",
},
},
installed: models.Packages{
"bouncycastle": models.Package{},
},
release: "7",
in: models.VulnInfo{
AffectedPackages: models.PackageStatuses{},
},
out: models.PackageStatuses{},
},
}
r := RedHat{}
for i, tt := range tests {
out := r.mergePackageStates(tt.in, tt.pkgstats, tt.installed, tt.release)
if ok := reflect.DeepEqual(tt.out, out); !ok {
t.Errorf("[%d]\nexpected: %v:%T\n actual: %v:%T\n", i, tt.out, tt.out, out, out)
}
}
}

253
gost/redhat.go Normal file
View File

@@ -0,0 +1,253 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gost
import (
"encoding/json"
"strconv"
"strings"
"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"
)
// RedHat is Gost client for RedHat family linux
type RedHat struct {
Base
}
// FillWithGost fills cve information that has in Gost
func (red RedHat) FillWithGost(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
if nCVEs, err = red.fillUnfixed(driver, r); err != nil {
return 0, err
}
return nCVEs, red.fillFixed(driver, r)
}
func (red RedHat) fillFixed(driver db.DB, r *models.ScanResult) error {
var cveIDs []string
for cveID, vuln := range r.ScannedCves {
if _, ok := vuln.CveContents[models.RedHatAPI]; ok {
continue
}
cveIDs = append(cveIDs, cveID)
}
if red.isFetchViaHTTP() {
prefix, _ := util.URLPathJoin(config.Conf.Gost.URL,
"redhat", "cves")
responses, err := getCvesViaHTTP(cveIDs, prefix)
if err != nil {
return err
}
for _, res := range responses {
redCve := gostmodels.RedhatCVE{}
if err := json.Unmarshal([]byte(res.json), &redCve); err != nil {
return err
}
if redCve.ID == 0 {
continue
}
cveCont := red.ConvertToModel(&redCve)
v, _ := r.ScannedCves[res.request.cveID]
v.CveContents[models.RedHatAPI] = *cveCont
r.ScannedCves[res.request.cveID] = v
}
} else {
if driver == nil {
return nil
}
for cveID, redCve := range driver.GetRedhatMulti(cveIDs) {
if redCve.ID == 0 {
continue
}
cveCont := red.ConvertToModel(&redCve)
v, _ := r.ScannedCves[cveID]
v.CveContents[models.RedHatAPI] = *cveCont
r.ScannedCves[cveID] = v
}
}
return nil
}
func (red RedHat) fillUnfixed(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
if red.isFetchViaHTTP() {
prefix, _ := util.URLPathJoin(config.Conf.Gost.URL,
"redhat", major(r.Release), "pkgs")
responses, err := getAllUnfixedCvesViaHTTP(r, prefix)
if err != nil {
return 0, err
}
for _, res := range responses {
// CVE-ID: RedhatCVE
cves := map[string]gostmodels.RedhatCVE{}
if err := json.Unmarshal([]byte(res.json), &cves); err != nil {
return 0, err
}
for _, cve := range cves {
cveCont := red.ConvertToModel(&cve)
v, ok := r.ScannedCves[cve.Name]
if ok {
v.CveContents[models.RedHatAPI] = *cveCont
} else {
v = models.VulnInfo{
CveID: cveCont.CveID,
CveContents: models.NewCveContents(*cveCont),
Confidences: models.Confidences{models.RedHatAPIMatch},
}
nCVEs++
}
pkgStats := red.mergePackageStates(v,
cve.PackageState, r.Packages, r.Release)
if 0 < len(pkgStats) {
v.AffectedPackages = pkgStats
r.ScannedCves[cve.Name] = v
}
}
}
} else {
if driver == nil {
return 0, nil
}
for _, pack := range r.Packages {
// CVE-ID: RedhatCVE
cves := map[string]gostmodels.RedhatCVE{}
cves = driver.GetUnfixedCvesRedhat(major(r.Release), pack.Name)
for _, cve := range cves {
cveCont := red.ConvertToModel(&cve)
v, ok := r.ScannedCves[cve.Name]
if ok {
v.CveContents[models.RedHatAPI] = *cveCont
} else {
v = models.VulnInfo{
CveID: cveCont.CveID,
CveContents: models.NewCveContents(*cveCont),
Confidences: models.Confidences{models.RedHatAPIMatch},
}
nCVEs++
}
pkgStats := red.mergePackageStates(v,
cve.PackageState, r.Packages, r.Release)
if 0 < len(pkgStats) {
v.AffectedPackages = pkgStats
r.ScannedCves[cve.Name] = v
}
}
}
}
return nCVEs, nil
}
func (red RedHat) mergePackageStates(v models.VulnInfo, ps []gostmodels.RedhatPackageState, installed models.Packages, release string) (pkgStats models.PackageStatuses) {
pkgStats = v.AffectedPackages
for _, pstate := range ps {
if pstate.Cpe !=
"cpe:/o:redhat:enterprise_linux:"+major(release) {
return
}
if !(pstate.FixState == "Will not fix" ||
pstate.FixState == "Fix deferred") {
return
}
if _, ok := installed[pstate.PackageName]; !ok {
return
}
notFixedYet := false
switch pstate.FixState {
case "Will not fix", "Fix deferred":
notFixedYet = true
}
pkgStats = pkgStats.Store(models.PackageStatus{
Name: pstate.PackageName,
FixState: pstate.FixState,
NotFixedYet: notFixedYet,
})
}
return
}
// ConvertToModel converts gost model to vuls model
func (red RedHat) ConvertToModel(cve *gostmodels.RedhatCVE) *models.CveContent {
cwes := []string{}
if cve.Cwe != "" {
s := strings.TrimPrefix(cve.Cwe, "(")
s = strings.TrimSuffix(s, ")")
if strings.Contains(cve.Cwe, "|") {
cwes = strings.Split(cve.Cwe, "|")
} else {
cwes = strings.Split(s, "->")
}
}
details := []string{}
for _, detail := range cve.Details {
details = append(details, detail.Detail)
}
v2score := 0.0
if cve.Cvss.CvssBaseScore != "" {
v2score, _ = strconv.ParseFloat(cve.Cvss.CvssBaseScore, 64)
}
v2severity := ""
if v2score != 0 {
v2severity = cve.ThreatSeverity
}
v3score := 0.0
if cve.Cvss3.Cvss3BaseScore != "" {
v3score, _ = strconv.ParseFloat(cve.Cvss3.Cvss3BaseScore, 64)
}
v3severity := ""
if v3score != 0 {
v3severity = cve.ThreatSeverity
}
var refs []models.Reference
for _, r := range cve.References {
refs = append(refs, models.Reference{Link: r.Reference})
}
return &models.CveContent{
Type: models.RedHatAPI,
CveID: cve.Name,
Title: cve.Bugzilla.Description,
Summary: strings.Join(details, "\n"),
Cvss2Score: v2score,
Cvss2Vector: cve.Cvss.CvssScoringVector,
Cvss2Severity: v2severity,
Cvss3Score: v3score,
Cvss3Vector: cve.Cvss3.Cvss3ScoringVector,
Cvss3Severity: v3severity,
References: refs,
CweIDs: cwes,
Mitigation: cve.Mitigation,
Published: cve.PublicDate,
SourceLink: "https://access.redhat.com/security/cve/" + cve.Name,
}
}

201
gost/util.go Normal file
View File

@@ -0,0 +1,201 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gost
import (
"fmt"
"net/http"
"time"
"github.com/cenkalti/backoff"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/parnurzeal/gorequest"
)
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, fmt.Errorf("Timeout Fetching OVAL")
}
}
if len(errs) != 0 {
return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs)
}
return
}
type request struct {
osMajorVersion string
packName string
isSrcPack bool
cveID string
}
func getAllUnfixedCvesViaHTTP(r *models.ScanResult, urlPrefix string) (
responses []response, err error) {
nReq := len(r.Packages) + len(r.SrcPackages)
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 _, pack := range r.Packages {
reqChan <- request{
osMajorVersion: major(r.Release),
packName: pack.Name,
isSrcPack: false,
}
}
for _, pack := range r.SrcPackages {
reqChan <- request{
osMajorVersion: major(r.Release),
packName: pack.Name,
isSrcPack: true,
}
}
}()
concurrency := 10
tasks := util.GenWorkers(concurrency)
for i := 0; i < nReq; i++ {
tasks <- func() {
select {
case req := <-reqChan:
url, err := util.URLPathJoin(
urlPrefix,
req.packName,
"unfixed-cves",
)
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, fmt.Errorf("Timeout Fetching OVAL")
}
}
if len(errs) != 0 {
return nil, fmt.Errorf("Failed to fetch OVAL. err: %v", errs)
}
return
}
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 fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v",
errs, url, resp)
}
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 <- fmt.Errorf("HTTP Error %s", err)
return
}
if count == retryMax {
errChan <- fmt.Errorf("HRetry count exceeded")
return
}
resChan <- response{
request: req,
json: body,
}
}

10
main.go
View File

@@ -25,15 +25,10 @@ import (
"context"
"github.com/future-architect/vuls/commands"
"github.com/future-architect/vuls/config"
"github.com/google/subcommands"
)
// Version of Vuls
var version = "0.4.2"
// Revision of Git
var revision string
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
@@ -44,13 +39,14 @@ func main() {
subcommands.Register(&commands.HistoryCmd{}, "history")
subcommands.Register(&commands.ReportCmd{}, "report")
subcommands.Register(&commands.ConfigtestCmd{}, "configtest")
subcommands.Register(&commands.ServerCmd{}, "server")
var v = flag.Bool("v", false, "Show version")
flag.Parse()
if *v {
fmt.Printf("vuls %s %s\n", version, revision)
fmt.Printf("vuls %s %s\n", config.Version, config.Revision)
os.Exit(int(subcommands.ExitSuccess))
}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package models
import (
"strings"
"time"
)
@@ -61,12 +60,12 @@ func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents)
// SourceLinks returns link of source
func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveContentStr) {
if lang == "ja" {
if cont, found := v[JVN]; found && 0 < len(cont.SourceLink) {
values = append(values, CveContentStr{JVN, cont.SourceLink})
if cont, found := v[Jvn]; found && 0 < len(cont.SourceLink) {
values = append(values, CveContentStr{Jvn, cont.SourceLink})
}
}
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
order := CveContentTypes{Nvd, NvdXML, NewCveContentType(myFamily)}
for _, ctype := range order {
if cont, found := v[ctype]; found {
values = append(values, CveContentStr{ctype, cont.SourceLink})
@@ -75,7 +74,7 @@ func (v CveContents) SourceLinks(lang, myFamily, cveID string) (values []CveCont
if len(values) == 0 {
return []CveContentStr{{
Type: NVD,
Type: Nvd,
Value: "https://nvd.nist.gov/vuln/detail/" + cveID,
}}
}
@@ -148,11 +147,14 @@ func (v CveContents) References(myFamily string) (values []CveContentRefs) {
func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) {
order := CveContentTypes{NewCveContentType(myFamily)}
order = append(order, AllCveContetTypes.Except(append(order)...)...)
for _, ctype := range order {
if cont, found := v[ctype]; found && 0 < len(cont.CweID) {
// RedHat's OVAL sometimes contains multiple CWE-IDs separated by spaces
for _, cweID := range strings.Fields(cont.CweID) {
if cont, found := v[ctype]; found && 0 < len(cont.CweIDs) {
for _, cweID := range cont.CweIDs {
for _, val := range values {
if val.Value == cweID {
continue
}
}
values = append(values, CveContentStr{
Type: ctype,
Value: cweID,
@@ -163,23 +165,38 @@ func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) {
return
}
// UniqCweIDs returns Uniq CweIDs
func (v CveContents) UniqCweIDs(myFamily string) (values []CveContentStr) {
uniq := map[string]CveContentStr{}
for _, cwes := range v.CweIDs(myFamily) {
uniq[cwes.Value] = cwes
}
for _, cwe := range uniq {
values = append(values, cwe)
}
return values
}
// CveContent has abstraction of various vulnerability information
type CveContent struct {
Type CveContentType
CveID string
Title string
Summary string
Severity string
Cvss2Score float64
Cvss2Vector string
Cvss3Score float64
Cvss3Vector string
SourceLink string
Cpes []Cpe
References References
CweID string
Published time.Time
LastModified time.Time
Type CveContentType `json:"type"`
CveID string `json:"cveID"`
Title string `json:"title"`
Summary string `json:"summary"`
Cvss2Score float64 `json:"cvss2Score"`
Cvss2Vector string `json:"cvss2Vector"`
Cvss2Severity string `json:"cvss2Severity"`
Cvss3Score float64 `json:"cvss3Score"`
Cvss3Vector string `json:"cvss3Vector"`
Cvss3Severity string `json:"cvss3Severity"`
SourceLink string `json:"sourceLink"`
Cpes []Cpe `json:"cpes,omitempty"`
References References `json:"references,omitempty"`
CweIDs []string `json:"cweIDs,omitempty"`
Published time.Time `json:"published"`
LastModified time.Time `json:"lastModified"`
Mitigation string `json:"mitigation"` // RedHat API
Optional map[string]string `json:"optional,omitempty"`
}
// Empty checks the content is empty
@@ -193,10 +210,12 @@ type CveContentType string
// NewCveContentType create CveContentType
func NewCveContentType(name string) CveContentType {
switch name {
case "nvdxml":
return NvdXML
case "nvd":
return NVD
return Nvd
case "jvn":
return JVN
return Jvn
case "redhat", "centos":
return RedHat
case "oracle":
@@ -205,21 +224,34 @@ func NewCveContentType(name string) CveContentType {
return Ubuntu
case "debian":
return Debian
case "redhat_api":
return RedHatAPI
case "debian_security_tracker":
return DebianSecurityTracker
default:
return Unknown
}
}
const (
// NVD is NVD
NVD CveContentType = "nvd"
// NvdXML is NvdXML
NvdXML CveContentType = "nvdxml"
// JVN is JVN
JVN CveContentType = "jvn"
// Nvd is Nvd
Nvd CveContentType = "nvd"
// Jvn is Jvn
Jvn CveContentType = "jvn"
// RedHat is RedHat
RedHat CveContentType = "redhat"
// RedHatAPI is RedHat
RedHatAPI CveContentType = "redhat_api"
// DebianSecurityTracker is Debian Secury tracker
DebianSecurityTracker CveContentType = "debian_security_tracker"
// Debian is Debian
Debian CveContentType = "debian"
@@ -240,7 +272,16 @@ const (
type CveContentTypes []CveContentType
// AllCveContetTypes has all of CveContentTypes
var AllCveContetTypes = CveContentTypes{NVD, JVN, RedHat, Debian, Ubuntu}
var AllCveContetTypes = CveContentTypes{
Nvd,
NvdXML,
Jvn,
RedHat,
Debian,
Ubuntu,
RedHatAPI,
DebianSecurityTracker,
}
// Except returns CveContentTypes except for given args
func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentTypes) {
@@ -261,7 +302,8 @@ func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentT
// Cpe is Common Platform Enumeration
type Cpe struct {
CpeName string
URI string `json:"uri"`
FormattedString string `json:"formattedString"`
}
// References is a slice of Reference
@@ -269,7 +311,7 @@ type References []Reference
// Reference has a related link of the CVE
type Reference struct {
Source string
Link string
RefID string
Source string `json:"source"`
Link string `json:"link"`
RefID string `json:"refID"`
}

View File

@@ -60,27 +60,27 @@ func TestSourceLinks(t *testing.T) {
lang: "ja",
cveID: "CVE-2017-6074",
cont: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
SourceLink: "https://jvn.jp/vu/JVNVU93610402/",
},
RedHat: {
Type: RedHat,
SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
},
},
out: []CveContentStr{
{
Type: JVN,
Type: Jvn,
Value: "https://jvn.jp/vu/JVNVU93610402/",
},
{
Type: NVD,
Type: NvdXML,
Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
{
@@ -95,23 +95,23 @@ func TestSourceLinks(t *testing.T) {
lang: "en",
cveID: "CVE-2017-6074",
cont: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
SourceLink: "https://jvn.jp/vu/JVNVU93610402/",
},
RedHat: {
Type: RedHat,
SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
},
},
out: []CveContentStr{
{
Type: NVD,
Type: NvdXML,
Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
{
@@ -129,16 +129,16 @@ func TestSourceLinks(t *testing.T) {
},
out: []CveContentStr{
{
Type: NVD,
Type: Nvd,
Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
},
},
}
for _, tt := range tests {
for i, tt := range tests {
actual := tt.in.cont.SourceLinks(tt.in.lang, "redhat", tt.in.cveID)
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual)
t.Errorf("\n[%d] expected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -158,16 +158,16 @@ func TestVendorLink(t *testing.T) {
vinfo: VulnInfo{
CveID: "CVE-2017-6074",
CveContents: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
SourceLink: "https://jvn.jp/vu/JVNVU93610402/",
},
RedHat: {
Type: RedHat,
SourceLink: "https://access.redhat.com/security/cve/CVE-2017-6074",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
SourceLink: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074",
},
},

View File

@@ -18,4 +18,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package models
// JSONVersion is JSON Version
const JSONVersion = 3
const JSONVersion = 4

View File

@@ -21,8 +21,6 @@ import (
"bytes"
"fmt"
"strings"
"github.com/future-architect/vuls/config"
)
// Packages is Map of Package
@@ -40,6 +38,12 @@ func NewPackages(packs ...Package) Packages {
// MergeNewVersion merges candidate version information to the receiver struct
func (ps Packages) MergeNewVersion(as Packages) {
for name, pack := range ps {
pack.NewVersion = pack.Version
pack.NewRelease = pack.Release
ps[name] = pack
}
for _, a := range as {
if pack, ok := ps[a.Name]; ok {
pack.NewVersion = a.NewVersion
@@ -62,22 +66,7 @@ func (ps Packages) Merge(other Packages) Packages {
return merged
}
// FormatUpdatablePacksSummary returns a summary of updatable packages
func (ps Packages) FormatUpdatablePacksSummary() string {
if config.Conf.Offline {
return fmt.Sprintf("%d installed", len(ps))
}
nUpdatable := 0
for _, p := range ps {
if p.NewVersion != "" {
nUpdatable++
}
}
return fmt.Sprintf("%d installed, %d updatable", len(ps), nUpdatable)
}
// FindOne search a element by name-newver-newrel-arch
// FindOne search a element
func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) {
for key, p := range ps {
if f(p) {
@@ -87,16 +76,44 @@ func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) {
return "", Package{}, false
}
// FindByFQPN search a package by Fully-Qualified-Package-Name
func (ps Packages) FindByFQPN(nameVerRelArc string) (*Package, error) {
for _, p := range ps {
if nameVerRelArc == p.FQPN() {
return &p, nil
}
}
return nil, fmt.Errorf("Failed to find the package: %s", nameVerRelArc)
}
// Package has installed binary packages.
type Package struct {
Name string
Version string
Release string
NewVersion string
NewRelease string
Arch string
Repository string
Changelog Changelog
Name string `json:"name"`
Version string `json:"version"`
Release string `json:"release"`
NewVersion string `json:"newVersion"`
NewRelease string `json:"newRelease"`
Arch string `json:"arch"`
Repository string `json:"repository"`
Changelog Changelog `json:"changelog"`
AffectedProcs []AffectedProcess `json:",omitempty"`
NeedRestartProcs []NeedRestartProcess `json:",omitempty"`
}
// FQPN returns Fully-Qualified-Package-Name
// name-version-release.arch
func (p Package) FQPN() string {
fqpn := p.Name
if p.Version != "" {
fqpn += fmt.Sprintf("-%s", p.Version)
}
if p.Release != "" {
fqpn += fmt.Sprintf("-%s", p.Release)
}
if p.Arch != "" {
fqpn += fmt.Sprintf(".%s", p.Arch)
}
return fqpn
}
// FormatVer returns package version-release
@@ -118,10 +135,14 @@ func (p Package) FormatNewVer() string {
}
// FormatVersionFromTo formats installed and new package version
func (p Package) FormatVersionFromTo(notFixedYet bool) string {
func (p Package) FormatVersionFromTo(notFixedYet bool, status string) string {
to := p.FormatNewVer()
if notFixedYet {
to = "Not Fixed Yet"
if status != "" {
to = status
} else {
to = "Not Fixed Yet"
}
} else if p.NewVersion == "" {
to = "Unknown"
}
@@ -156,8 +177,23 @@ func (p Package) FormatChangelog() string {
// Changelog has contents of changelog and how to get it.
// Method: models.detectionMethodStr
type Changelog struct {
Contents string
Method DetectionMethod
Contents string `json:"contents"`
Method DetectionMethod `json:"method"`
}
// AffectedProcess keep a processes information affected by software update
type AffectedProcess struct {
PID string `json:"pid"`
Name string `json:"name"`
}
// NeedRestartProcess keep a processes information affected by software update
type NeedRestartProcess struct {
PID string `json:"pid"`
Path string `json:"path"`
ServiceName string `json:"serviceName"`
InitSystem string `json:"initSystem"`
HasInit bool `json:"-"`
}
// SrcPackage has installed source package information.
@@ -166,9 +202,9 @@ type Changelog struct {
// so it is also needed to capture source version for OVAL version comparison.
// https://github.com/future-architect/vuls/issues/504
type SrcPackage struct {
Name string
Version string
BinaryNames []string
Name string `json:"name"`
Version string `json:"version"`
BinaryNames []string `json:"binaryNames"`
}
// AddBinaryName add the name if not exists

View File

@@ -20,9 +20,13 @@ package models
import (
"bytes"
"fmt"
"regexp"
"strings"
"time"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/cwe"
"github.com/future-architect/vuls/util"
)
// ScanResults is a slide of ScanResult
@@ -30,40 +34,83 @@ type ScanResults []ScanResult
// ScanResult has the result of scanned CVE information.
type ScanResult struct {
ScannedAt time.Time
ReportedAt time.Time
JSONVersion int
Lang string
ServerUUID string
ServerName string // TOML Section key
Family string
Release string
Container Container
Platform Platform
IPv4Addrs []string // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
IPv6Addrs []string // only global unicast address (https://golang.org/pkg/net/#IP.IsGlobalUnicast)
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)
ScannedAt time.Time `json:"scannedAt"`
ScannedVersion string `json:"scannedVersion"`
ScannedRevision string `json:"scannedRevision"`
ScannedBy string `json:"scannedBy"`
ReportedAt time.Time `json:"reportedAt"`
ReportedVersion string `json:"reportedVersion"`
ReportedRevision string `json:"reportedRevision"`
ReportedBy string `json:"reportedBy"`
ScannedCves VulnInfos `json:"scannedCves"`
RunningKernel Kernel `json:"runningKernel"`
Packages Packages `json:"packages"`
CweDict CweDict `json:"cweDict"`
Optional map[string]interface{} `json:",omitempty"`
SrcPackages SrcPackages `json:",omitempty"`
Errors []string `json:"errors"`
Config struct {
Scan config.Config `json:"scan"`
Report config.Config `json:"report"`
} `json:"config"`
}
// Scanned Vulns by SSH scan + CPE + OVAL
ScannedCves VulnInfos
// CweDict is a dictionary for CWE
type CweDict map[string]CweDictEntry
RunningKernel Kernel
Packages Packages
SrcPackages SrcPackages
Errors []string
Optional [][]interface{}
Config struct {
Scan config.Config
Report config.Config
// Get the name, url, top10URL for the specified cweID, lang
func (c CweDict) Get(cweID, lang string) (name, url, top10Rank, top10URL 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 := 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]
}
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"`
}
// Kernel has the Release, version and whether need restart
type Kernel struct {
Release string
Version string
RebootRequired bool
Release string `json:"release"`
Version string `json:"version"`
RebootRequired bool `json:"rebootRequired"`
}
// FilterByCvssOver is filter function.
@@ -85,9 +132,29 @@ func (r ScanResult) FilterByCvssOver(over float64) ScanResult {
}
// FilterIgnoreCves is filter function.
func (r ScanResult) FilterIgnoreCves(cveIDs []string) ScanResult {
func (r ScanResult) FilterIgnoreCves() ScanResult {
ignoreCves := []string{}
if len(r.Container.Name) == 0 {
ignoreCves = config.Conf.Servers[r.ServerName].IgnoreCves
} else {
if s, ok := config.Conf.Servers[r.ServerName]; ok {
if con, ok := s.Containers[r.Container.Name]; ok {
ignoreCves = con.IgnoreCves
} else {
util.Log.Errorf("%s is not found in config.toml",
r.Container.Name)
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 cveIDs {
for _, c := range ignoreCves {
if v.CveID == c {
return false
}
@@ -114,6 +181,63 @@ func (r ScanResult) FilterUnfixed() ScanResult {
return r
}
// FilterIgnorePkgs is filter function.
func (r ScanResult) FilterIgnorePkgs() ScanResult {
ignorePkgsRegexps := []string{}
if len(r.Container.Name) == 0 {
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 {
util.Log.Errorf("%s is not found in config.toml",
r.Container.Name)
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("Faild to parse %s, %s", 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
}
// ReportFileName returns the filename on localhost without extention
func (r ScanResult) ReportFileName() (name string) {
if len(r.Container.ContainerID) == 0 {
@@ -180,29 +304,94 @@ func (r ScanResult) FormatServerName() (name string) {
// FormatTextReportHeadedr returns header of text report
func (r ScanResult) FormatTextReportHeadedr() string {
serverInfo := r.ServerInfo()
var buf bytes.Buffer
for i := 0; i < len(serverInfo); i++ {
for i := 0; i < len(r.ServerInfo()); i++ {
buf.WriteString("=")
}
return fmt.Sprintf("%s\n%s\n%s\t%s\n",
return fmt.Sprintf("%s\n%s\n%s, %s, %s\n",
r.ServerInfo(),
buf.String(),
r.ScannedCves.FormatCveSummary(),
r.Packages.FormatUpdatablePacksSummary(),
r.ScannedCves.FormatFixedStatus(r.Packages),
r.FormatUpdatablePacksSummary(),
)
}
// FormatUpdatablePacksSummary returns a summary of updatable packages
func (r ScanResult) FormatUpdatablePacksSummary() string {
if !r.isDisplayUpdatableNum() {
return fmt.Sprintf("%d installed", len(r.Packages))
}
nUpdatable := 0
for _, p := range r.Packages {
if p.NewVersion == "" {
continue
}
if p.Version != p.NewVersion || p.Release != p.NewRelease {
nUpdatable++
}
}
return fmt.Sprintf("%d installed, %d updatable",
len(r.Packages),
nUpdatable)
}
func (r ScanResult) isDisplayUpdatableNum() bool {
var mode config.ScanMode
s, _ := config.Conf.Servers[r.ServerName]
mode = s.Mode
if mode.IsOffline() {
return false
}
if mode.IsFastRoot() || mode.IsDeep() {
return true
}
if mode.IsFast() {
switch r.Family {
case config.RedHat,
config.Oracle,
config.Debian,
config.Ubuntu,
config.Raspbian:
return false
default:
return true
}
}
return false
}
// IsContainer returns whether this ServerInfo is about container
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 {
for _, m := range s.ScanMode {
if m == "deep" {
return true
}
}
}
return false
}
// Container has Container information
type Container struct {
ContainerID string
Name string
Image string
Type string
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 // aws or azure or gcp or other...
InstanceID string
Name string `json:"name"` // aws or azure or gcp or other...
InstanceID string `json:"instanceID"`
}

View File

@@ -43,7 +43,7 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: NVD,
Type: NvdXML,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
@@ -54,7 +54,7 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: NVD,
Type: NvdXML,
CveID: "CVE-2017-0002",
Cvss2Score: 6.9,
LastModified: time.Time{},
@@ -65,13 +65,13 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: NVD,
Type: NvdXML,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: JVN,
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
@@ -87,7 +87,7 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: NVD,
Type: NvdXML,
CveID: "CVE-2017-0001",
Cvss2Score: 7.1,
LastModified: time.Time{},
@@ -98,13 +98,13 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: NVD,
Type: NvdXML,
CveID: "CVE-2017-0003",
Cvss2Score: 6.9,
LastModified: time.Time{},
},
CveContent{
Type: JVN,
Type: Jvn,
CveID: "CVE-2017-0003",
Cvss2Score: 7.2,
LastModified: time.Time{},
@@ -124,10 +124,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Severity: "HIGH",
LastModified: time.Time{},
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss2Severity: "HIGH",
LastModified: time.Time{},
},
),
},
@@ -135,10 +135,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: RedHat,
CveID: "CVE-2017-0002",
Severity: "CRITICAL",
LastModified: time.Time{},
Type: RedHat,
CveID: "CVE-2017-0002",
Cvss2Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
@@ -146,10 +146,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Oracle,
CveID: "CVE-2017-0003",
Severity: "IMPORTANT",
LastModified: time.Time{},
Type: Oracle,
CveID: "CVE-2017-0003",
Cvss2Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
@@ -162,10 +162,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: NewCveContents(
CveContent{
Type: Ubuntu,
CveID: "CVE-2017-0001",
Severity: "HIGH",
LastModified: time.Time{},
Type: Ubuntu,
CveID: "CVE-2017-0001",
Cvss2Severity: "HIGH",
LastModified: time.Time{},
},
),
},
@@ -173,10 +173,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: NewCveContents(
CveContent{
Type: RedHat,
CveID: "CVE-2017-0002",
Severity: "CRITICAL",
LastModified: time.Time{},
Type: RedHat,
CveID: "CVE-2017-0002",
Cvss2Severity: "CRITICAL",
LastModified: time.Time{},
},
),
},
@@ -184,10 +184,10 @@ func TestFilterByCvssOver(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: NewCveContents(
CveContent{
Type: Oracle,
CveID: "CVE-2017-0003",
Severity: "IMPORTANT",
LastModified: time.Time{},
Type: Oracle,
CveID: "CVE-2017-0003",
Cvss2Severity: "IMPORTANT",
LastModified: time.Time{},
},
),
},
@@ -206,7 +206,6 @@ func TestFilterByCvssOver(t *testing.T) {
}
}
}
func TestFilterIgnoreCveIDs(t *testing.T) {
type in struct {
cves []string
@@ -220,6 +219,7 @@ func TestFilterIgnoreCveIDs(t *testing.T) {
in: in{
cves: []string{"CVE-2017-0002"},
rs: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
@@ -234,6 +234,7 @@ func TestFilterIgnoreCveIDs(t *testing.T) {
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
@@ -246,7 +247,10 @@ func TestFilterIgnoreCveIDs(t *testing.T) {
},
}
for _, tt := range tests {
actual := tt.in.rs.FilterIgnoreCves(tt.in.cves)
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])
@@ -254,6 +258,83 @@ func TestFilterIgnoreCveIDs(t *testing.T) {
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)
}
}
}
}
@@ -336,3 +417,322 @@ func TestFilterUnfixed(t *testing.T) {
}
}
}
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: PackageStatuses{
{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: PackageStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageStatuses{
{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: PackageStatuses{
{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: PackageStatuses{
{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: PackageStatuses{
{Name: "kernel"},
{Name: "vim"},
},
},
},
},
},
out: ScanResult{
ServerName: "name",
Container: Container{Name: "dockerA"},
ScannedCves: VulnInfos{
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
AffectedPackages: PackageStatuses{
{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: PackageStatuses{
{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
family string
expected bool
}{
{
mode: []byte{config.Offline},
expected: false,
},
{
mode: []byte{config.FastRoot},
expected: true,
},
{
mode: []byte{config.Deep},
expected: true,
},
{
mode: []byte{config.Fast},
family: config.RedHat,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Oracle,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Debian,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Ubuntu,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.Raspbian,
expected: false,
},
{
mode: []byte{config.Fast},
family: config.CentOS,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.Amazon,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.FreeBSD,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.OpenSUSE,
expected: true,
},
{
mode: []byte{config.Fast},
family: config.Alpine,
expected: true,
},
}
for i, tt := range tests {
mode := config.ScanMode{}
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()
if tt.expected != act {
t.Errorf("[%d] expected %#v, actual %#v", i, tt.expected, act)
}
}
}

View File

@@ -18,17 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package models
import (
"fmt"
"strings"
cvedict "github.com/kotakanbe/go-cve-dictionary/models"
)
// ConvertNvdToModel convert NVD to CveContent
func ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent {
// ConvertNvdXMLToModel convert NVD to CveContent
func ConvertNvdXMLToModel(cveID string, nvd *cvedict.NvdXML) *CveContent {
if nvd == nil {
return nil
}
var cpes []Cpe
for _, c := range nvd.Cpes {
cpes = append(cpes, Cpe{CpeName: c.CpeName})
cpes = append(cpes, Cpe{
FormattedString: c.FormattedString,
URI: c.URI,
})
}
var refs []Reference
@@ -39,42 +44,21 @@ func ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent {
})
}
validVec := true
for _, v := range []string{
nvd.AccessVector,
nvd.AccessComplexity,
nvd.Authentication,
nvd.ConfidentialityImpact,
nvd.IntegrityImpact,
nvd.AvailabilityImpact,
} {
if len(v) == 0 {
validVec = false
}
cweIDs := []string{}
for _, cid := range nvd.Cwes {
cweIDs = append(cweIDs, cid.CweID)
}
vector := ""
if validVec {
vector = fmt.Sprintf("AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s",
string(nvd.AccessVector[0]),
string(nvd.AccessComplexity[0]),
string(nvd.Authentication[0]),
string(nvd.ConfidentialityImpact[0]),
string(nvd.IntegrityImpact[0]),
string(nvd.AvailabilityImpact[0]))
}
//TODO CVSSv3
return &CveContent{
Type: NVD,
CveID: cveID,
Summary: nvd.Summary,
Cvss2Score: nvd.Score,
Cvss2Vector: vector,
Severity: "", // severity is not contained in NVD
SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID,
Cpes: cpes,
CweID: nvd.CweID,
Type: Nvd,
CveID: cveID,
Summary: nvd.Summary,
Cvss2Score: nvd.Cvss2.BaseScore,
Cvss2Vector: nvd.Cvss2.VectorString,
Cvss2Severity: nvd.Cvss2.Severity,
SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID,
// Cpes: cpes,
CweIDs: cweIDs,
References: refs,
Published: nvd.PublishedDate,
LastModified: nvd.LastModifiedDate,
@@ -82,10 +66,16 @@ func ConvertNvdToModel(cveID string, nvd cvedict.Nvd) *CveContent {
}
// ConvertJvnToModel convert JVN to CveContent
func ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent {
func ConvertJvnToModel(cveID string, jvn *cvedict.Jvn) *CveContent {
if jvn == nil {
return nil
}
var cpes []Cpe
for _, c := range jvn.Cpes {
cpes = append(cpes, Cpe{CpeName: c.CpeName})
cpes = append(cpes, Cpe{
FormattedString: c.FormattedString,
URI: c.URI,
})
}
refs := []Reference{}
@@ -96,19 +86,71 @@ func ConvertJvnToModel(cveID string, jvn cvedict.Jvn) *CveContent {
})
}
vector := strings.TrimSuffix(strings.TrimPrefix(jvn.Vector, "("), ")")
return &CveContent{
Type: JVN,
CveID: cveID,
Title: jvn.Title,
Summary: jvn.Summary,
Severity: jvn.Severity,
Cvss2Score: jvn.Score,
Cvss2Vector: vector,
SourceLink: jvn.JvnLink,
Cpes: cpes,
Type: Jvn,
CveID: cveID,
Title: jvn.Title,
Summary: jvn.Summary,
Cvss2Score: jvn.Cvss2.BaseScore,
Cvss2Vector: jvn.Cvss2.VectorString,
Cvss2Severity: jvn.Cvss2.Severity,
Cvss3Score: jvn.Cvss3.BaseScore,
Cvss3Vector: jvn.Cvss3.VectorString,
Cvss3Severity: jvn.Cvss3.BaseSeverity,
SourceLink: jvn.JvnLink,
// Cpes: cpes,
References: refs,
Published: jvn.PublishedDate,
LastModified: jvn.LastModifiedDate,
}
}
// ConvertNvdJSONToModel convert NVD to CveContent
func ConvertNvdJSONToModel(cveID string, nvd *cvedict.NvdJSON) *CveContent {
if nvd == nil {
return nil
}
var cpes []Cpe
for _, c := range nvd.Cpes {
cpes = append(cpes, Cpe{
FormattedString: c.FormattedString,
URI: c.URI,
})
}
var refs []Reference
for _, r := range nvd.References {
refs = append(refs, Reference{
Link: r.Link,
Source: r.Source,
})
}
cweIDs := []string{}
for _, cid := range nvd.Cwes {
cweIDs = append(cweIDs, cid.CweID)
}
desc := []string{}
for _, d := range nvd.Descriptions {
desc = append(desc, d.Value)
}
return &CveContent{
Type: Nvd,
CveID: cveID,
Summary: strings.Join(desc, "\n"),
Cvss2Score: nvd.Cvss2.BaseScore,
Cvss2Vector: nvd.Cvss2.VectorString,
Cvss2Severity: nvd.Cvss2.Severity,
Cvss3Score: nvd.Cvss3.BaseScore,
Cvss3Vector: nvd.Cvss3.VectorString,
Cvss3Severity: nvd.Cvss3.BaseSeverity,
SourceLink: "https://nvd.nist.gov/vuln/detail/" + cveID,
// Cpes: cpes,
CweIDs: cweIDs,
References: refs,
Published: nvd.PublishedDate,
LastModified: nvd.LastModifiedDate,
}
}

View File

@@ -104,43 +104,85 @@ func (v VulnInfos) FormatCveSummary() string {
m["High"], m["Medium"], m["Low"], m["Unknown"])
}
// FormatFixedStatus summarize the number of cves are fixed.
func (v VulnInfos) FormatFixedStatus(packs Packages) string {
total, fixed := 0, 0
for _, vInfo := range v {
if len(vInfo.CpeURIs) != 0 {
continue
}
total++
if vInfo.PatchStatus(packs) == "Fixed" {
fixed++
}
}
return fmt.Sprintf("%d/%d Fixed", fixed, total)
}
// PackageStatuses is a list of PackageStatus
type PackageStatuses []PackageStatus
// FormatTuiSummary format packname to show TUI summary
func (ps PackageStatuses) FormatTuiSummary() string {
names := []string{}
for _, p := range ps {
names = append(names, p.Name)
}
return strings.Join(names, ", ")
}
// Store insert given pkg if missing, update pkg if exists
func (ps PackageStatuses) Store(pkg PackageStatus) PackageStatuses {
for i, p := range ps {
if p.Name == pkg.Name {
ps[i] = pkg
return ps
}
}
ps = append(ps, pkg)
return ps
}
// Sort by Name
func (p PackageStatuses) Sort() {
sort.Slice(p, func(i, j int) bool {
return p[i].Name < p[j].Name
func (ps PackageStatuses) Sort() {
sort.Slice(ps, func(i, j int) bool {
return ps[i].Name < ps[j].Name
})
return
}
// PackageStatus has name and other status abount the package
type PackageStatus struct {
Name string
NotFixedYet bool
Name string `json:"name"`
NotFixedYet bool `json:"notFixedYet"`
FixState string `json:"fixState"`
}
// VulnInfo has a vulnerability information and unsecure packages
type VulnInfo struct {
CveID string
Confidence Confidence
AffectedPackages PackageStatuses
DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD
CpeNames []string
CveContents CveContents
CveID string `json:"cveID"`
Confidences Confidences `json:"confidences"`
AffectedPackages PackageStatuses `json:"affectedPackages"`
DistroAdvisories []DistroAdvisory `json:"distroAdvisories,omitempty"` // for Aamazon, RHEL, FreeBSD
CpeURIs []string `json:"cpeURIs,omitempty"` // CpeURIs related to this CVE defined in config.toml
CveContents CveContents `json:"cveContents"`
}
// Titles returns tilte (TUI)
func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) {
if lang == "ja" {
if cont, found := v.CveContents[JVN]; found && 0 < len(cont.Title) {
values = append(values, CveContentStr{JVN, cont.Title})
if cont, found := v.CveContents[Jvn]; found && 0 < len(cont.Title) {
values = append(values, CveContentStr{Jvn, cont.Title})
}
}
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...)
// RedHat API has one line title.
if cont, found := v.CveContents[RedHatAPI]; found && 0 < len(cont.Title) {
values = append(values, CveContentStr{RedHatAPI, cont.Title})
}
order := CveContentTypes{Nvd, NvdXML, 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) {
@@ -171,16 +213,16 @@ 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 && 0 < len(cont.Summary) {
summary := cont.Title
summary += "\n" + strings.Replace(
strings.Replace(cont.Summary, "\n", " ", -1), "\r", " ", -1)
values = append(values, CveContentStr{JVN, summary})
values = append(values, CveContentStr{Jvn, summary})
}
}
order := CveContentTypes{NVD, NewCveContentType(myFamily)}
order = append(order, AllCveContetTypes.Except(append(order, JVN)...)...)
order := CveContentTypes{Nvd, NvdXML, NewCveContentType(myFamily)}
order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...)
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Summary) {
summary := strings.Replace(cont.Summary, "\n", " ", -1)
@@ -208,28 +250,71 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) {
return
}
// Cvss2Scores returns CVSS V2 Scores
func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) {
order := []CveContentType{NVD, RedHat, JVN}
// Mitigations returns mitigations
func (v VulnInfo) Mitigations(myFamily string) (values []CveContentStr) {
order := CveContentTypes{RedHatAPI}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && 0 < cont.Cvss2Score {
// https://nvd.nist.gov/vuln-metrics/cvss
sev := cont.Severity
if ctype == NVD {
sev = cvss2ScoreToSeverity(cont.Cvss2Score)
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Mitigation) {
values = append(values, CveContentStr{
Type: ctype,
Value: cont.Mitigation,
})
}
}
if len(values) == 0 {
return []CveContentStr{{
Type: Unknown,
Value: "-",
}}
}
return
}
// Cvss2Scores returns CVSS V2 Scores
func (v VulnInfo) Cvss2Scores(myFamily string) (values []CveContentCvss) {
order := []CveContentType{Nvd, NvdXML, RedHat, Jvn}
if myFamily != config.RedHat && myFamily != config.CentOS {
order = append(order, NewCveContentType(myFamily))
}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found {
if cont.Cvss2Score == 0 && cont.Cvss2Severity == "" {
continue
}
// https://nvd.nist.gov/vuln-metrics/cvss
values = append(values, CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS2,
Score: cont.Cvss2Score,
Vector: cont.Cvss2Vector,
Severity: strings.ToUpper(sev),
Severity: strings.ToUpper(cont.Cvss2Severity),
},
})
}
}
for _, v := range values {
if v.Type == RedHat {
return
}
}
// Set the CVSS v2 score of vuln that exists only in gost.
// Unfixed vulnerabilities detected by gost are not in OVAL, because
// OVAL data has only vulnerabilities for already fixed.
if cont, found := v.CveContents[RedHatAPI]; found {
values = append(values, CveContentCvss{
Type: RedHatAPI,
Value: Cvss{
Type: CVSS2,
Score: cont.Cvss2Score,
Vector: cont.Cvss2Vector,
Severity: strings.ToUpper(cont.Cvss2Severity),
},
})
}
for _, adv := range v.DistroAdvisories {
if adv.Severity != "" {
values = append(values, CveContentCvss{
@@ -246,22 +331,22 @@ func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) {
}
// An OVAL entry in Ubuntu and Debian has only severity (CVSS score isn't included).
// Show severity and dummy score calculated roghly.
// 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.Severity != "" {
cont.Cvss2Severity != "" {
values = append(values, CveContentCvss{
Type: cont.Type,
Value: Cvss{
Type: CVSS2,
Score: severityToV2ScoreRoughly(cont.Severity),
Score: severityToV2ScoreRoughly(cont.Cvss2Severity),
CalculatedBySeverity: true,
Vector: "-",
Severity: strings.ToUpper(cont.Severity),
Severity: strings.ToUpper(cont.Cvss2Severity),
},
})
}
@@ -272,30 +357,48 @@ func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) {
// Cvss3Scores returns CVSS V3 Score
func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) {
// TODO implement NVD
order := []CveContentType{RedHat}
order := []CveContentType{Nvd, RedHat, Jvn}
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && 0 < cont.Cvss3Score {
if cont, found := v.CveContents[ctype]; found {
// https://nvd.nist.gov/vuln-metrics/cvss
sev := cont.Severity
values = append(values, CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS3,
Score: cont.Cvss3Score,
Vector: cont.Cvss3Vector,
Severity: strings.ToUpper(sev),
Severity: strings.ToUpper(cont.Cvss3Severity),
},
})
}
}
for _, v := range values {
if v.Type == RedHat {
return
}
}
// Set the CVSS v3 score of vuln that exists only in gost.
// Unfixed vulnerabilities detected by gost are not in OVAL, because
// OVAL data has only vulnerabilities for already fixed.
if cont, found := v.CveContents[RedHatAPI]; found {
values = append(values, CveContentCvss{
Type: RedHatAPI,
Value: Cvss{
Type: CVSS3,
Score: cont.Cvss3Score,
Vector: cont.Cvss3Vector,
Severity: strings.ToUpper(cont.Cvss3Severity),
},
})
}
return
}
// MaxCvss3Score returns Max CVSS V3 Score
func (v VulnInfo) MaxCvss3Score() CveContentCvss {
// TODO implement NVD
order := []CveContentType{RedHat}
order := []CveContentType{Nvd, RedHat, RedHatAPI, Jvn}
max := 0.0
value := CveContentCvss{
Type: Unknown,
@@ -304,14 +407,13 @@ func (v VulnInfo) MaxCvss3Score() CveContentCvss {
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && max < cont.Cvss3Score {
// https://nvd.nist.gov/vuln-metrics/cvss
sev := cont.Severity
value = CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS3,
Score: cont.Cvss3Score,
Vector: cont.Cvss3Vector,
Severity: sev,
Severity: strings.ToUpper(cont.Cvss3Severity),
},
}
max = cont.Cvss3Score
@@ -338,7 +440,7 @@ func (v VulnInfo) MaxCvssScore() CveContentCvss {
// MaxCvss2Score returns Max CVSS V2 Score
func (v VulnInfo) MaxCvss2Score() CveContentCvss {
order := []CveContentType{NVD, RedHat, JVN}
order := []CveContentType{Nvd, NvdXML, RedHat, RedHatAPI, Jvn}
max := 0.0
value := CveContentCvss{
Type: Unknown,
@@ -347,17 +449,13 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss {
for _, ctype := range order {
if cont, found := v.CveContents[ctype]; found && max < cont.Cvss2Score {
// https://nvd.nist.gov/vuln-metrics/cvss
sev := cont.Severity
if ctype == NVD {
sev = cvss2ScoreToSeverity(cont.Cvss2Score)
}
value = CveContentCvss{
Type: ctype,
Value: Cvss{
Type: CVSS2,
Score: cont.Cvss2Score,
Vector: cont.Cvss2Vector,
Severity: sev,
Severity: strings.ToUpper(cont.Cvss2Severity),
},
}
max = cont.Cvss2Score
@@ -372,8 +470,8 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss {
// 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.Severity) {
score := severityToV2ScoreRoughly(cont.Severity)
if cont, found := v.CveContents[ctype]; found && 0 < len(cont.Cvss2Severity) {
score := severityToV2ScoreRoughly(cont.Cvss2Severity)
if max < score {
value = CveContentCvss{
Type: ctype,
@@ -382,7 +480,7 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss {
Score: score,
CalculatedBySeverity: true,
Vector: cont.Cvss2Vector,
Severity: cont.Severity,
Severity: strings.ToUpper(cont.Cvss2Severity),
},
}
}
@@ -411,10 +509,55 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss {
return value
}
// CveContentCvss has CveContentType and Cvss2
// AttackVector returns attack vector string
func (v VulnInfo) AttackVector() string {
for _, cnt := range v.CveContents {
if strings.HasPrefix(cnt.Cvss2Vector, "AV:N") ||
strings.HasPrefix(cnt.Cvss3Vector, "CVSS:3.0/AV:N") {
return "Network"
} else if strings.HasPrefix(cnt.Cvss2Vector, "AV:A") ||
strings.HasPrefix(cnt.Cvss3Vector, "CVSS:3.0/AV:A") {
return "Adjacent"
} else if strings.HasPrefix(cnt.Cvss2Vector, "AV:L") ||
strings.HasPrefix(cnt.Cvss3Vector, "CVSS:3.0/AV:L") {
return "Local"
} else if strings.HasPrefix(cnt.Cvss3Vector, "CVSS:3.0/AV:P") {
return "Physical"
}
}
if cont, found := v.CveContents[DebianSecurityTracker]; found {
if attackRange, found := cont.Optional["attack range"]; found {
return attackRange
}
}
return ""
}
// PatchStatus returns attack vector string
func (v VulnInfo) PatchStatus(packs Packages) string {
// Vuls don't know patch status of the CPE
if len(v.CpeURIs) != 0 {
return ""
}
for _, p := range v.AffectedPackages {
if p.NotFixedYet {
return "Unfixed"
}
// fast, offline mode doesn't have new version
if pack, ok := packs[p.Name]; ok {
if pack.NewVersion == "" {
return "Unknown"
}
}
}
return "Fixed"
}
// CveContentCvss has CVSS information
type CveContentCvss struct {
Type CveContentType
Value Cvss
Type CveContentType `json:"type"`
Value Cvss `json:"value"`
}
// CvssType Represent the type of CVSS
@@ -430,20 +573,23 @@ const (
// Cvss has CVSS Score
type Cvss struct {
Type CvssType
Score float64
CalculatedBySeverity bool
Vector string
Severity string
Type CvssType `json:"type"`
Score float64 `json:"score"`
CalculatedBySeverity bool `json:"calculatedBySeverity"`
Vector string `json:"vector"`
Severity string `json:"severity"`
}
// Format CVSS Score and Vector
func (c Cvss) Format() string {
if c.Score == 0 || c.Vector == "" {
return c.Severity
}
switch c.Type {
case CVSS2:
return fmt.Sprintf("%3.1f/%s", c.Score, c.Vector)
return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
case CVSS3:
return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector)
return fmt.Sprintf("%3.1f/%s %s", c.Score, c.Vector, c.Severity)
}
return ""
}
@@ -483,35 +629,6 @@ func severityToV2ScoreRoughly(severity string) float64 {
return 0
}
// CveContentCvss3 has CveContentType and Cvss3
// type CveContentCvss3 struct {
// Type CveContentType
// Value Cvss3
// }
// Cvss3 has CVSS v3 Score, Vector and Severity
// type Cvss3 struct {
// Score float64
// Vector string
// Severity string
// }
// Format CVSS Score and Vector
// func (c Cvss3) Format() string {
// return fmt.Sprintf("%3.1f/CVSS:3.0/%s", c.Score, c.Vector)
// }
// func cvss3ScoreToSeverity(score float64) string {
// if 9.0 <= score {
// return "CRITICAL"
// } else if 7.0 <= score {
// return "HIGH"
// } else if 4.0 <= score {
// return "MEDIUM"
// }
// return "LOW"
// }
// FormatMaxCvssScore returns Max CVSS Score
func (v VulnInfo) FormatMaxCvssScore() string {
max := v.MaxCvssScore()
@@ -573,37 +690,13 @@ func (v VulnInfo) VendorLinks(family string) map[string]string {
return links
}
// NilToEmpty set nil slice or map fields to empty to avoid null in JSON
func (v *VulnInfo) NilToEmpty() *VulnInfo {
if v.CpeNames == nil {
v.CpeNames = []string{}
}
if v.DistroAdvisories == nil {
v.DistroAdvisories = []DistroAdvisory{}
}
if v.AffectedPackages == nil {
v.AffectedPackages = PackageStatuses{}
}
if v.CveContents == nil {
v.CveContents = NewCveContents()
}
for key := range v.CveContents {
if v.CveContents[key].Cpes == nil {
cont := v.CveContents[key]
cont.Cpes = []Cpe{}
v.CveContents[key] = cont
}
}
return v
}
// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information.
type DistroAdvisory struct {
AdvisoryID string
Severity string
Issued time.Time
Updated time.Time
Description string
AdvisoryID string `json:"advisoryID"`
Severity string `json:"severity"`
Issued time.Time `json:"issued"`
Updated time.Time `json:"updated"`
Description string `json:"description"`
}
// Format the distro advisory information
@@ -620,11 +713,33 @@ func (p DistroAdvisory) Format() string {
return strings.Join(buf, "\n")
}
// Confidences is a list of Confidence
type Confidences []Confidence
// AppendIfMissing appends confidence to the list if missiong
func (cs *Confidences) AppendIfMissing(confidence Confidence) {
for _, c := range *cs {
if c.DetectionMethod == confidence.DetectionMethod {
return
}
}
*cs = append(*cs, confidence)
}
// SortByConfident sorts Confidences
func (cs Confidences) SortByConfident() Confidences {
sort.Slice(cs, func(i, j int) bool {
return cs[i].SortOrder < cs[j].SortOrder
})
return cs
}
// Confidence is a ranking how confident the CVE-ID was deteted correctly
// Score: 0 - 100
type Confidence struct {
Score int
DetectionMethod DetectionMethod
Score int `json:"score"`
DetectionMethod DetectionMethod `json:"detectionMethod"`
SortOrder int `json:"-"`
}
func (c Confidence) String() string {
@@ -649,6 +764,12 @@ const (
// OvalMatchStr is a String representation of OvalMatch
OvalMatchStr = "OvalMatch"
// RedHatAPIStr is a String representation of RedHatAPIMatch
RedHatAPIStr = "RedHatAPIMatch"
// DebianSecurityTrackerMatchStr is a String representation of DebianSecurityTrackerMatch
DebianSecurityTrackerMatchStr = "DebianSecurityTrackerMatch"
// ChangelogExactMatchStr is a String representation of ChangelogExactMatch
ChangelogExactMatchStr = "ChangelogExactMatch"
@@ -664,20 +785,26 @@ const (
var (
// CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly
CpeNameMatch = Confidence{100, CpeNameMatchStr}
CpeNameMatch = Confidence{100, CpeNameMatchStr, 1}
// YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly
YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr}
YumUpdateSecurityMatch = Confidence{100, YumUpdateSecurityMatchStr, 2}
// PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly
PkgAuditMatch = Confidence{100, PkgAuditMatchStr}
PkgAuditMatch = Confidence{100, PkgAuditMatchStr, 2}
// OvalMatch is a ranking how confident the CVE-ID was deteted correctly
OvalMatch = Confidence{100, OvalMatchStr}
OvalMatch = Confidence{100, OvalMatchStr, 0}
// RedHatAPIMatch ranking how confident the CVE-ID was deteted correctly
RedHatAPIMatch = Confidence{100, RedHatAPIStr, 0}
// DebianSecurityTrackerMatch ranking how confident the CVE-ID was deteted correctly
DebianSecurityTrackerMatch = Confidence{100, DebianSecurityTrackerMatchStr, 0}
// ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly
ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr}
ChangelogExactMatch = Confidence{95, ChangelogExactMatchStr, 3}
// ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly
ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr}
ChangelogLenientMatch = Confidence{50, ChangelogLenientMatchStr, 4}
)

View File

@@ -36,16 +36,16 @@ func TestTitles(t *testing.T) {
lang: "ja",
cont: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
Title: "Title1",
},
RedHat: {
Type: RedHat,
Summary: "Summary RedHat",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Summary: "Summary NVD",
// Severity is NIOT included in NVD
},
@@ -54,11 +54,11 @@ func TestTitles(t *testing.T) {
},
out: []CveContentStr{
{
Type: JVN,
Type: Jvn,
Value: "Title1",
},
{
Type: NVD,
Type: NvdXML,
Value: "Summary NVD",
},
{
@@ -73,16 +73,16 @@ func TestTitles(t *testing.T) {
lang: "en",
cont: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
Title: "Title1",
},
RedHat: {
Type: RedHat,
Summary: "Summary RedHat",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Summary: "Summary NVD",
// Severity is NIOT included in NVD
},
@@ -91,7 +91,7 @@ func TestTitles(t *testing.T) {
},
out: []CveContentStr{
{
Type: NVD,
Type: NvdXML,
Value: "Summary NVD",
},
{
@@ -137,8 +137,8 @@ func TestSummaries(t *testing.T) {
lang: "ja",
cont: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
Title: "Title JVN",
Summary: "Summary JVN",
},
@@ -146,8 +146,8 @@ func TestSummaries(t *testing.T) {
Type: RedHat,
Summary: "Summary RedHat",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Summary: "Summary NVD",
// Severity is NIOT included in NVD
},
@@ -156,11 +156,11 @@ func TestSummaries(t *testing.T) {
},
out: []CveContentStr{
{
Type: JVN,
Type: Jvn,
Value: "Title JVN\nSummary JVN",
},
{
Type: NVD,
Type: NvdXML,
Value: "Summary NVD",
},
{
@@ -175,8 +175,8 @@ func TestSummaries(t *testing.T) {
lang: "en",
cont: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Jvn: {
Type: Jvn,
Title: "Title JVN",
Summary: "Summary JVN",
},
@@ -184,8 +184,8 @@ func TestSummaries(t *testing.T) {
Type: RedHat,
Summary: "Summary RedHat",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Summary: "Summary NVD",
// Severity is NIOT included in NVD
},
@@ -194,7 +194,7 @@ func TestSummaries(t *testing.T) {
},
out: []CveContentStr{
{
Type: NVD,
Type: NvdXML,
Value: "Summary NVD",
},
{
@@ -235,8 +235,8 @@ func TestCountGroupBySeverity(t *testing.T) {
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 6.0,
},
RedHat: {
@@ -248,8 +248,8 @@ func TestCountGroupBySeverity(t *testing.T) {
"CVE-2017-0003": {
CveID: "CVE-2017-0003",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 2.0,
},
},
@@ -257,8 +257,8 @@ func TestCountGroupBySeverity(t *testing.T) {
"CVE-2017-0004": {
CveID: "CVE-2017-0004",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 5.0,
},
},
@@ -296,8 +296,8 @@ func TestToSortedSlice(t *testing.T) {
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 6.0,
},
RedHat: {
@@ -309,8 +309,8 @@ func TestToSortedSlice(t *testing.T) {
"CVE-2017-0001": {
CveID: "CVE-2017-0001",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 7.0,
},
RedHat: {
@@ -324,8 +324,8 @@ func TestToSortedSlice(t *testing.T) {
{
CveID: "CVE-2017-0001",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 7.0,
},
RedHat: {
@@ -337,8 +337,8 @@ func TestToSortedSlice(t *testing.T) {
{
CveID: "CVE-2017-0002",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 6.0,
},
RedHat: {
@@ -355,8 +355,8 @@ func TestToSortedSlice(t *testing.T) {
"CVE-2017-0002": {
CveID: "CVE-2017-0002",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 6.0,
},
RedHat: {
@@ -388,8 +388,8 @@ func TestToSortedSlice(t *testing.T) {
{
CveID: "CVE-2017-0002",
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 6.0,
},
RedHat: {
@@ -407,8 +407,8 @@ func TestToSortedSlice(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "High",
Type: Ubuntu,
Cvss2Severity: "High",
},
},
},
@@ -416,8 +416,8 @@ func TestToSortedSlice(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "Low",
Type: Ubuntu,
Cvss2Severity: "Low",
},
},
},
@@ -427,8 +427,8 @@ func TestToSortedSlice(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "High",
Type: Ubuntu,
Cvss2Severity: "High",
},
},
},
@@ -436,8 +436,8 @@ func TestToSortedSlice(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "Low",
Type: Ubuntu,
Cvss2Severity: "Low",
},
},
},
@@ -460,29 +460,29 @@ func TestCvss2Scores(t *testing.T) {
{
in: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Severity: "HIGH",
Cvss2Score: 8.2,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Jvn: {
Type: Jvn,
Cvss2Severity: "HIGH",
Cvss2Score: 8.2,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
},
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss2Score: 8.0,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Type: RedHat,
Cvss2Severity: "HIGH",
Cvss2Score: 8.0,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
},
NVD: {
Type: NVD,
Cvss2Score: 8.1,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
// Severity is NIOT included in NVD
NvdXML: {
Type: NvdXML,
Cvss2Score: 8.1,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Cvss2Severity: "HIGH",
},
},
},
out: []CveContentCvss{
{
Type: NVD,
Type: NvdXML,
Value: Cvss{
Type: CVSS2,
Score: 8.1,
@@ -500,7 +500,7 @@ func TestCvss2Scores(t *testing.T) {
},
},
{
Type: JVN,
Type: Jvn,
Value: Cvss{
Type: CVSS2,
Score: 8.2,
@@ -517,9 +517,9 @@ func TestCvss2Scores(t *testing.T) {
},
}
for i, tt := range tests {
actual := tt.in.Cvss2Scores()
actual := tt.in.Cvss2Scores("redhat")
if !reflect.DeepEqual(tt.out, actual) {
t.Errorf("[%d] expected: %v\n actual: %v\n", i, tt.out, actual)
t.Errorf("[%d]\nexpected: %v\n actual: %v\n", i, tt.out, actual)
}
}
}
@@ -532,20 +532,20 @@ func TestMaxCvss2Scores(t *testing.T) {
{
in: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Severity: "HIGH",
Cvss2Score: 8.2,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Jvn: {
Type: Jvn,
Cvss2Severity: "HIGH",
Cvss2Score: 8.2,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
},
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss2Score: 8.0,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
Type: RedHat,
Cvss2Severity: "HIGH",
Cvss2Score: 8.0,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 8.1,
Cvss2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P",
// Severity is NIOT included in NVD
@@ -553,7 +553,7 @@ func TestMaxCvss2Scores(t *testing.T) {
},
},
out: CveContentCvss{
Type: JVN,
Type: Jvn,
Value: Cvss{
Type: CVSS2,
Score: 8.2,
@@ -567,8 +567,8 @@ func TestMaxCvss2Scores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "HIGH",
Type: Ubuntu,
Cvss2Severity: "HIGH",
},
},
},
@@ -613,16 +613,16 @@ func TestCvss3Scores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss3Score: 8.0,
Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
Type: RedHat,
Cvss3Severity: "HIGH",
Cvss3Score: 8.0,
Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
},
NVD: {
Type: NVD,
Cvss3Score: 8.1,
Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
// Severity is NIOT included in NVD
NvdXML: {
Type: NvdXML,
Cvss2Score: 8.1,
Cvss2Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
Cvss2Severity: "HIGH",
},
},
},
@@ -661,10 +661,10 @@ func TestMaxCvss3Scores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss3Score: 8.0,
Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
Type: RedHat,
Cvss3Severity: "HIGH",
Cvss3Score: 8.0,
Cvss3Vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
},
},
},
@@ -708,8 +708,8 @@ func TestMaxCvssScores(t *testing.T) {
{
in: VulnInfo{
CveContents: CveContents{
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss3Score: 7.0,
},
RedHat: {
@@ -748,8 +748,8 @@ func TestMaxCvssScores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "HIGH",
Type: Ubuntu,
Cvss2Severity: "HIGH",
},
},
},
@@ -768,17 +768,18 @@ func TestMaxCvssScores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "MEDIUM",
Type: Ubuntu,
Cvss2Severity: "MEDIUM",
},
NVD: {
Type: NVD,
Cvss2Score: 7.0,
NvdXML: {
Type: NvdXML,
Cvss2Score: 7.0,
Cvss2Severity: "HIGH",
},
},
},
out: CveContentCvss{
Type: NVD,
Type: NvdXML,
Value: Cvss{
Type: CVSS2,
Score: 7.0,
@@ -810,12 +811,13 @@ func TestMaxCvssScores(t *testing.T) {
in: VulnInfo{
CveContents: CveContents{
Ubuntu: {
Type: Ubuntu,
Severity: "MEDIUM",
Type: Ubuntu,
Cvss2Severity: "MEDIUM",
},
NVD: {
Type: NVD,
Cvss2Score: 4.0,
NvdXML: {
Type: NvdXML,
Cvss2Score: 4.0,
Cvss2Severity: "MEDIUM",
},
},
DistroAdvisories: []DistroAdvisory{
@@ -825,7 +827,7 @@ func TestMaxCvssScores(t *testing.T) {
},
},
out: CveContentCvss{
Type: NVD,
Type: NvdXML,
Value: Cvss{
Type: CVSS2,
Score: 4,
@@ -861,18 +863,18 @@ func TestFormatMaxCvssScore(t *testing.T) {
{
in: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Severity: "HIGH",
Cvss2Score: 8.3,
Jvn: {
Type: Jvn,
Cvss2Severity: "HIGH",
Cvss2Score: 8.3,
},
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss3Score: 8.0,
Type: RedHat,
Cvss2Severity: "HIGH",
Cvss3Score: 8.0,
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 8.1,
// Severity is NIOT included in NVD
},
@@ -883,19 +885,20 @@ func TestFormatMaxCvssScore(t *testing.T) {
{
in: VulnInfo{
CveContents: CveContents{
JVN: {
Type: JVN,
Severity: "HIGH",
Cvss2Score: 8.3,
Jvn: {
Type: Jvn,
Cvss2Severity: "HIGH",
Cvss2Score: 8.3,
},
RedHat: {
Type: RedHat,
Severity: "HIGH",
Cvss2Score: 8.0,
Cvss3Score: 9.9,
Type: RedHat,
Cvss2Severity: "HIGH",
Cvss2Score: 8.0,
Cvss3Severity: "HIGH",
Cvss3Score: 9.9,
},
NVD: {
Type: NVD,
NvdXML: {
Type: NvdXML,
Cvss2Score: 8.1,
},
},
@@ -934,3 +937,100 @@ func TestSortPackageStatues(t *testing.T) {
}
}
}
func TestStorePackageStatueses(t *testing.T) {
var tests = []struct {
pkgstats PackageStatuses
in PackageStatus
out PackageStatuses
}{
{
pkgstats: PackageStatuses{
{Name: "a"},
{Name: "b"},
},
in: PackageStatus{
Name: "c",
},
out: PackageStatuses{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
},
}
for _, tt := range tests {
out := tt.pkgstats.Store(tt.in)
if ok := reflect.DeepEqual(tt.out, out); !ok {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, out)
}
}
}
func TestAppendIfMissing(t *testing.T) {
var tests = []struct {
in Confidences
arg Confidence
out Confidences
}{
{
in: Confidences{
CpeNameMatch,
},
arg: CpeNameMatch,
out: Confidences{
CpeNameMatch,
},
},
{
in: Confidences{
CpeNameMatch,
},
arg: ChangelogExactMatch,
out: Confidences{
CpeNameMatch,
ChangelogExactMatch,
},
},
}
for _, tt := range tests {
tt.in.AppendIfMissing(tt.arg)
if !reflect.DeepEqual(tt.in, tt.out) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, tt.in)
}
}
}
func TestSortByConfiden(t *testing.T) {
var tests = []struct {
in Confidences
out Confidences
}{
{
in: Confidences{
OvalMatch,
CpeNameMatch,
},
out: Confidences{
OvalMatch,
CpeNameMatch,
},
},
{
in: Confidences{
CpeNameMatch,
OvalMatch,
},
out: Confidences{
OvalMatch,
CpeNameMatch,
},
},
}
for _, tt := range tests {
act := tt.in.SortByConfident()
if !reflect.DeepEqual(tt.out, act) {
t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, act)
}
}
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/future-architect/vuls/config"
"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
@@ -38,22 +39,22 @@ func NewAlpine() Alpine {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Alpine) FillWithOval(r *models.ScanResult) (err error) {
func (o Alpine) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if o.isFetchViaHTTP() {
if o.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
return err
return 0, err
}
} else {
if relatedDefs, err = getDefsByPackNameFromOvalDB(r); err != nil {
return err
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
for _, defPacks := range relatedDefs.entries {
o.update(r, defPacks)
}
return nil
return len(relatedDefs.entries), nil
}
func (o Alpine) update(r *models.ScanResult, defPacks defPacks) {
@@ -62,8 +63,8 @@ func (o Alpine) update(r *models.ScanResult, defPacks defPacks) {
if !ok {
util.Log.Debugf("%s is newly detected by OVAL", cveID)
vinfo = models.VulnInfo{
CveID: cveID,
Confidence: models.OvalMatch,
CveID: cveID,
Confidences: []models.Confidence{models.OvalMatch},
}
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/future-architect/vuls/config"
"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"
)
@@ -37,7 +38,7 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) {
util.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Debian.CveID)
vinfo = models.VulnInfo{
CveID: defPacks.def.Debian.CveID,
Confidence: models.OvalMatch,
Confidences: []models.Confidence{models.OvalMatch},
CveContents: models.NewCveContents(ovalContent),
}
} else {
@@ -51,17 +52,14 @@ func (o DebianBase) update(r *models.ScanResult, defPacks defPacks) {
defPacks.def.Debian.CveID)
cveContents = models.CveContents{}
}
if vinfo.Confidence.Score < models.OvalMatch.Score {
vinfo.Confidence = models.OvalMatch
}
vinfo.Confidences.AppendIfMissing(models.OvalMatch)
cveContents[ctype] = ovalContent
vinfo.CveContents = cveContents
}
// uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames)
for _, pack := range vinfo.AffectedPackages {
notFixedYet, _ := defPacks.actuallyAffectedPackNames[pack.Name]
defPacks.actuallyAffectedPackNames[pack.Name] = notFixedYet
defPacks.actuallyAffectedPackNames[pack.Name] = pack.NotFixedYet
}
// update notFixedYet of SrcPackage
@@ -91,11 +89,11 @@ func (o DebianBase) convertToModel(def *ovalmodels.Definition) *models.CveConten
}
return &models.CveContent{
CveID: def.Debian.CveID,
Title: def.Title,
Summary: def.Description,
Severity: def.Advisory.Severity,
References: refs,
CveID: def.Debian.CveID,
Title: def.Title,
Summary: def.Description,
Cvss2Severity: def.Advisory.Severity,
References: refs,
}
}
@@ -116,17 +114,17 @@ func NewDebian() Debian {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Debian) FillWithOval(r *models.ScanResult) (err error) {
func (o Debian) FillWithOval(driver db.DB, 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
// Add linux and set the version of running kernel to search OVAL.
newVer := ""
if p, ok := r.Packages[linuxImage]; ok {
newVer = p.NewVersion
}
if r.Container.ContainerID == "" {
newVer := ""
if p, ok := r.Packages[linuxImage]; ok {
newVer = p.NewVersion
}
r.Packages["linux"] = models.Package{
Name: "linux",
Version: r.RunningKernel.Version,
@@ -135,13 +133,13 @@ func (o Debian) FillWithOval(r *models.ScanResult) (err error) {
}
var relatedDefs ovalResult
if o.isFetchViaHTTP() {
if o.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
return err
return 0, err
}
} else {
if relatedDefs, err = getDefsByPackNameFromOvalDB(r); err != nil {
return err
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
@@ -170,7 +168,7 @@ func (o Debian) FillWithOval(r *models.ScanResult) (err error) {
vuln.CveContents[models.Debian] = cont
}
}
return nil
return len(relatedDefs.entries), nil
}
// Ubuntu is the interface for Debian OVAL
@@ -190,7 +188,7 @@ func NewUbuntu() Ubuntu {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o Ubuntu) FillWithOval(r *models.ScanResult) (err error) {
func (o Ubuntu) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
ovalKernelImageNames := []string{
"linux-aws",
"linux-azure",
@@ -245,13 +243,13 @@ func (o Ubuntu) FillWithOval(r *models.ScanResult) (err error) {
}
var relatedDefs ovalResult
if o.isFetchViaHTTP() {
if o.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
return err
return 0, err
}
} else {
if relatedDefs, err = getDefsByPackNameFromOvalDB(r); err != nil {
return err
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
@@ -282,5 +280,5 @@ func (o Ubuntu) FillWithOval(r *models.ScanResult) (err error) {
vuln.CveContents[models.Ubuntu] = cont
}
}
return nil
return len(relatedDefs.entries), nil
}

View File

@@ -23,7 +23,7 @@ import (
"net/http"
"time"
"github.com/future-architect/vuls/config"
cnf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/kotakanbe/goval-dictionary/db"
@@ -33,11 +33,12 @@ import (
// Client is the interface of OVAL client.
type Client interface {
CheckHTTPHealth() error
FillWithOval(r *models.ScanResult) error
FillWithOval(db.DB, *models.ScanResult) (int, error)
// CheckIfOvalFetched checks if oval entries are in DB by family, release.
CheckIfOvalFetched(string, string) (bool, error)
CheckIfOvalFresh(string, string) (bool, error)
CheckIfOvalFetched(db.DB, string, string) (bool, error)
CheckIfOvalFresh(db.DB, string, string) (bool, error)
IsFetchViaHTTP() bool
}
// Base is a base struct
@@ -47,11 +48,11 @@ type Base struct {
// CheckHTTPHealth do health check
func (b Base) CheckHTTPHealth() error {
if !b.isFetchViaHTTP() {
if !b.IsFetchViaHTTP() {
return nil
}
url := fmt.Sprintf("%s/health", config.Conf.OvalDBURL)
url := fmt.Sprintf("%s/health", cnf.Conf.OvalDict.URL)
var errs []error
var resp *http.Response
resp, _, errs = gorequest.New().Get(url).End()
@@ -65,19 +66,9 @@ func (b Base) CheckHTTPHealth() error {
}
// CheckIfOvalFetched checks if oval entries are in DB by family, release.
func (b Base) CheckIfOvalFetched(osFamily, release string) (fetched bool, err error) {
if !b.isFetchViaHTTP() {
var ovaldb db.DB
if ovaldb, _, err = db.NewDB(
osFamily,
config.Conf.OvalDBType,
config.Conf.OvalDBPath,
config.Conf.DebugSQL,
); err != nil {
return false, err
}
defer ovaldb.CloseDB()
count, err := ovaldb.CountDefs(osFamily, release)
func (b Base) CheckIfOvalFetched(driver db.DB, osFamily, release string) (fetched bool, err error) {
if !b.IsFetchViaHTTP() {
count, err := driver.CountDefs(osFamily, release)
if err != nil {
return false, fmt.Errorf("Failed to count OVAL defs: %s, %s, %v",
osFamily, release, err)
@@ -85,7 +76,7 @@ func (b Base) CheckIfOvalFetched(osFamily, release string) (fetched bool, err er
return 0 < count, nil
}
url, _ := util.URLPathJoin(config.Conf.OvalDBURL, "count", osFamily, release)
url, _ := util.URLPathJoin(cnf.Conf.OvalDict.URL, "count", osFamily, release)
resp, body, errs := gorequest.New().Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return false, fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v",
@@ -100,22 +91,12 @@ func (b Base) CheckIfOvalFetched(osFamily, release string) (fetched bool, err er
}
// CheckIfOvalFresh checks if oval entries are fresh enough
func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) {
func (b Base) CheckIfOvalFresh(driver db.DB, osFamily, release string) (ok bool, err error) {
var lastModified time.Time
if !b.isFetchViaHTTP() {
var ovaldb db.DB
if ovaldb, _, err = db.NewDB(
osFamily,
config.Conf.OvalDBType,
config.Conf.OvalDBPath,
config.Conf.DebugSQL,
); err != nil {
return false, err
}
defer ovaldb.CloseDB()
lastModified = ovaldb.GetLastModified(osFamily, release)
if !b.IsFetchViaHTTP() {
lastModified = driver.GetLastModified(osFamily, release)
} else {
url, _ := util.URLPathJoin(config.Conf.OvalDBURL, "lastmodified", osFamily, release)
url, _ := util.URLPathJoin(cnf.Conf.OvalDict.URL, "lastmodified", osFamily, release)
resp, body, errs := gorequest.New().Get(url).End()
if 0 < len(errs) || resp == nil || resp.StatusCode != 200 {
return false, fmt.Errorf("HTTP GET error: %v, url: %s, resp: %v",
@@ -139,7 +120,8 @@ func (b Base) CheckIfOvalFresh(osFamily, release string) (ok bool, err error) {
return true, nil
}
func (b Base) isFetchViaHTTP() bool {
// IsFetchViaHTTP checks whether fetch via HTTP
func (b Base) IsFetchViaHTTP() bool {
// Default value of OvalDBType is sqlite3
return config.Conf.OvalDBURL != "" && config.Conf.OvalDBType == "sqlite3"
return cnf.Conf.OvalDict.URL != "" && cnf.Conf.OvalDict.Type == "sqlite3"
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/future-architect/vuls/config"
"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"
)
@@ -34,20 +35,20 @@ type RedHatBase struct {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o RedHatBase) FillWithOval(r *models.ScanResult) (err error) {
func (o RedHatBase) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if o.isFetchViaHTTP() {
if o.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
return err
return 0, err
}
} else {
if relatedDefs, err = getDefsByPackNameFromOvalDB(r); err != nil {
return err
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
for _, defPacks := range relatedDefs.entries {
o.update(r, defPacks)
nCVEs += o.update(r, defPacks)
}
for _, vuln := range r.ScannedCves {
@@ -64,7 +65,8 @@ func (o RedHatBase) FillWithOval(r *models.ScanResult) (err error) {
}
}
}
return nil
return nCVEs, nil
}
var kernelRelatedPackNames = map[string]bool{
@@ -98,7 +100,7 @@ var kernelRelatedPackNames = map[string]bool{
"python-perf": true,
}
func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) {
func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) (nCVEs int) {
ctype := models.NewCveContentType(o.family)
for _, cve := range defPacks.def.Advisory.Cves {
ovalContent := *o.convertToModel(cve.CveID, &defPacks.def)
@@ -107,9 +109,10 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) {
util.Log.Debugf("%s is newly detected by OVAL", cve.CveID)
vinfo = models.VulnInfo{
CveID: cve.CveID,
Confidence: models.OvalMatch,
Confidences: models.Confidences{models.OvalMatch},
CveContents: models.NewCveContents(ovalContent),
}
nCVEs++
} else {
cveContents := vinfo.CveContents
if v, ok := vinfo.CveContents[ctype]; ok {
@@ -125,22 +128,24 @@ func (o RedHatBase) update(r *models.ScanResult, defPacks defPacks) {
cveContents = models.CveContents{}
}
if vinfo.Confidence.Score < models.OvalMatch.Score {
vinfo.Confidence = models.OvalMatch
}
vinfo.Confidences.AppendIfMissing(models.OvalMatch)
cveContents[ctype] = ovalContent
vinfo.CveContents = cveContents
}
// uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames)
for _, pack := range vinfo.AffectedPackages {
notFixedYet, _ := defPacks.actuallyAffectedPackNames[pack.Name]
defPacks.actuallyAffectedPackNames[pack.Name] = notFixedYet
if nfy, ok := defPacks.actuallyAffectedPackNames[pack.Name]; !ok {
defPacks.actuallyAffectedPackNames[pack.Name] = pack.NotFixedYet
} else if nfy {
defPacks.actuallyAffectedPackNames[pack.Name] = true
}
}
vinfo.AffectedPackages = defPacks.toPackStatuses()
vinfo.AffectedPackages.Sort()
r.ScannedCves[cve.CveID] = vinfo
}
return
}
func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *models.CveContent {
@@ -165,20 +170,32 @@ func (o RedHatBase) convertToModel(cveID string, def *ovalmodels.Definition) *mo
severity = cve.Impact
}
sev2, sev3 := "", ""
if score2 != 0 {
sev2 = severity
}
if score3 != 0 {
sev3 = severity
}
// CWE-ID in RedHat OVAL may have multiple cweIDs separated by space
cwes := strings.Fields(cve.Cwe)
return &models.CveContent{
Type: models.NewCveContentType(o.family),
CveID: cve.CveID,
Title: def.Title,
Summary: def.Description,
Severity: severity,
Cvss2Score: score2,
Cvss2Vector: vec2,
Cvss3Score: score3,
Cvss3Vector: vec3,
References: refs,
CweID: cve.Cwe,
Published: def.Advisory.Issued,
LastModified: def.Advisory.Updated,
Type: models.NewCveContentType(o.family),
CveID: cve.CveID,
Title: def.Title,
Summary: def.Description,
Cvss2Score: score2,
Cvss2Vector: vec2,
Cvss2Severity: sev2,
Cvss3Score: score3,
Cvss3Vector: vec3,
Cvss3Severity: sev3,
References: refs,
CweIDs: cwes,
Published: def.Advisory.Issued,
LastModified: def.Advisory.Updated,
}
}
return nil
@@ -207,7 +224,7 @@ func (o RedHatBase) parseCvss3(scoreVector string) (score float64, vector string
if score, err = strconv.ParseFloat(ss[0], 64); err != nil {
return 0, ""
}
return score, strings.Join(ss[1:], "/")
return score, fmt.Sprintf("CVSS:3.0/%s", ss[1])
}
return 0, ""
}

View File

@@ -72,7 +72,7 @@ func TestParseCvss3(t *testing.T) {
in: "5.6/CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
out: out{
score: 5.6,
vector: "AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
vector: "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
},
},
{

View File

@@ -21,6 +21,7 @@ import (
"github.com/future-architect/vuls/config"
"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"
)
@@ -40,15 +41,15 @@ func NewSUSE() SUSE {
}
// FillWithOval returns scan result after updating CVE info by OVAL
func (o SUSE) FillWithOval(r *models.ScanResult) (err error) {
func (o SUSE) FillWithOval(driver db.DB, r *models.ScanResult) (nCVEs int, err error) {
var relatedDefs ovalResult
if o.isFetchViaHTTP() {
if o.IsFetchViaHTTP() {
if relatedDefs, err = getDefsByPackNameViaHTTP(r); err != nil {
return err
return 0, err
}
} else {
if relatedDefs, err = getDefsByPackNameFromOvalDB(r); err != nil {
return err
if relatedDefs, err = getDefsByPackNameFromOvalDB(driver, r); err != nil {
return 0, err
}
}
for _, defPacks := range relatedDefs.entries {
@@ -61,7 +62,7 @@ func (o SUSE) FillWithOval(r *models.ScanResult) (err error) {
vuln.CveContents[models.SUSE] = cont
}
}
return nil
return len(relatedDefs.entries), nil
}
func (o SUSE) update(r *models.ScanResult, defPacks defPacks) {
@@ -72,7 +73,7 @@ func (o SUSE) update(r *models.ScanResult, defPacks defPacks) {
util.Log.Debugf("%s is newly detected by OVAL", defPacks.def.Title)
vinfo = models.VulnInfo{
CveID: defPacks.def.Title,
Confidence: models.OvalMatch,
Confidences: models.Confidences{models.OvalMatch},
CveContents: models.NewCveContents(ovalContent),
}
} else {
@@ -84,17 +85,14 @@ func (o SUSE) update(r *models.ScanResult, defPacks defPacks) {
util.Log.Debugf("%s is also detected by OVAL", defPacks.def.Title)
cveContents = models.CveContents{}
}
if vinfo.Confidence.Score < models.OvalMatch.Score {
vinfo.Confidence = models.OvalMatch
}
vinfo.Confidences.AppendIfMissing(models.OvalMatch)
cveContents[ctype] = ovalContent
vinfo.CveContents = cveContents
}
// uniq(vinfo.PackNames + defPacks.actuallyAffectedPackNames)
for _, pack := range vinfo.AffectedPackages {
notFixedYet, _ := defPacks.actuallyAffectedPackNames[pack.Name]
defPacks.actuallyAffectedPackNames[pack.Name] = notFixedYet
defPacks.actuallyAffectedPackNames[pack.Name] = pack.NotFixedYet
}
vinfo.AffectedPackages = defPacks.toPackStatuses()
vinfo.AffectedPackages.Sort()

View File

@@ -126,7 +126,7 @@ func getDefsByPackNameViaHTTP(r *models.ScanResult) (
select {
case req := <-reqChan:
url, err := util.URLPathJoin(
config.Conf.OvalDBURL,
config.Conf.OvalDict.URL,
"packs",
r.Family,
r.Release,
@@ -216,20 +216,7 @@ func httpGet(url string, req request, resChan chan<- response, errChan chan<- er
}
}
func getDefsByPackNameFromOvalDB(r *models.ScanResult) (relatedDefs ovalResult, err error) {
path := config.Conf.OvalDBURL
if config.Conf.OvalDBType == "sqlite3" {
path = config.Conf.OvalDBPath
}
util.Log.Debugf("Open oval-dictionary db (%s): %s", config.Conf.OvalDBType, path)
var ovaldb db.DB
if ovaldb, _, err = db.NewDB(r.Family, config.Conf.OvalDBType,
path, config.Conf.DebugSQL); err != nil {
return
}
defer ovaldb.CloseDB()
func getDefsByPackNameFromOvalDB(driver db.DB, r *models.ScanResult) (relatedDefs ovalResult, err error) {
requests := []request{}
for _, pack := range r.Packages {
requests = append(requests, request{
@@ -249,9 +236,9 @@ func getDefsByPackNameFromOvalDB(r *models.ScanResult) (relatedDefs ovalResult,
}
for _, req := range requests {
definitions, err := ovaldb.GetByPackName(r.Release, req.packName)
definitions, err := driver.GetByPackName(r.Release, req.packName)
if err != nil {
return relatedDefs, fmt.Errorf("Failed to get %s OVAL info by package name: %v", r.Family, err)
return relatedDefs, fmt.Errorf("Failed to get %s OVAL info by package: %#v, err: %s", r.Family, req, err)
}
for _, def := range definitions {
affected, notFixedYet := isOvalDefAffected(def, req, r.Family, r.RunningKernel)
@@ -316,8 +303,12 @@ func isOvalDefAffected(def ovalmodels.Definition, req request, family string, ru
// Unable to judge whether fixed or not fixed of src package(Ubuntu, Debian)
return true, false
}
// `offline` or `fast` scan mode can't get a updatable version.
// In these mode, the blow field was set empty.
// Vuls can not judge fixed or unfixed.
if req.NewVersionRelease == "" {
return true, true
return true, false
}
// compare version: newVer vs oval

View File

@@ -319,7 +319,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: false,
},
// RedHat
// 5 RedHat
{
in: in{
family: "redhat",
@@ -337,64 +337,16 @@ func TestIsOvalDefAffected(t *testing.T) {
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.7",
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.7",
NewVersionRelease: "",
},
},
affected: true,
notFixedYet: true,
},
{
in: in{
family: "redhat",
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.8",
},
},
affected: false,
notFixedYet: false,
},
{
in: in{
family: "redhat",
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.9",
},
},
affected: false,
notFixedYet: false,
},
// 6 RedHat
{
in: in{
family: "redhat",
@@ -421,6 +373,86 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: true,
},
// 7 RedHat
{
in: in{
family: "redhat",
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.8",
},
},
affected: false,
notFixedYet: false,
},
// 8 RedHat
{
in: in{
family: "redhat",
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.9",
},
},
affected: false,
notFixedYet: false,
},
// 9 RedHat
{
in: in{
family: "redhat",
def: ovalmodels.Definition{
AffectedPacks: []ovalmodels.Package{
{
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6_7.6",
NewVersionRelease: "0:1.2.3-45.el6_7.7",
},
},
affected: true,
notFixedYet: true,
},
// 10 RedHat
{
in: in{
family: "redhat",
@@ -447,6 +479,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: false,
},
// 11 RedHat
{
in: in{
family: "redhat",
@@ -456,8 +489,7 @@ func TestIsOvalDefAffected(t *testing.T) {
Name: "a",
NotFixedYet: false,
},
{
Name: "b",
{Name: "b",
NotFixedYet: false,
Version: "0:1.2.3-45.el6_7.8",
},
@@ -473,6 +505,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: false,
},
// 12 RedHat
{
in: in{
family: "redhat",
@@ -498,6 +531,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// 13 RedHat
{
in: in{
family: "redhat",
@@ -523,7 +557,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// CentOS
// 14 CentOS
{
in: in{
family: "centos",
@@ -541,14 +575,16 @@ func TestIsOvalDefAffected(t *testing.T) {
},
},
req: request{
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6.centos.7",
packName: "b",
isSrcPack: false,
versionRelease: "0:1.2.3-45.el6.centos.7",
NewVersionRelease: "",
},
},
affected: true,
notFixedYet: true,
notFixedYet: false,
},
// 15
{
in: in{
family: "centos",
@@ -574,6 +610,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// 16
{
in: in{
family: "centos",
@@ -599,6 +636,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// 17
{
in: in{
family: "centos",
@@ -625,6 +663,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: true,
},
// 18
{
in: in{
family: "centos",
@@ -651,6 +690,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: false,
},
// 19
{
in: in{
family: "centos",
@@ -677,6 +717,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: true,
notFixedYet: false,
},
// 20
{
in: in{
family: "centos",
@@ -702,6 +743,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// 21
{
in: in{
family: "centos",
@@ -727,7 +769,7 @@ func TestIsOvalDefAffected(t *testing.T) {
affected: false,
notFixedYet: false,
},
// TODO: If vuls support Scientific, replace "centos" below to "scientific".
// 22
{
in: in{
family: "centos",
@@ -751,7 +793,7 @@ func TestIsOvalDefAffected(t *testing.T) {
},
},
affected: true,
notFixedYet: true,
notFixedYet: false,
},
{
in: in{

View File

@@ -67,9 +67,9 @@ func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) {
}
}
if c.Conf.FormatShortText {
if c.Conf.FormatList {
k := key + "_short.txt"
b := []byte(formatShortPlainText(r))
b := []byte(formatList(r))
if err := createBlockBlob(cli, k, b); err != nil {
return err
}
@@ -111,19 +111,19 @@ func CheckIfAzureContainerExists() error {
found := false
for _, con := range r.Containers {
if con.Name == c.Conf.AzureContainer {
if con.Name == c.Conf.Azure.ContainerName {
found = true
break
}
}
if !found {
return fmt.Errorf("Container not found. Container: %s", c.Conf.AzureContainer)
return fmt.Errorf("Container not found. Container: %s", c.Conf.Azure.ContainerName)
}
return nil
}
func getBlobClient() (storage.BlobStorageClient, error) {
api, err := storage.NewBasicClient(c.Conf.AzureAccount, c.Conf.AzureKey)
api, err := storage.NewBasicClient(c.Conf.Azure.AccountName, c.Conf.Azure.AccountKey)
if err != nil {
return storage.BlobStorageClient{}, err
}
@@ -139,11 +139,11 @@ func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte) error {
k = k + ".gz"
}
ref := cli.GetContainerReference(c.Conf.AzureContainer)
ref := cli.GetContainerReference(c.Conf.Azure.ContainerName)
blob := ref.GetBlobReference(k)
if err := blob.CreateBlockBlobFromReader(bytes.NewReader(b), nil); err != nil {
return fmt.Errorf("Failed to upload data to %s/%s, %s",
c.Conf.AzureContainer, k, err)
c.Conf.Azure.ContainerName, k, err)
}
return nil
}

View File

@@ -28,10 +28,8 @@ import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/util"
cveconfig "github.com/kotakanbe/go-cve-dictionary/config"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
cve "github.com/kotakanbe/go-cve-dictionary/models"
log "github.com/sirupsen/logrus"
)
// CveClient is api client of CVE disctionary service.
@@ -43,12 +41,12 @@ type cvedictClient struct {
}
func (api *cvedictClient) initialize() {
api.baseURL = config.Conf.CveDBURL
api.baseURL = config.Conf.CveDict.URL
}
func (api cvedictClient) CheckHealth() error {
if !api.isFetchViaHTTP() {
util.Log.Debugf("get cve-dictionary from %s", config.Conf.CveDBType)
util.Log.Debugf("get cve-dictionary from %s", config.Conf.CveDict.Type)
return nil
}
@@ -70,12 +68,25 @@ type response struct {
CveDetail cve.CveDetail
}
func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails []*cve.CveDetail, err error) {
func (api cvedictClient) FetchCveDetails(driver cvedb.DB, cveIDs []string) (cveDetails []cve.CveDetail, err error) {
if !api.isFetchViaHTTP() {
return api.FetchCveDetailsFromCveDB(cveIDs)
for _, cveID := range cveIDs {
cveDetail, err := driver.Get(cveID)
if err != nil {
return nil, fmt.Errorf("Failed to fetch CVE. err: %s", err)
}
if len(cveDetail.CveID) == 0 {
cveDetails = append(cveDetails, cve.CveDetail{
CveID: cveID,
})
} else {
cveDetails = append(cveDetails, *cveDetail)
}
}
return
}
api.baseURL = config.Conf.CveDBURL
api.baseURL = config.Conf.CveDict.URL
reqChan := make(chan string, len(cveIDs))
resChan := make(chan response, len(cveIDs))
errChan := make(chan error, len(cveIDs))
@@ -112,55 +123,25 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails []*cve.Cve
select {
case res := <-resChan:
if len(res.CveDetail.CveID) == 0 {
cveDetails = append(cveDetails, &cve.CveDetail{
cveDetails = append(cveDetails, cve.CveDetail{
CveID: res.Key,
})
} else {
cveDetails = append(cveDetails, &res.CveDetail)
cveDetails = append(cveDetails, res.CveDetail)
}
case err := <-errChan:
errs = append(errs, err)
case <-timeout:
return []*cve.CveDetail{}, fmt.Errorf("Timeout Fetching CVE")
return nil, fmt.Errorf("Timeout Fetching CVE")
}
}
if len(errs) != 0 {
return []*cve.CveDetail{},
return nil,
fmt.Errorf("Failed to fetch CVE. err: %v", errs)
}
return
}
func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails []*cve.CveDetail, err error) {
util.Log.Debugf("open cve-dictionary db (%s)", config.Conf.CveDBType)
cveconfig.Conf.DBType = config.Conf.CveDBType
if config.Conf.CveDBType == "sqlite3" {
cveconfig.Conf.DBPath = config.Conf.CveDBPath
} else {
cveconfig.Conf.DBPath = config.Conf.CveDBURL
}
cveconfig.Conf.DebugSQL = config.Conf.DebugSQL
var driver cvedb.DB
if driver, err = cvedb.NewDB(cveconfig.Conf.DBType, cveconfig.Conf.DBPath, cveconfig.Conf.DebugSQL); err != nil {
log.Error(err)
return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err)
}
defer driver.CloseDB()
for _, cveID := range cveIDs {
cveDetail := driver.Get(cveID)
if len(cveDetail.CveID) == 0 {
cveDetails = append(cveDetails, &cve.CveDetail{
CveID: cveID,
})
} else {
cveDetails = append(cveDetails, cveDetail)
}
}
return
}
func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errChan chan<- error) {
var body string
var errs []error
@@ -195,36 +176,30 @@ func (api cvedictClient) httpGet(key, url string, resChan chan<- response, errCh
}
}
type responseGetCveDetailByCpeName struct {
CpeName string
CveDetails []cve.CveDetail
}
func (api cvedictClient) isFetchViaHTTP() bool {
// Default value of CveDBType is sqlite3
if config.Conf.CveDBURL != "" && config.Conf.CveDBType == "sqlite3" {
if config.Conf.CveDict.URL != "" && config.Conf.CveDict.Type == "sqlite3" {
return true
}
return false
}
func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]*cve.CveDetail, error) {
func (api cvedictClient) FetchCveDetailsByCpeName(driver cvedb.DB, cpeName string) ([]cve.CveDetail, error) {
if api.isFetchViaHTTP() {
api.baseURL = config.Conf.CveDBURL
api.baseURL = config.Conf.CveDict.URL
url, err := util.URLPathJoin(api.baseURL, "cpes")
if err != nil {
return []*cve.CveDetail{}, err
return nil, err
}
query := map[string]string{"name": cpeName}
util.Log.Debugf("HTTP Request to %s, query: %#v", url, query)
return api.httpPost(cpeName, url, query)
}
return api.FetchCveDetailsByCpeNameFromDB(cpeName)
return driver.GetByCpeURI(cpeName)
}
func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]*cve.CveDetail, error) {
func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]cve.CveDetail, error) {
var body string
var errs []error
var resp *http.Response
@@ -245,33 +220,13 @@ func (api cvedictClient) httpPost(key, url string, query map[string]string) ([]*
}
err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify)
if err != nil {
return []*cve.CveDetail{}, fmt.Errorf("HTTP Error %s", err)
return nil, fmt.Errorf("HTTP Error %s", err)
}
cveDetails := []*cve.CveDetail{}
cveDetails := []cve.CveDetail{}
if err := json.Unmarshal([]byte(body), &cveDetails); err != nil {
return []*cve.CveDetail{},
return nil,
fmt.Errorf("Failed to Unmarshall. body: %s, err: %s", body, err)
}
return cveDetails, nil
}
func (api cvedictClient) FetchCveDetailsByCpeNameFromDB(cpeName string) (cveDetails []*cve.CveDetail, err error) {
util.Log.Debugf("open cve-dictionary db (%s)", config.Conf.CveDBType)
cveconfig.Conf.DBType = config.Conf.CveDBType
if config.Conf.CveDBType == "sqlite3" {
cveconfig.Conf.DBPath = config.Conf.CveDBPath
} else {
cveconfig.Conf.DBPath = config.Conf.CveDBURL
}
cveconfig.Conf.DebugSQL = config.Conf.DebugSQL
var driver cvedb.DB
if driver, err = cvedb.NewDB(cveconfig.Conf.DBType, cveconfig.Conf.DBPath, cveconfig.Conf.DebugSQL); err != nil {
log.Error(err)
return []*cve.CveDetail{}, fmt.Errorf("Failed to New DB. err: %s", err)
}
util.Log.Debugf("Opening DB (%s).", driver.Name())
return driver.GetByCpeName(cpeName), nil
}

158
report/db_client.go Normal file
View File

@@ -0,0 +1,158 @@
package report
import (
"fmt"
"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"
)
// DBClient is a dictionarie's db client for reporting
type DBClient struct {
CveDB cvedb.DB
OvalDB ovaldb.DB
GostDB gostdb.DB
}
// DBClientConf has a configuration of Vulnerability DBs
type DBClientConf struct {
CveDictCnf config.GoCveDictConf
OvalDictCnf config.GovalDictConf
GostCnf config.GostConf
DebugSQL bool
}
func (c DBClientConf) isCveDBViaHTTP() bool {
return c.CveDictCnf.URL != "" && c.CveDictCnf.Type == "sqlite3"
}
func (c DBClientConf) isOvalViaHTTP() bool {
return c.OvalDictCnf.URL != "" && c.OvalDictCnf.Type == "sqlite3"
}
func (c DBClientConf) isGostViaHTTP() bool {
return c.GostCnf.URL != "" && c.GostCnf.Type == "sqlite3"
}
// NewDBClient returns db clients
func NewDBClient(cnf DBClientConf) (dbclient *DBClient, locked bool, err error) {
cveDriver, locked, err := NewCveDB(cnf)
if err != nil {
return nil, locked, err
}
ovaldb, locked, err := NewOvalDB(cnf)
if locked {
return nil, true, fmt.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, fmt.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)
}
return &DBClient{
CveDB: cveDriver,
OvalDB: ovaldb,
GostDB: gostdb,
}, false, nil
}
// NewCveDB returns cve db client
func NewCveDB(cnf DBClientConf) (driver cvedb.DB, locked bool, err error) {
if cnf.isCveDBViaHTTP() {
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
}
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 = fmt.Errorf("Failed to init CVE DB. err: %s, 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 cnf.isOvalViaHTTP() {
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 is not found. It's recommended to use OVAL to improve scanning accuracy. For details, see https://github.com/kotakanbe/goval-dictionary#usage", 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 = fmt.Errorf("Failed to new OVAL DB. err: %s", 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 cnf.isGostViaHTTP() {
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 is not found. If the scan target server is Debian, RHEL or CentOS, it's recommended to use gost to improve scanning accuracy. To use gost database, 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: %s", 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: %s", err)
}
}
if d.OvalDB != nil {
if err := d.OvalDB.CloseDB(); err != nil {
util.Log.Errorf("Failed to close DB: %s", err)
}
}
}

62
report/http.go Normal file
View File

@@ -0,0 +1,62 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"bytes"
"encoding/json"
"net/http"
"github.com/pkg/errors"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
// HTTPRequestWriter writes results to HTTP request
type HTTPRequestWriter struct{}
// Write sends results as HTTP response
func (w HTTPRequestWriter) Write(rs ...models.ScanResult) (err error) {
for _, r := range rs {
b := new(bytes.Buffer)
json.NewEncoder(b).Encode(r)
_, err = http.Post(c.Conf.HTTP.URL, "application/json; charset=utf-8", b)
if err != nil {
return err
}
}
return nil
}
// HTTPResponseWriter writes results to HTTP response
type HTTPResponseWriter struct {
Writer http.ResponseWriter
}
// Write sends results as HTTP response
func (w HTTPResponseWriter) Write(rs ...models.ScanResult) (err error) {
res, err := json.Marshal(rs)
if err != nil {
return errors.Wrap(err, "Failed to marshal scah results")
}
w.Writer.Header().Set("Content-Type", "application/json")
_, err = w.Writer.Write(res)
return err
}

View File

@@ -72,7 +72,7 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
}
}
if c.Conf.FormatShortText {
if c.Conf.FormatList {
var p string
if c.Conf.Diff {
p = path + "_short_diff.txt"
@@ -81,7 +81,7 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) {
}
if err := writeFile(
p, []byte(formatShortPlainText(r)), 0600); err != nil {
p, []byte(formatList(r)), 0600); err != nil {
return fmt.Errorf(
"Failed to write text files. path: %s, err: %s", p, err)
}
@@ -131,11 +131,5 @@ func writeFile(path string, data []byte, perm os.FileMode) error {
}
path = path + ".gz"
}
if err := ioutil.WriteFile(
path, []byte(data), perm); err != nil {
return err
}
return nil
return ioutil.WriteFile(path, []byte(data), perm)
}

View File

@@ -18,13 +18,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package report
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"reflect"
"regexp"
"sort"
"strings"
"time"
"github.com/BurntSushi/toml"
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/gost"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/oval"
"github.com/future-architect/vuls/util"
"github.com/hashicorp/uuid"
gostdb "github.com/knqyf263/gost/db"
cvedb "github.com/kotakanbe/go-cve-dictionary/db"
ovaldb "github.com/kotakanbe/goval-dictionary/db"
)
const (
@@ -33,16 +48,49 @@ const (
)
// FillCveInfos fills CVE Detailed Information
func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
func FillCveInfos(dbclient DBClient, rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
var filledResults []models.ScanResult
reportedAt := time.Now()
hostname, _ := os.Hostname()
for _, r := range rs {
if c.Conf.RefreshCve || needToRefreshCve(r) {
if err := FillCveInfo(&r); err != nil {
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, fmt.Errorf("Failed to read OWASP Dependency Check XML: %s, %s, %s",
r.ServerName, owaspDCXMLPath, err)
}
cpeURIs = append(cpeURIs, cpes...)
}
} else {
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, fmt.Errorf("Failed to read OWASP Dependency Check XML: %s, %s, %s",
r.ServerInfo(), owaspDCXMLPath, err)
}
cpeURIs = append(cpeURIs, cpes...)
}
}
}
}
if err := FillCveInfo(dbclient, &r, cpeURIs); err != nil {
return nil, err
}
r.Lang = c.Conf.Lang
r.ReportedAt = reportedAt
r.ReportedVersion = c.Version
r.ReportedRevision = c.Revision
r.ReportedBy = hostname
r.Config.Report = c.Conf
r.Config.Report.Servers = map[string]c.ServerInfo{
r.ServerName: c.Conf.Servers[r.ServerName],
@@ -69,7 +117,7 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro
}
filledResults = []models.ScanResult{}
for _, r := range diff {
if err := fillCveDetail(&r); err != nil {
if err := fillCveDetail(dbclient.CveDB, &r); err != nil {
return nil, err
}
filledResults = append(filledResults, r)
@@ -79,8 +127,9 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro
filtered := []models.ScanResult{}
for _, r := range filledResults {
r = r.FilterByCvssOver(c.Conf.CvssScoreOver)
r = r.FilterIgnoreCves(c.Conf.Servers[r.ServerName].IgnoreCves)
r = r.FilterIgnoreCves()
r = r.FilterUnfixed()
r = r.FilterIgnorePkgs()
if c.Conf.IgnoreUnscoredCves {
r.ScannedCves = r.ScannedCves.FindScoredVulns()
}
@@ -90,48 +139,73 @@ func FillCveInfos(rs []models.ScanResult, dir string) ([]models.ScanResult, erro
}
// FillCveInfo fill scanResult with cve info.
func FillCveInfo(r *models.ScanResult) error {
func FillCveInfo(dbclient DBClient, r *models.ScanResult, cpeURIs []string) error {
util.Log.Debugf("need to refresh")
util.Log.Infof("Fill CVE detailed information with OVAL")
if err := FillWithOval(r); err != nil {
return fmt.Errorf("Failed to fill OVAL information: %s", err)
nCVEs, err := FillWithOval(dbclient.OvalDB, r)
if err != nil {
return fmt.Errorf("Failed to fill with OVAL: %s", err)
}
util.Log.Infof("%s: %d CVEs are detected with OVAL",
r.FormatServerName(), nCVEs)
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
}
}
}
nCVEs, err = fillVulnByCpeURIs(dbclient.CveDB, r, cpeURIs)
if err != nil {
return fmt.Errorf("Failed to detect vulns of %s: %s", cpeURIs, err)
}
util.Log.Infof("%s: %d CVEs are detected with CPE", r.FormatServerName(), nCVEs)
nCVEs, err = FillWithGost(dbclient.GostDB, r)
if err != nil {
return fmt.Errorf("Failed to fill with gost: %s", err)
}
util.Log.Infof("%s: %d unfixed CVEs are detected with gost",
r.FormatServerName(), nCVEs)
util.Log.Infof("Fill CVE detailed information with CVE-DB")
if err := fillWithCveDB(r); err != nil {
return fmt.Errorf("Failed to fill CVE information: %s", err)
if err := fillCveDetail(dbclient.CveDB, r); err != nil {
return fmt.Errorf("Failed to fill with CVE: %s", err)
}
for cveID := range r.ScannedCves {
vinfo := r.ScannedCves[cveID]
r.ScannedCves[cveID] = *vinfo.NilToEmpty()
}
fillCweDict(r)
return nil
}
// fillCveDetail fetches NVD, JVN from CVE Database, and then set to fields.
func fillCveDetail(r *models.ScanResult) error {
// fillCveDetail fetches NVD, JVN from CVE Database
func fillCveDetail(driver cvedb.DB, r *models.ScanResult) error {
var cveIDs []string
for _, v := range r.ScannedCves {
cveIDs = append(cveIDs, v.CveID)
}
ds, err := CveClient.FetchCveDetails(cveIDs)
ds, err := CveClient.FetchCveDetails(driver, cveIDs)
if err != nil {
return err
}
for _, d := range ds {
nvd := models.ConvertNvdToModel(d.CveID, d.Nvd)
nvd := models.ConvertNvdJSONToModel(d.CveID, d.NvdJSON)
if nvd == nil {
nvd = models.ConvertNvdXMLToModel(d.CveID, d.NvdXML)
}
jvn := models.ConvertJvnToModel(d.CveID, d.Jvn)
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.Empty() {
vinfo.CveContents[con.Type] = con
for _, con := range []*models.CveContent{nvd, jvn} {
if con != nil && !con.Empty() {
vinfo.CveContents[con.Type] = *con
}
}
r.ScannedCves[cveID] = vinfo
@@ -142,23 +216,11 @@ func fillCveDetail(r *models.ScanResult) error {
return nil
}
func fillWithCveDB(r *models.ScanResult) error {
sInfo := c.Conf.Servers[r.ServerName]
if err := fillVulnByCpeNames(sInfo.CpeNames, r.ScannedCves); err != nil {
return err
}
if err := fillCveDetail(r); err != nil {
return err
}
return nil
}
// FillWithOval fetches OVAL database, and then set to fields.
func FillWithOval(r *models.ScanResult) (err error) {
// FillWithOval fetches OVAL database
func FillWithOval(driver ovaldb.DB, r *models.ScanResult) (nCVEs int, err error) {
var ovalClient oval.Client
var ovalFamily string
// TODO
switch r.Family {
case c.Debian:
ovalClient = oval.NewDebian()
@@ -184,60 +246,367 @@ func FillWithOval(r *models.ScanResult) (err error) {
ovalClient = oval.NewAlpine()
ovalFamily = c.Alpine
case c.Amazon, c.Raspbian, c.FreeBSD, c.Windows:
return nil
return 0, nil
case c.ServerTypePseudo:
return nil
return 0, nil
default:
if r.Family == "" {
return fmt.Errorf("Probably an error occurred during scanning. Check the error message")
return 0, fmt.Errorf("Probably an error occurred during scanning. Check the error message")
}
return fmt.Errorf("OVAL for %s is not implemented yet", r.Family)
return 0, fmt.Errorf("OVAL for %s is not implemented yet", r.Family)
}
util.Log.Debugf("Check whether oval is already fetched: %s %s",
if !ovalClient.IsFetchViaHTTP() && driver == nil {
return 0, nil
}
if err = driver.NewOvalDB(ovalFamily); err != nil {
return 0, fmt.Errorf("Failed to New Oval DB. err: %s", err)
}
util.Log.Debugf("Check whether oval fetched: %s %s",
ovalFamily, r.Release)
ok, err := ovalClient.CheckIfOvalFetched(ovalFamily, r.Release)
ok, err := ovalClient.CheckIfOvalFetched(driver, ovalFamily, r.Release)
if err != nil {
return err
return 0, err
}
if !ok {
util.Log.Warnf("OVAL entries of %s %s are not found. It's recommended to use OVAL to improve scanning accuracy. For details, see https://github.com/kotakanbe/goval-dictionary#usage , Then report with --ovaldb-path or --ovaldb-url flag", ovalFamily, r.Release)
return nil
return 0, nil
}
_, err = ovalClient.CheckIfOvalFresh(ovalFamily, r.Release)
_, err = ovalClient.CheckIfOvalFresh(driver, ovalFamily, r.Release)
if err != nil {
return err
return 0, err
}
if err := ovalClient.FillWithOval(r); err != nil {
return err
}
return nil
return ovalClient.FillWithOval(driver, r)
}
func fillVulnByCpeNames(cpeNames []string, scannedVulns models.VulnInfos) error {
for _, name := range cpeNames {
details, err := CveClient.FetchCveDetailsByCpeName(name)
// FillWithGost fills CVEs with gost dataabase
// https://github.com/knqyf263/gost
func FillWithGost(driver gostdb.DB, r *models.ScanResult) (nCVEs int, err error) {
gostClient := gost.NewClient(r.Family)
// TODO chekc if fetched
// TODO chekc if fresh enough
return gostClient.FillWithGost(driver, r)
}
func fillVulnByCpeURIs(driver cvedb.DB, r *models.ScanResult, cpeURIs []string) (nCVEs int, err error) {
for _, name := range cpeURIs {
details, err := CveClient.FetchCveDetailsByCpeName(driver, name)
if err != nil {
return err
return 0, err
}
for _, detail := range details {
if val, ok := scannedVulns[detail.CveID]; ok {
names := val.CpeNames
if val, ok := r.ScannedCves[detail.CveID]; ok {
names := val.CpeURIs
names = util.AppendIfMissing(names, name)
val.CpeNames = names
val.Confidence = models.CpeNameMatch
scannedVulns[detail.CveID] = val
val.CpeURIs = names
val.Confidences.AppendIfMissing(models.CpeNameMatch)
r.ScannedCves[detail.CveID] = val
} else {
v := models.VulnInfo{
CveID: detail.CveID,
CpeNames: []string{name},
Confidence: models.CpeNameMatch,
CveID: detail.CveID,
CpeURIs: []string{name},
Confidences: models.Confidences{models.CpeNameMatch},
}
scannedVulns[detail.CveID] = v
r.ScannedCves[detail.CveID] = v
nCVEs++
}
}
}
return nil
return nCVEs, 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
}
}
}
}
// TODO check the format of CWEID, clean CWEID
// JVN, NVD XML, JSON, OVALs
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
}
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
}
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
}
const reUUID = "[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}"
// EnsureUUIDs generate a new UUID of the scan target server if UUID is not assigned yet.
// And then set the generated UUID to config.toml and scan results.
func EnsureUUIDs(configPath string, results models.ScanResults) error {
// Sort Host->Container
sort.Slice(results, func(i, j int) bool {
if results[i].ServerName == results[j].ServerName {
return results[i].Container.ContainerID < results[j].Container.ContainerID
}
return results[i].ServerName < results[j].ServerName
})
for i, r := range results {
server := c.Conf.Servers[r.ServerName]
if server.UUIDs == nil {
server.UUIDs = map[string]string{}
}
name := ""
if r.IsContainer() {
name = fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
// Scanning with the -containers-only flag at scan time, the UUID of Container Host may not be generated,
// so check it. Otherwise create a UUID of the Container Host and set it.
serverUUID := ""
if id, ok := server.UUIDs[r.ServerName]; !ok {
serverUUID = uuid.GenerateUUID()
} else {
matched, err := regexp.MatchString(reUUID, id)
if !matched || err != nil {
serverUUID = uuid.GenerateUUID()
}
}
if serverUUID != "" {
server.UUIDs[r.ServerName] = serverUUID
}
} else {
name = r.ServerName
}
if id, ok := server.UUIDs[name]; ok {
matched, err := regexp.MatchString(reUUID, id)
if !matched || err != nil {
util.Log.Warnf("UUID is invalid. Re-generate UUID %s: %s", id, err)
} else {
if r.IsContainer() {
results[i].Container.UUID = id
results[i].ServerUUID = server.UUIDs[r.ServerName]
} else {
results[i].ServerUUID = id
}
// continue if the UUID has already assigned and valid
continue
}
}
// Generate a new UUID and set to config and scan result
id := uuid.GenerateUUID()
server.UUIDs[name] = id
server = cleanForTOMLEncoding(server, c.Conf.Default)
c.Conf.Servers[r.ServerName] = server
if r.IsContainer() {
results[i].Container.UUID = id
results[i].ServerUUID = server.UUIDs[r.ServerName]
} else {
results[i].ServerUUID = id
}
}
for name, server := range c.Conf.Servers {
server = cleanForTOMLEncoding(server, c.Conf.Default)
c.Conf.Servers[name] = server
}
email := &c.Conf.EMail
if email.SMTPAddr == "" {
email = nil
}
slack := &c.Conf.Slack
if slack.HookURL == "" {
slack = nil
}
cveDict := &c.Conf.CveDict
ovalDict := &c.Conf.OvalDict
gost := &c.Conf.Gost
http := &c.Conf.HTTP
if http.URL == "" {
http = nil
}
syslog := &c.Conf.Syslog
if syslog.Host == "" {
syslog = nil
}
aws := &c.Conf.AWS
if aws.S3Bucket == "" {
aws = nil
}
azure := &c.Conf.Azure
if azure.AccountName == "" {
azure = nil
}
stride := &c.Conf.Stride
if stride.HookURL == "" {
stride = nil
}
hipChat := &c.Conf.HipChat
if hipChat.AuthToken == "" {
hipChat = nil
}
chatWork := &c.Conf.ChatWork
if chatWork.APIToken == "" {
chatWork = nil
}
saas := &c.Conf.Saas
if saas.GroupID == 0 {
saas = nil
}
c := struct {
CveDict *c.GoCveDictConf `toml:"cveDict"`
OvalDict *c.GovalDictConf `toml:"ovalDict"`
Gost *c.GostConf `toml:"gost"`
Slack *c.SlackConf `toml:"slack"`
Email *c.SMTPConf `toml:"email"`
HTTP *c.HTTPConf `toml:"http"`
Syslog *c.SyslogConf `toml:"syslog"`
AWS *c.AWS `toml:"aws"`
Azure *c.Azure `toml:"azure"`
Stride *c.StrideConf `toml:"stride"`
HipChat *c.HipChatConf `toml:"hipChat"`
ChatWork *c.ChatWorkConf `toml:"chatWork"`
Saas *c.SaasConf `toml:"saas"`
Default c.ServerInfo `toml:"default"`
Servers map[string]c.ServerInfo `toml:"servers"`
}{
CveDict: cveDict,
OvalDict: ovalDict,
Gost: gost,
Slack: slack,
Email: email,
HTTP: http,
Syslog: syslog,
AWS: aws,
Azure: azure,
Stride: stride,
HipChat: hipChat,
ChatWork: chatWork,
Saas: saas,
Default: c.Conf.Default,
Servers: c.Conf.Servers,
}
// rename the current config.toml to config.toml.bak
info, err := os.Lstat(configPath)
if err != nil {
return fmt.Errorf("Failed to lstat %s: %s", configPath, err)
}
realPath := configPath
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
if realPath, err = os.Readlink(configPath); err != nil {
return fmt.Errorf("Failed to Read link %s: %s", configPath, err)
}
}
if err := os.Rename(realPath, realPath+".bak"); err != nil {
return fmt.Errorf("Failed to rename %s: %s", configPath, err)
}
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(c); err != nil {
return fmt.Errorf("Failed to encode to toml: %s", err)
}
str := strings.Replace(buf.String(), "\n [", "\n\n [", -1)
str = fmt.Sprintf("%s\n\n%s",
"# See REAME for details: https://vuls.io/docs/en/usage-settings.html",
str)
return ioutil.WriteFile(realPath, []byte(str), 0600)
}
func cleanForTOMLEncoding(server c.ServerInfo, def c.ServerInfo) c.ServerInfo {
if reflect.DeepEqual(server.Optional, def.Optional) {
server.Optional = nil
}
if def.User == server.User {
server.User = ""
}
if def.Host == server.Host {
server.Host = ""
}
if def.Port == server.Port {
server.Port = ""
}
if def.KeyPath == server.KeyPath {
server.KeyPath = ""
}
if reflect.DeepEqual(server.ScanMode, def.ScanMode) {
server.ScanMode = nil
}
if def.Type == server.Type {
server.Type = ""
}
if reflect.DeepEqual(server.CpeNames, def.CpeNames) {
server.CpeNames = nil
}
if def.OwaspDCXMLPath == server.OwaspDCXMLPath {
server.OwaspDCXMLPath = ""
}
if reflect.DeepEqual(server.IgnoreCves, def.IgnoreCves) {
server.IgnoreCves = nil
}
if reflect.DeepEqual(server.Enablerepo, def.Enablerepo) {
server.Enablerepo = nil
}
for k, v := range def.Optional {
if vv, ok := server.Optional[k]; ok && v == vv {
delete(server.Optional, k)
}
}
return server
}

View File

@@ -41,10 +41,10 @@ type S3Writer struct{}
func getS3() *s3.S3 {
Config := &aws.Config{
Region: aws.String(c.Conf.AwsRegion),
Region: aws.String(c.Conf.AWS.Region),
Credentials: credentials.NewChainCredentials([]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: c.Conf.AwsProfile},
&credentials.SharedCredentialsProvider{Filename: "", Profile: c.Conf.AWS.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(session.New())},
}),
}
@@ -82,9 +82,9 @@ func (w S3Writer) Write(rs ...models.ScanResult) (err error) {
}
}
if c.Conf.FormatShortText {
if c.Conf.FormatList {
k := key + "_short.txt"
text := formatShortPlainText(r)
text := formatList(r)
if err := putObject(svc, k, []byte(text)); err != nil {
return err
}
@@ -120,12 +120,12 @@ func CheckIfBucketExists() error {
if err != nil {
return fmt.Errorf(
"Failed to list buckets. err: %s, profile: %s, region: %s",
err, c.Conf.AwsProfile, c.Conf.AwsRegion)
err, c.Conf.AWS.Profile, c.Conf.AWS.Region)
}
found := false
for _, bucket := range result.Buckets {
if *bucket.Name == c.Conf.S3Bucket {
if *bucket.Name == c.Conf.AWS.S3Bucket {
found = true
break
}
@@ -133,7 +133,7 @@ func CheckIfBucketExists() error {
if !found {
return fmt.Errorf(
"Failed to find the buckets. profile: %s, region: %s, bucket: %s",
c.Conf.AwsProfile, c.Conf.AwsRegion, c.Conf.S3Bucket)
c.Conf.AWS.Profile, c.Conf.AWS.Region, c.Conf.AWS.S3Bucket)
}
return nil
}
@@ -148,18 +148,18 @@ func putObject(svc *s3.S3, k string, b []byte) error {
}
putObjectInput := &s3.PutObjectInput{
Bucket: aws.String(c.Conf.S3Bucket),
Key: aws.String(path.Join(c.Conf.S3ResultsDir, k)),
Bucket: aws.String(c.Conf.AWS.S3Bucket),
Key: aws.String(path.Join(c.Conf.AWS.S3ResultsDir, k)),
Body: bytes.NewReader(b),
}
if c.Conf.S3ServerSideEncryption != "" {
putObjectInput.ServerSideEncryption = aws.String(c.Conf.S3ServerSideEncryption)
if c.Conf.AWS.S3ServerSideEncryption != "" {
putObjectInput.ServerSideEncryption = aws.String(c.Conf.AWS.S3ServerSideEncryption)
}
if _, err := svc.PutObject(putObjectInput); err != nil {
return fmt.Errorf("Failed to upload data to %s/%s, %s",
c.Conf.S3Bucket, k, err)
c.Conf.AWS.S3Bucket, k, err)
}
return nil
}

153
report/saas.go Normal file
View File

@@ -0,0 +1,153 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Corporation , Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package report
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/sts"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// SaasWriter writes results to SaaS
type SaasWriter struct{}
// TempCredential : TempCredential
type TempCredential struct {
Credential *sts.Credentials `json:"Credential"`
S3Bucket string `json:"S3Bucket"`
S3ResultsDir string `json:"S3ResultsDir"`
}
type payload struct {
GroupID int `json:"GroupID"`
Token string `json:"Token"`
}
// UploadSaas : UploadSaas
func (w SaasWriter) Write(rs ...models.ScanResult) (err error) {
// dir string, configPath string, config *c.Config
if len(rs) == 0 {
return nil
}
payload := payload{
GroupID: c.Conf.Saas.GroupID,
Token: c.Conf.Saas.Token,
}
var body []byte
if body, err = json.Marshal(payload); err != nil {
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
}
var req *http.Request
if req, err = http.NewRequest("POST", c.Conf.Saas.URL, bytes.NewBuffer(body)); err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
proxy := c.Conf.HTTPProxy
var client http.Client
if proxy != "" {
proxyURL, _ := url.Parse(proxy)
client = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}
} else {
client = http.Client{}
}
var resp *http.Response
if resp, err = client.Do(req); err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Failed to get Credential. Request JSON : %s,", string(body))
}
var t []byte
if t, err = ioutil.ReadAll(resp.Body); err != nil {
return err
}
var tempCredential TempCredential
if err = json.Unmarshal(t, &tempCredential); err != nil {
return fmt.Errorf("Failed to unmarshal saas credential file. err : %s", err)
}
credential := credentials.NewStaticCredentialsFromCreds(credentials.Value{
AccessKeyID: *tempCredential.Credential.AccessKeyId,
SecretAccessKey: *tempCredential.Credential.SecretAccessKey,
SessionToken: *tempCredential.Credential.SessionToken,
})
var sess *session.Session
if sess, err = session.NewSession(&aws.Config{
Credentials: credential,
Region: aws.String("ap-northeast-1"),
}); err != nil {
return fmt.Errorf("Failed to new aws session. err : %s", err)
}
svc := s3.New(sess)
for _, r := range rs {
s3Key := renameKeyNameUTC(r.ScannedAt, r.ServerUUID, r.Container)
var b []byte
if b, err = json.Marshal(r); err != nil {
return fmt.Errorf("Failed to Marshal to JSON: %s", err)
}
util.Log.Infof("Uploading...: ServerName: %s, ", r.ServerName)
putObjectInput := &s3.PutObjectInput{
Bucket: aws.String(tempCredential.S3Bucket),
Key: aws.String(path.Join(tempCredential.S3ResultsDir, s3Key)),
Body: bytes.NewReader(b),
}
if _, err := svc.PutObject(putObjectInput); err != nil {
return fmt.Errorf("Failed to upload data to %s/%s, %s",
tempCredential.S3Bucket, s3Key, err)
}
}
return nil
}
func renameKeyNameUTC(scannedAt time.Time, uuid string, container models.Container) string {
timestr := scannedAt.UTC().Format(time.RFC3339)
if len(container.ContainerID) == 0 {
return fmt.Sprintf("%s/%s.json", timestr, uuid)
}
return fmt.Sprintf("%s/%s@%s.json", timestr, container.UUID, uuid)
}

View File

@@ -177,18 +177,20 @@ func msgText(r models.ScanResult) string {
serverInfo := fmt.Sprintf("*%s*", r.ServerInfo())
if 0 < len(r.Errors) {
return fmt.Sprintf("%s\n%s\n%s\n%s\nError: %s",
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\nError: %s",
notifyUsers,
serverInfo,
r.ScannedCves.FormatCveSummary(),
r.Packages.FormatUpdatablePacksSummary(),
r.ScannedCves.FormatFixedStatus(r.Packages),
r.FormatUpdatablePacksSummary(),
r.Errors)
}
return fmt.Sprintf("%s\n%s\n%s\n%s",
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s",
notifyUsers,
serverInfo,
r.ScannedCves.FormatCveSummary(),
r.Packages.FormatUpdatablePacksSummary())
r.ScannedCves.FormatFixedStatus(r.Packages),
r.FormatUpdatablePacksSummary())
}
func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
@@ -203,7 +205,7 @@ func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
curent = append(curent, affected.Name)
}
}
for _, n := range vinfo.CpeNames {
for _, n := range vinfo.CpeURIs {
curent = append(curent, n)
}
@@ -219,14 +221,14 @@ func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
new = append(new, "?")
}
}
for range vinfo.CpeNames {
for range vinfo.CpeURIs {
new = append(new, "?")
}
a := slack.Attachment{
Title: vinfo.CveID,
TitleLink: "https://nvd.nist.gov/vuln/detail/" + vinfo.CveID,
Text: attachmentText(vinfo, r.Family),
Text: attachmentText(vinfo, r.Family, r.CweDict, r.Packages),
MarkdownIn: []string{"text", "pretext"},
Fields: []slack.AttachmentField{
{
@@ -241,7 +243,7 @@ func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
Short: true,
},
},
Color: color(vinfo.MaxCvssScore().Value.Score),
Color: cvssColor(vinfo.MaxCvssScore().Value.Score),
}
attaches = append(attaches, a)
}
@@ -249,7 +251,7 @@ func toSlackAttachments(r models.ScanResult) (attaches []slack.Attachment) {
}
// https://api.slack.com/docs/attachments
func color(cvssScore float64) string {
func cvssColor(cvssScore float64) string {
switch {
case 7 <= cvssScore:
return "danger"
@@ -262,10 +264,15 @@ func color(cvssScore float64) string {
}
}
func attachmentText(vinfo models.VulnInfo, osFamily string) string {
func attachmentText(vinfo models.VulnInfo, osFamily string, cweDict map[string]models.CweDictEntry, packs models.Packages) string {
maxCvss := vinfo.MaxCvssScore()
vectors := []string{}
for _, cvss := range vinfo.Cvss2Scores() {
scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores(osFamily)...)
for _, cvss := range scores {
if cvss.Value.Severity == "" {
continue
}
calcURL := ""
switch cvss.Value.Type {
case models.CVSS2:
@@ -279,9 +286,10 @@ func attachmentText(vinfo models.VulnInfo, osFamily string) string {
}
if cont, ok := vinfo.CveContents[cvss.Type]; ok {
v := fmt.Sprintf("<%s|%s> (<%s|%s>)",
v := fmt.Sprintf("<%s|%s> %s (<%s|%s>)",
calcURL,
cvss.Value.Format(),
fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
cvss.Value.Severity,
cont.SourceLink,
cvss.Type)
vectors = append(vectors, v)
@@ -294,9 +302,10 @@ func attachmentText(vinfo models.VulnInfo, osFamily string) string {
v, k))
}
v := fmt.Sprintf("<%s|%s> (%s)",
v := fmt.Sprintf("<%s|%s> %s (%s)",
calcURL,
cvss.Value.Format(),
fmt.Sprintf("%3.1f/%s", cvss.Value.Score, cvss.Value.Vector),
cvss.Value.Severity,
strings.Join(links, ", "))
vectors = append(vectors, v)
}
@@ -308,27 +317,42 @@ func attachmentText(vinfo models.VulnInfo, osFamily string) string {
severity = "?"
}
return fmt.Sprintf("*%4.1f (%s)* %s\n%s\n```%s```",
nwvec := vinfo.AttackVector()
if nwvec == "Network" || nwvec == "remote" {
nwvec = fmt.Sprintf("*%s*", nwvec)
}
mitigation := ""
if vinfo.Mitigations(osFamily)[0].Type != models.Unknown {
mitigation = fmt.Sprintf("\nMitigation:\n```%s```\n",
vinfo.Mitigations(osFamily)[0].Value)
}
return fmt.Sprintf("*%4.1f (%s)* %s %s\n%s\n```\n%s\n```%s\n%s\n",
maxCvss.Value.Score,
severity,
cweIDs(vinfo, osFamily),
nwvec,
vinfo.PatchStatus(packs),
strings.Join(vectors, "\n"),
vinfo.Summaries(config.Conf.Lang, osFamily)[0].Value,
mitigation,
cweIDs(vinfo, osFamily, cweDict),
)
}
func cweIDs(vinfo models.VulnInfo, osFamily string) string {
func cweIDs(vinfo models.VulnInfo, osFamily string, cweDict models.CweDict) string {
links := []string{}
for _, cwe := range vinfo.CveContents.CweIDs(osFamily) {
if config.Conf.Lang == "ja" {
links = append(links, fmt.Sprintf("<%s|%s>",
cweJvnURL(cwe.Value), cwe.Value))
} else {
links = append(links, fmt.Sprintf("<%s|%s>",
cweURL(cwe.Value), cwe.Value))
for _, c := range vinfo.CveContents.UniqCweIDs(osFamily) {
name, url, top10Rank, top10URL := cweDict.Get(c.Value, osFamily)
line := ""
if top10Rank != "" {
line = fmt.Sprintf("<%s|[OWASP Top %s]>",
top10URL, top10Rank)
}
links = append(links, fmt.Sprintf("%s <%s|%s>: %s",
line, url, c.Value, name))
}
return strings.Join(links, " / ")
return strings.Join(links, "\n")
}
// See testcase

View File

@@ -44,9 +44,9 @@ func (w StdoutWriter) Write(rs ...models.ScanResult) error {
fmt.Print("\n")
}
if c.Conf.FormatShortText {
if c.Conf.FormatList {
for _, r := range rs {
fmt.Println(formatShortPlainText(r))
fmt.Println(formatList(r))
}
}

View File

@@ -76,7 +76,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() {
for _, cvss := range vinfo.Cvss2Scores(result.Family) {
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))
}
@@ -86,8 +86,9 @@ func (w SyslogWriter) encodeSyslog(result models.ScanResult) (messages []string)
kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v3="%s"`, cvss.Type, cvss.Value.Vector))
}
if content, ok := vinfo.CveContents[models.NVD]; ok {
kvPairs = append(kvPairs, fmt.Sprintf(`cwe_id="%s"`, content.CweID))
if content, ok := vinfo.CveContents[models.NvdXML]; ok {
cwes := strings.Join(content.CweIDs, ",")
kvPairs = append(kvPairs, fmt.Sprintf(`cwe_ids="%s"`, cwes))
if config.Conf.Syslog.Verbose {
kvPairs = append(kvPairs, fmt.Sprintf(`source_link="%s"`, content.SourceLink))
kvPairs = append(kvPairs, fmt.Sprintf(`summary="%s"`, content.Summary))

View File

@@ -33,10 +33,11 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
models.PackageStatus{Name: "pkg4"},
},
CveContents: models.CveContents{
models.NVD: models.CveContent{
Cvss2Score: 5.0,
Cvss2Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C",
CweID: "CWE-20",
models.NvdXML: models.CveContent{
Cvss2Score: 5.0,
Cvss2Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C",
Cvss2Severity: "MEDIUM",
CweIDs: []string{"CWE-20"},
},
},
},
@@ -44,7 +45,7 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
},
expectedMessages: []string{
`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="pkg1,pkg2" cve_id="CVE-2017-0001"`,
`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" cwe_id="CWE-20"`,
`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_nvdxml_v2="5.00" cvss_vector_nvdxml_v2="AV:L/AC:L/Au:N/C:N/I:N/A:C" cwe_ids="CWE-20"`,
},
},
{
@@ -63,7 +64,7 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
models.RedHat: models.CveContent{
Cvss3Score: 5.0,
Cvss3Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C",
CweID: "CWE-284",
CweIDs: []string{"CWE-284"},
Title: "RHSA-2017:0001: pkg5 security update (Important)",
},
},
@@ -103,7 +104,7 @@ func TestSyslogWriterEncodeSyslog(t *testing.T) {
for j, m := range messages {
e := tt.expectedMessages[j]
if e != m {
t.Errorf("test: %d, Messsage %d: expected %s, actual %s", i, j, e, m)
t.Errorf("test: %d, Messsage %d: \nexpected %s \nactual %s", i, j, e, m)
}
}
}

View File

@@ -32,7 +32,6 @@ import (
"github.com/google/subcommands"
"github.com/gosuri/uitable"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
var scanResults models.ScanResults
@@ -55,7 +54,7 @@ func RunTui(results models.ScanResults) subcommands.ExitStatus {
// g, err := gocui.NewGui(gocui.OutputNormal)
g := gocui.NewGui()
if err := g.Init(); err != nil {
log.Errorf("%s", err)
util.Log.Errorf("%s", err)
return subcommands.ExitFailure
}
defer g.Close()
@@ -63,7 +62,7 @@ func RunTui(results models.ScanResults) subcommands.ExitStatus {
g.SetLayout(layout)
// g.SetManagerFunc(layout)
if err := keybindings(g); err != nil {
log.Errorf("%s", err)
util.Log.Errorf("%s", err)
return subcommands.ExitFailure
}
g.SelBgColor = gocui.ColorGreen
@@ -72,7 +71,7 @@ func RunTui(results models.ScanResults) subcommands.ExitStatus {
if err := g.MainLoop(); err != nil {
g.Close()
log.Errorf("%s", err)
util.Log.Errorf("%s", err)
os.Exit(1)
}
return subcommands.ExitSuccess
@@ -468,10 +467,7 @@ func changeHost(g *gocui.Gui, v *gocui.View) error {
if err := setDetailLayout(g); err != nil {
return err
}
if err := setChangelogLayout(g); err != nil {
return err
}
return nil
return setChangelogLayout(g)
}
func redrawDetail(g *gocui.Gui) error {
@@ -479,10 +475,7 @@ func redrawDetail(g *gocui.Gui) error {
return err
}
if err := setDetailLayout(g); err != nil {
return err
}
return nil
return setDetailLayout(g)
}
func redrawChangelog(g *gocui.Gui) error {
@@ -490,10 +483,7 @@ func redrawChangelog(g *gocui.Gui) error {
return err
}
if err := setChangelogLayout(g); err != nil {
return err
}
return nil
return setChangelogLayout(g)
}
func getLine(g *gocui.Gui, v *gocui.View) error {
@@ -545,10 +535,7 @@ func delMsg(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView("msg"); err != nil {
return err
}
if err := g.SetCurrentView("summary"); err != nil {
return err
}
return nil
return g.SetCurrentView("summary")
}
func quit(g *gocui.Gui, v *gocui.View) error {
@@ -565,11 +552,7 @@ func layout(g *gocui.Gui) error {
if err := setDetailLayout(g); err != nil {
return err
}
if err := setChangelogLayout(g); err != nil {
return err
}
return nil
return setChangelogLayout(g)
}
func debug(g *gocui.Gui, str string) error {
@@ -615,7 +598,7 @@ func setSummaryLayout(g *gocui.Gui) error {
return err
}
lines := summaryLines()
lines := summaryLines(currentScanResult)
fmt.Fprintf(v, lines)
v.Highlight = true
@@ -625,37 +608,42 @@ func setSummaryLayout(g *gocui.Gui) error {
return nil
}
func summaryLines() string {
func summaryLines(r models.ScanResult) string {
stable := uitable.New()
stable.MaxColWidth = 1000
stable.Wrap = false
if len(currentScanResult.Errors) != 0 {
if len(r.Errors) != 0 {
return "Error: Scan with --debug to view the details"
}
indexFormat := ""
if len(currentScanResult.ScannedCves) < 10 {
if len(r.ScannedCves) < 10 {
indexFormat = "[%1d]"
} else if len(currentScanResult.ScannedCves) < 100 {
} else if len(r.ScannedCves) < 100 {
indexFormat = "[%2d]"
} else {
indexFormat = "[%3d]"
}
for i, vinfo := range vinfos {
summary := vinfo.Titles(
config.Conf.Lang, currentScanResult.Family)[0].Value
cvssScore := fmt.Sprintf("| %4.1f",
vinfo.MaxCvssScore().Value.Score)
for i, vinfo := range r.ScannedCves.ToSortedSlice() {
max := vinfo.MaxCvssScore().Value.Score
cvssScore := "| "
if 0 < max {
cvssScore = fmt.Sprintf("| %4.1f", max)
}
packname := vinfo.AffectedPackages.FormatTuiSummary()
packname += strings.Join(vinfo.CpeURIs, ", ")
var cols []string
cols = []string{
fmt.Sprintf(indexFormat, i+1),
vinfo.CveID,
cvssScore,
fmt.Sprintf("| %3d |", vinfo.Confidence.Score),
summary,
cvssScore + " |",
fmt.Sprintf("%8s |", vinfo.AttackVector()),
fmt.Sprintf("%7s |", vinfo.PatchStatus(r.Packages)),
packname,
}
icols := make([]interface{}, len(cols))
for j := range cols {
@@ -695,16 +683,12 @@ func setDetailLayout(g *gocui.Gui) error {
}
func setChangelogLayout(g *gocui.Gui) error {
maxX, maxY := g.Size()
summaryView, err := g.View("summary")
if err != nil {
return err
}
_, cy := summaryView.Cursor()
_, oy := summaryView.Origin()
currentVinfo = cy + oy
maxX, maxY := g.Size()
if v, err := g.SetView("changelog", int(float64(maxX)*0.5), int(float64(maxY)*0.2), maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
@@ -713,17 +697,56 @@ func setChangelogLayout(g *gocui.Gui) error {
return nil
}
lines := []string{}
lines := []string{
"Affected Packages, Processes",
"============================",
}
_, cy := summaryView.Cursor()
_, oy := summaryView.Origin()
currentVinfo = cy + oy
vinfo := vinfos[currentVinfo]
vinfo.AffectedPackages.Sort()
for _, affected := range vinfo.AffectedPackages {
// packages detected by OVAL may not be actually installed
if pack, ok := currentScanResult.Packages[affected.Name]; ok {
lines = append(lines,
"* "+pack.FormatVersionFromTo(
affected.NotFixedYet, affected.FixState))
if len(pack.AffectedProcs) != 0 {
for _, p := range pack.AffectedProcs {
lines = append(lines, fmt.Sprintf(" * PID: %s %s", p.PID, p.Name))
}
} else {
// lines = append(lines, fmt.Sprintf(" * No affected process"))
}
}
}
sort.Strings(vinfo.CpeURIs)
for _, uri := range vinfo.CpeURIs {
lines = append(lines, "* "+uri)
}
for _, adv := range vinfo.DistroAdvisories {
lines = append(lines, "\n",
"Advisories",
"==========",
)
lines = append(lines, adv.Format())
}
for _, affected := range vinfo.AffectedPackages {
pack := currentScanResult.Packages[affected.Name]
for _, p := range currentScanResult.Packages {
if pack.Name == p.Name {
lines = append(lines, p.FormatChangelog(), "\n")
if currentScanResult.IsDeepScanMode() {
lines = append(lines, "\n",
"ChangeLogs",
"==========",
)
for _, affected := range vinfo.AffectedPackages {
pack := currentScanResult.Packages[affected.Name]
for _, p := range currentScanResult.Packages {
if pack.Name == p.Name {
lines = append(lines, p.FormatChangelog(), "\n")
}
}
}
}
@@ -741,12 +764,13 @@ type dataForTmpl struct {
CveID string
Cvsses string
Summary string
Confidence models.Confidence
Cwes []models.CveContentStr
Mitigation string
Confidences models.Confidences
Cwes []models.CweDictEntry
Links []string
References []models.Reference
Packages []string
CpeNames []string
CpeURIs []string
PublishedDate time.Time
LastModifiedDate time.Time
}
@@ -767,20 +791,6 @@ func detailLines() (string, error) {
}
vinfo := vinfos[currentVinfo]
packsVer := []string{}
vinfo.AffectedPackages.Sort()
for _, affected := range vinfo.AffectedPackages {
// packages detected by OVAL may not be actually installed
if pack, ok := r.Packages[affected.Name]; ok {
packsVer = append(packsVer, pack.FormatVersionFromTo(affected.NotFixedYet))
}
}
sort.Strings(vinfo.CpeNames)
for _, name := range vinfo.CpeNames {
packsVer = append(packsVer, name)
}
links := []string{vinfo.CveContents.SourceLinks(
config.Conf.Lang, r.Family, vinfo.CveID)[0].Value,
vinfo.Cvss2CalcURL(),
@@ -792,35 +802,57 @@ func detailLines() (string, error) {
refs := []models.Reference{}
for _, rr := range vinfo.CveContents.References(r.Family) {
for _, ref := range rr.Value {
if ref.Source == "" {
ref.Source = "-"
}
refs = append(refs, ref)
}
}
summary := vinfo.Summaries(r.Lang, r.Family)[0]
mitigation := vinfo.Mitigations(r.Family)[0]
table := uitable.New()
table.MaxColWidth = maxColWidth
table.Wrap = true
scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)
scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores(r.Family)...)
var cols []interface{}
for _, score := range scores {
if score.Value.Score == 0 && score.Value.Severity == "" {
continue
}
scoreStr := "-"
if 0 < score.Value.Score {
scoreStr = fmt.Sprintf("%3.1f", score.Value.Score)
}
scoreVec := fmt.Sprintf("%s/%s", scoreStr, score.Value.Vector)
cols = []interface{}{
scoreVec,
score.Value.Severity,
score.Value.Format(),
score.Type,
}
table.AddRow(cols...)
}
uniqCweIDs := vinfo.CveContents.UniqCweIDs(r.Family)
cwes := []models.CweDictEntry{}
for _, cweID := range uniqCweIDs {
if strings.HasPrefix(cweID.Value, "CWE-") {
if dict, ok := r.CweDict[strings.TrimPrefix(cweID.Value, "CWE-")]; ok {
cwes = append(cwes, dict)
}
}
}
data := dataForTmpl{
CveID: vinfo.CveID,
Cvsses: fmt.Sprintf("%s\n", table),
Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type),
Confidence: vinfo.Confidence,
Cwes: vinfo.CveContents.CweIDs(r.Family),
Links: util.Distinct(links),
Packages: packsVer,
References: refs,
CveID: vinfo.CveID,
Cvsses: fmt.Sprintf("%s\n", table),
Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type),
Mitigation: fmt.Sprintf("%s (%s)", mitigation.Value, mitigation.Type),
Confidences: vinfo.Confidences,
Cwes: cwes,
Links: util.Distinct(links),
References: refs,
}
buf := bytes.NewBuffer(nil) // create empty buffer
@@ -833,47 +865,41 @@ func detailLines() (string, error) {
const mdTemplate = `
{{.CveID}}
==============
================
CVSS Scores
--------------
-----------
{{.Cvsses }}
Summary
--------------
-----------
{{.Summary }}
Mitigation
-----------
{{.Mitigation }}
Links
--------------
-----------
{{range $link := .Links -}}
* {{$link}}
{{end}}
CWE
--------------
-----------
{{range .Cwes -}}
* {{.Value}} ({{.Type}})
* {{.En.CweID}} [{{.En.Name}}](https://cwe.mitre.org/data/definitions/{{.En.CweID}}.html)
{{end}}
Package/CPE
--------------
{{range $pack := .Packages -}}
* {{$pack}}
{{end -}}
{{range $name := .CpeNames -}}
{{range $name := .CpeURIs -}}
* {{$name}}
{{end}}
Confidence
--------------
{{.Confidence }}
-----------
{{range $confidence := .Confidences -}}
* {{$confidence.DetectionMethod}}
{{end}}
References
--------------
-----------
{{range .References -}}
* [{{.Source}}]( {{.Link}} )
* [{{.Source}}]({{.Link}})
{{end}}
`

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package report
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
@@ -33,9 +34,10 @@ import (
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
"github.com/gosuri/uitable"
"github.com/olekukonko/tablewriter"
)
const maxColWidth = 80
const maxColWidth = 100
func formatScanSummary(rs ...models.ScanResult) string {
table := uitable.New()
@@ -47,7 +49,7 @@ func formatScanSummary(rs ...models.ScanResult) string {
cols = []interface{}{
r.FormatServerName(),
fmt.Sprintf("%s%s", r.Family, r.Release),
r.Packages.FormatUpdatablePacksSummary(),
r.FormatUpdatablePacksSummary(),
}
} else {
cols = []interface{}{
@@ -72,7 +74,8 @@ func formatOneLineSummary(rs ...models.ScanResult) string {
cols = []interface{}{
r.FormatServerName(),
r.ScannedCves.FormatCveSummary(),
r.Packages.FormatUpdatablePacksSummary(),
r.ScannedCves.FormatFixedStatus(r.Packages),
r.FormatUpdatablePacksSummary(),
}
} else {
cols = []interface{}{
@@ -86,7 +89,7 @@ func formatOneLineSummary(rs ...models.ScanResult) string {
return fmt.Sprintf("%s\n", table)
}
func formatShortPlainText(r models.ScanResult) string {
func formatList(r models.ScanResult) string {
header := r.FormatTextReportHeadedr()
if len(r.Errors) != 0 {
return fmt.Sprintf(
@@ -99,63 +102,49 @@ func formatShortPlainText(r models.ScanResult) string {
%s
No CVE-IDs are found in updatable packages.
%s
`, header, r.Packages.FormatUpdatablePacksSummary())
`, header, r.FormatUpdatablePacksSummary())
}
stable := uitable.New()
stable.MaxColWidth = maxColWidth
stable.Wrap = true
for _, vuln := range r.ScannedCves.ToSortedSlice() {
summaries := vuln.Summaries(config.Conf.Lang, r.Family)
links := vuln.CveContents.SourceLinks(
config.Conf.Lang, r.Family, vuln.CveID)
data := [][]string{}
for _, vinfo := range r.ScannedCves.ToSortedSlice() {
max := vinfo.MaxCvssScore().Value.Score
// v2max := vinfo.MaxCvss2Score().Value.Score
// v3max := vinfo.MaxCvss3Score().Value.Score
vlinks := []string{}
for name, url := range vuln.VendorLinks(r.Family) {
vlinks = append(vlinks, fmt.Sprintf("%s (%s)", url, name))
}
// packname := vinfo.AffectedPackages.FormatTuiSummary()
// packname += strings.Join(vinfo.CpeURIs, ", ")
cvsses := ""
for _, cvss := range vuln.Cvss2Scores() {
cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type)
}
cvsses += vuln.Cvss2CalcURL() + "\n"
for _, cvss := range vuln.Cvss3Scores() {
cvsses += fmt.Sprintf("%s (%s)\n", cvss.Value.Format(), cvss.Type)
}
if 0 < len(vuln.Cvss3Scores()) {
cvsses += vuln.Cvss3CalcURL() + "\n"
}
maxCvss := vuln.FormatMaxCvssScore()
rightCol := fmt.Sprintf(`%s
%s
---
%s
%s
%sConfidence: %v`,
maxCvss,
summaries[0].Value,
links[0].Value,
strings.Join(vlinks, "\n"),
cvsses,
// packsVer,
vuln.Confidence,
)
leftCol := fmt.Sprintf("%s", vuln.CveID)
scols := []string{leftCol, rightCol}
cols := make([]interface{}, len(scols))
for i := range cols {
cols[i] = scols[i]
}
stable.AddRow(cols...)
stable.AddRow("")
data = append(data, []string{
vinfo.CveID,
fmt.Sprintf("%4.1f", max),
// fmt.Sprintf("%4.1f", v2max),
// fmt.Sprintf("%4.1f", v3max),
fmt.Sprintf("%8s", vinfo.AttackVector()),
fmt.Sprintf("%7s", vinfo.PatchStatus(r.Packages)),
// packname,
fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vinfo.CveID),
})
}
return fmt.Sprintf("%s\n%s\n", header, stable)
b := bytes.Buffer{}
table := tablewriter.NewWriter(&b)
table.SetHeader([]string{
"CVE-ID",
"CVSS",
// "v3",
// "v2",
"Attack",
"Fixed",
// "Pkg",
"NVD",
})
table.SetBorder(true)
table.AppendBulk(data)
table.Render()
return fmt.Sprintf("%s\n%s", header, b.String())
}
func formatFullPlainText(r models.ScanResult) string {
func formatFullPlainText(r models.ScanResult) (lines string) {
header := r.FormatTextReportHeadedr()
if len(r.Errors) != 0 {
return fmt.Sprintf(
@@ -168,62 +157,117 @@ func formatFullPlainText(r models.ScanResult) string {
%s
No CVE-IDs are found in updatable packages.
%s
`, header, r.Packages.FormatUpdatablePacksSummary())
`, header, r.FormatUpdatablePacksSummary())
}
table := uitable.New()
table.MaxColWidth = maxColWidth
table.Wrap = true
lines = header + "\n"
for _, vuln := range r.ScannedCves.ToSortedSlice() {
table.AddRow(vuln.CveID)
table.AddRow("----------------")
table.AddRow("Max Score", vuln.FormatMaxCvssScore())
for _, cvss := range vuln.Cvss2Scores() {
table.AddRow(cvss.Type, cvss.Value.Format())
}
data := [][]string{}
data = append(data, []string{"Max Score", vuln.FormatMaxCvssScore()})
for _, cvss := range vuln.Cvss3Scores() {
table.AddRow(cvss.Type, cvss.Value.Format())
}
if 0 < len(vuln.Cvss2Scores()) {
table.AddRow("CVSSv2 Calc", vuln.Cvss2CalcURL())
}
if 0 < len(vuln.Cvss3Scores()) {
table.AddRow("CVSSv3 Calc", vuln.Cvss3CalcURL())
}
table.AddRow("Summary", vuln.Summaries(
config.Conf.Lang, r.Family)[0].Value)
links := vuln.CveContents.SourceLinks(
config.Conf.Lang, r.Family, vuln.CveID)
table.AddRow("Source", links[0].Value)
vlinks := vuln.VendorLinks(r.Family)
for name, url := range vlinks {
table.AddRow(name, url)
if cvssstr := cvss.Value.Format(); cvssstr != "" {
data = append(data, []string{string(cvss.Type), cvssstr})
}
}
for _, v := range vuln.CveContents.CweIDs(r.Family) {
table.AddRow(fmt.Sprintf("%s (%s)", v.Value, v.Type), cweURL(v.Value))
for _, cvss := range vuln.Cvss2Scores(r.Family) {
if cvssstr := cvss.Value.Format(); cvssstr != "" {
data = append(data, []string{string(cvss.Type), cvssstr})
}
}
data = append(data, []string{"Summary", vuln.Summaries(
config.Conf.Lang, r.Family)[0].Value})
mitigation := vuln.Mitigations(r.Family)[0]
if mitigation.Type != models.Unknown {
data = append(data, []string{"Mitigation", mitigation.Value})
}
cweURLs, top10URLs := []string{}, []string{}
for _, v := range vuln.CveContents.UniqCweIDs(r.Family) {
name, url, top10Rank, top10URL := r.CweDict.Get(v.Value, r.Lang)
if top10Rank != "" {
data = append(data, []string{"CWE",
fmt.Sprintf("[OWASP Top%s] %s: %s (%s)",
top10Rank, v.Value, name, v.Type)})
top10URLs = append(top10URLs, top10URL)
} else {
data = append(data, []string{"CWE", fmt.Sprintf("%s: %s (%s)",
v.Value, name, v.Type)})
}
cweURLs = append(cweURLs, url)
}
packsVer := []string{}
vuln.AffectedPackages.Sort()
for _, affected := range vuln.AffectedPackages {
if pack, ok := r.Packages[affected.Name]; ok {
packsVer = append(packsVer, pack.FormatVersionFromTo(affected.NotFixedYet))
data = append(data, []string{"Affected PKG",
pack.FormatVersionFromTo(affected.NotFixedYet, affected.FixState)})
if len(pack.AffectedProcs) != 0 {
for _, p := range pack.AffectedProcs {
data = append(data, []string{"",
fmt.Sprintf(" - PID: %s %s", p.PID, p.Name)})
}
}
}
}
sort.Strings(vuln.CpeNames)
for _, name := range vuln.CpeNames {
packsVer = append(packsVer, name)
sort.Strings(vuln.CpeURIs)
for _, name := range vuln.CpeURIs {
data = append(data, []string{"CPE", name})
}
table.AddRow("Package/CPE", strings.Join(packsVer, "\n"))
table.AddRow("Confidence", vuln.Confidence)
table.AddRow("\n")
for _, confidence := range vuln.Confidences {
data = append(data, []string{"Confidence", confidence.String()})
}
links := vuln.CveContents.SourceLinks(
config.Conf.Lang, r.Family, vuln.CveID)
data = append(data, []string{"Source", links[0].Value})
if 0 < len(vuln.Cvss2Scores(r.Family)) {
data = append(data, []string{"CVSSv2 Calc", vuln.Cvss2CalcURL()})
}
if 0 < len(vuln.Cvss3Scores()) {
data = append(data, []string{"CVSSv3 Calc", vuln.Cvss3CalcURL()})
}
vlinks := vuln.VendorLinks(r.Family)
for name, url := range vlinks {
data = append(data, []string{name, url})
}
for _, url := range cweURLs {
data = append(data, []string{"CWE", url})
}
for _, url := range top10URLs {
data = append(data, []string{"OWASP Top10", url})
}
// for _, rr := range vuln.CveContents.References(r.Family) {
// for _, ref := range rr.Value {
// data = append(data, []string{ref.Source, ref.Link})
// }
// }
b := bytes.Buffer{}
table := tablewriter.NewWriter(&b)
table.SetColWidth(80)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetHeader([]string{
vuln.CveID,
"",
})
table.SetBorder(true)
table.SetHeaderColor(
tablewriter.Colors{tablewriter.Normal},
tablewriter.Colors{tablewriter.Normal},
)
table.AppendBulk(data)
table.Render()
lines += b.String() + "\n"
}
return fmt.Sprintf("%s\n%s", header, table)
return
}
func cweURL(cweID string) string {
@@ -387,8 +431,8 @@ func isCveFixed(current models.VulnInfo, previous models.ScanResult) bool {
func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool {
cTypes := []models.CveContentType{
models.NVD,
models.JVN,
models.NvdXML,
models.Jvn,
models.NewCveContentType(current.Family),
}

View File

@@ -42,7 +42,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2017-0001",
LastModified: time.Time{},
},
@@ -56,7 +56,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0001",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2017-0001",
LastModified: time.Time{},
},
@@ -77,7 +77,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.JVN,
Type: models.Jvn,
CveID: "CVE-2017-0002",
LastModified: old,
},
@@ -91,7 +91,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0002",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.JVN,
Type: models.Jvn,
CveID: "CVE-2017-0002",
LastModified: old,
},
@@ -113,7 +113,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2017-0002",
LastModified: new,
},
@@ -128,7 +128,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0003",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2017-0002",
LastModified: old,
},
@@ -150,7 +150,7 @@ func TestIsCveInfoUpdated(t *testing.T) {
CveID: "CVE-2017-0004",
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2017-0002",
LastModified: old,
},
@@ -194,18 +194,18 @@ func TestDiff(t *testing.T) {
CveID: "CVE-2012-6702",
AffectedPackages: models.PackageStatuses{{Name: "libexpat1"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
"CVE-2014-9761": {
CveID: "CVE-2014-9761",
AffectedPackages: models.PackageStatuses{{Name: "libc-bin"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
},
Packages: models.Packages{},
Errors: []string{},
Optional: [][]interface{}{},
Optional: map[string]interface{}{},
},
},
inPrevious: models.ScanResults{
@@ -219,18 +219,18 @@ func TestDiff(t *testing.T) {
CveID: "CVE-2012-6702",
AffectedPackages: models.PackageStatuses{{Name: "libexpat1"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
"CVE-2014-9761": {
CveID: "CVE-2014-9761",
AffectedPackages: models.PackageStatuses{{Name: "libc-bin"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
},
Packages: models.Packages{},
Errors: []string{},
Optional: [][]interface{}{},
Optional: map[string]interface{}{},
},
},
out: models.ScanResult{
@@ -241,7 +241,7 @@ func TestDiff(t *testing.T) {
Packages: models.Packages{},
ScannedCves: models.VulnInfos{},
Errors: []string{},
Optional: [][]interface{}{},
Optional: map[string]interface{}{},
},
},
{
@@ -256,7 +256,7 @@ func TestDiff(t *testing.T) {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageStatuses{{Name: "mysql-libs"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
},
Packages: models.Packages{
@@ -294,7 +294,7 @@ func TestDiff(t *testing.T) {
CveID: "CVE-2016-6662",
AffectedPackages: models.PackageStatuses{{Name: "mysql-libs"}},
DistroAdvisories: []models.DistroAdvisory{},
CpeNames: []string{},
CpeURIs: []string{},
},
},
Packages: models.Packages{
@@ -356,7 +356,7 @@ func TestIsCveFixed(t *testing.T) {
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
@@ -374,7 +374,7 @@ func TestIsCveFixed(t *testing.T) {
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
@@ -397,7 +397,7 @@ func TestIsCveFixed(t *testing.T) {
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},
@@ -415,7 +415,7 @@ func TestIsCveFixed(t *testing.T) {
},
CveContents: models.NewCveContents(
models.CveContent{
Type: models.NVD,
Type: models.NvdXML,
CveID: "CVE-2016-6662",
LastModified: time.Time{},
},

View File

@@ -64,7 +64,14 @@ func detectAlpine(c config.ServerInfo) (itsMe bool, os osTypeInterface) {
return false, os
}
func (o *alpine) checkDependencies() error {
func (o *alpine) checkScanMode() error {
if o.getServerInfo().Mode.IsOffline() {
return fmt.Errorf("Remove offline scan mode, Alpine needs internet connection")
}
return nil
}
func (o *alpine) checkDeps() error {
o.log.Infof("Dependencies... No need")
return nil
}
@@ -83,6 +90,7 @@ func (o *alpine) apkUpdate() error {
}
func (o *alpine) preCure() error {
o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
if err := o.detectIPAddr(); err != nil {
o.log.Debugf("Failed to detect IP addresses: %s", err)
}
@@ -140,6 +148,11 @@ func (o *alpine) scanInstalledPackages() (models.Packages, error) {
return o.parseApkInfo(r.Stdout)
}
func (o *alpine) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
installedPackages, err := o.parseApkInfo(stdout)
return installedPackages, nil, err
}
func (o *alpine) parseApkInfo(stdout string) (models.Packages, error) {
packs := models.Packages{}
scanner := bufio.NewScanner(strings.NewReader(stdout))

114
scan/amazon.go Normal file
View File

@@ -0,0 +1,114 @@
package scan
import (
"fmt"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// inherit OsTypeInterface
type amazon struct {
redhatBase
}
// NewAmazon is constructor
func newAmazon(c config.ServerInfo) *amazon {
r := &amazon{
redhatBase{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
VulnInfos: models.VulnInfos{},
},
},
sudo: rootPrivAmazon{},
},
}
r.log = util.NewCustomLogger(c)
r.setServerInfo(c)
return r
}
func (o *amazon) checkScanMode() error {
if o.getServerInfo().Mode.IsOffline() {
return fmt.Errorf("Remove offline scan mode, Amazon needs internet connection")
}
return nil
}
func (o *amazon) checkDeps() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckDeps(o.depsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckDeps(o.depsFastRoot())
} else if o.getServerInfo().Mode.IsDeep() {
return o.execCheckDeps(o.depsDeep())
}
return fmt.Errorf("Unknown scan mode")
}
func (o *amazon) depsFast() []string {
if o.getServerInfo().Mode.IsOffline() {
return []string{}
}
// repoquery
return []string{"yum-utils"}
}
func (o *amazon) depsFastRoot() []string {
return []string{
"yum-utils",
"yum-plugin-ps",
}
}
func (o *amazon) depsDeep() []string {
return o.depsFastRoot()
}
func (o *amazon) checkIfSudoNoPasswd() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFastRoot())
} else {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsDeep())
}
}
func (o *amazon) sudoNoPasswdCmdsFast() []cmd {
return []cmd{}
}
func (o *amazon) sudoNoPasswdCmdsFastRoot() []cmd {
return []cmd{
{"yum -q ps all --color=never", exitStatusZero},
{"stat /proc/1/exe", exitStatusZero},
{"needs-restarting", exitStatusZero},
{"which which", exitStatusZero},
}
}
func (o *amazon) sudoNoPasswdCmdsDeep() []cmd {
return o.sudoNoPasswdCmdsFastRoot()
}
type rootPrivAmazon struct{}
func (o rootPrivAmazon) repoquery() bool {
return false
}
func (o rootPrivAmazon) yumRepolist() bool {
return false
}
func (o rootPrivAmazon) yumUpdateInfo() bool {
return false
}
func (o rootPrivAmazon) yumChangelog() bool {
return false
}

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package scan
import (
"bufio"
"fmt"
"net"
"regexp"
@@ -33,7 +34,6 @@ type base struct {
ServerInfo config.ServerInfo
Distro config.Distro
Platform models.Platform
osPackages
log *logrus.Entry
@@ -98,7 +98,7 @@ func (l *base) runningKernel() (release, version string, err error) {
}
func (l *base) allContainers() (containers []config.Container, err error) {
switch l.ServerInfo.Containers.Type {
switch l.ServerInfo.ContainerType {
case "", "docker":
stdout, err := l.dockerPs("-a --format '{{.ID}} {{.Names}} {{.Image}}'")
if err != nil {
@@ -119,12 +119,12 @@ func (l *base) allContainers() (containers []config.Container, err error) {
return l.parseLxcPs(stdout)
default:
return containers, fmt.Errorf(
"Not supported yet: %s", l.ServerInfo.Containers.Type)
"Not supported yet: %s", l.ServerInfo.ContainerType)
}
}
func (l *base) runningContainers() (containers []config.Container, err error) {
switch l.ServerInfo.Containers.Type {
switch l.ServerInfo.ContainerType {
case "", "docker":
stdout, err := l.dockerPs("--format '{{.ID}} {{.Names}} {{.Image}}'")
if err != nil {
@@ -145,12 +145,12 @@ func (l *base) runningContainers() (containers []config.Container, err error) {
return l.parseLxcPs(stdout)
default:
return containers, fmt.Errorf(
"Not supported yet: %s", l.ServerInfo.Containers.Type)
"Not supported yet: %s", l.ServerInfo.ContainerType)
}
}
func (l *base) exitedContainers() (containers []config.Container, err error) {
switch l.ServerInfo.Containers.Type {
switch l.ServerInfo.ContainerType {
case "", "docker":
stdout, err := l.dockerPs("--filter 'status=exited' --format '{{.ID}} {{.Names}} {{.Image}}'")
if err != nil {
@@ -171,7 +171,7 @@ func (l *base) exitedContainers() (containers []config.Container, err error) {
return l.parseLxcPs(stdout)
default:
return containers, fmt.Errorf(
"Not supported yet: %s", l.ServerInfo.Containers.Type)
"Not supported yet: %s", l.ServerInfo.ContainerType)
}
}
@@ -296,6 +296,10 @@ func (l *base) parseIP(stdout string) (ipv4Addrs []string, ipv6Addrs []string) {
}
func (l *base) detectPlatform() {
if l.getServerInfo().Mode.IsOffline() {
l.setPlatform(models.Platform{Name: "unknown"})
return
}
ok, instanceID, err := l.detectRunningOnAws()
if err != nil {
l.setPlatform(models.Platform{Name: "other"})
@@ -316,7 +320,7 @@ func (l *base) detectPlatform() {
func (l *base) detectRunningOnAws() (ok bool, instanceID string, err error) {
if r := l.exec("type curl", noSudo); r.isSuccess() {
cmd := "curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id"
cmd := "curl --max-time 1 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id"
r := l.exec(cmd, noSudo)
if r.isSuccess() {
id := strings.TrimSpace(r.Stdout)
@@ -367,7 +371,7 @@ func (l *base) isAwsInstanceID(str string) bool {
}
func (l *base) convertToModel() models.ScanResult {
ctype := l.ServerInfo.Containers.Type
ctype := l.ServerInfo.ContainerType
if l.ServerInfo.Container.ContainerID != "" && ctype == "" {
ctype = "docker"
}
@@ -409,3 +413,62 @@ func (l *base) setErrs(errs []error) {
func (l *base) getErrs() []error {
return l.errs
}
const (
systemd = "systemd"
upstart = "upstart"
sysVinit = "init"
)
// https://unix.stackexchange.com/questions/196166/how-to-find-out-if-a-system-uses-sysv-upstart-or-systemd-initsystem
func (l *base) detectInitSystem() (string, error) {
var f func(string) (string, error)
f = func(cmd string) (string, error) {
r := l.exec(cmd, sudo)
if !r.isSuccess() {
return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
}
scanner := bufio.NewScanner(strings.NewReader(r.Stdout))
scanner.Scan()
line := strings.TrimSpace(scanner.Text())
if strings.Contains(line, "systemd") {
return systemd, nil
} else if strings.Contains(line, "upstart") {
return upstart, nil
} else if strings.Contains(line, "File: /proc/1/exe -> /sbin/init") ||
strings.Contains(line, "File: `/proc/1/exe' -> `/sbin/init'") {
return f("stat /sbin/init")
} else if line == "File: /sbin/init" ||
line == "File: `/sbin/init'" {
r := l.exec("/sbin/init --version", noSudo)
if r.isSuccess() {
if strings.Contains(r.Stdout, "upstart") {
return upstart, nil
}
}
return sysVinit, nil
}
return "", fmt.Errorf("Failed to detect a init system: %s", line)
}
return f("stat /proc/1/exe")
}
func (l *base) detectServiceName(pid string) (string, error) {
cmd := fmt.Sprintf("systemctl status --quiet --no-pager %s", pid)
r := l.exec(cmd, noSudo)
if !r.isSuccess() {
return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
}
return l.parseSystemctlStatus(r.Stdout), nil
}
func (l *base) parseSystemctlStatus(stdout string) string {
scanner := bufio.NewScanner(strings.NewReader(stdout))
scanner.Scan()
line := scanner.Text()
ss := strings.Fields(line)
if len(ss) < 2 || strings.HasPrefix(line, "Failed to get unit for PID") {
return ""
}
return ss[1]
}

View File

@@ -25,7 +25,6 @@ import (
)
func TestParseDockerPs(t *testing.T) {
var test = struct {
in string
expected []config.Container
@@ -46,7 +45,7 @@ f570ae647edc agitated_lovelace centos:latest`,
},
}
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
actual, err := r.parseDockerPs(test.in)
if err != nil {
t.Errorf("Error occurred. in: %s, err: %s", test.in, err)
@@ -60,7 +59,6 @@ f570ae647edc agitated_lovelace centos:latest`,
}
func TestParseLxdPs(t *testing.T) {
var test = struct {
in string
expected []config.Container
@@ -84,7 +82,7 @@ func TestParseLxdPs(t *testing.T) {
},
}
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
actual, err := r.parseLxdPs(test.in)
if err != nil {
t.Errorf("Error occurred. in: %s, err: %s", test.in, err)
@@ -117,7 +115,7 @@ func TestParseIp(t *testing.T) {
expected6: []string{"2001:db8::68"},
}
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
actual4, actual6 := r.parseIP(test.in)
if !reflect.DeepEqual(test.expected4, actual4) {
t.Errorf("expected %v, actual %v", test.expected4, actual4)
@@ -140,7 +138,7 @@ func TestIsAwsInstanceID(t *testing.T) {
{"no data", false},
}
r := newRedhat(config.ServerInfo{})
r := newAmazon(config.ServerInfo{})
for _, tt := range tests {
actual := r.isAwsInstanceID(tt.in)
if tt.expected != actual {
@@ -148,3 +146,35 @@ func TestIsAwsInstanceID(t *testing.T) {
}
}
}
func TestParseSystemctlStatus(t *testing.T) {
var tests = []struct {
in string
out string
}{
{
in: `● NetworkManager.service - Network Manager
Loaded: loaded (/usr/lib/systemd/system/NetworkManager.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2018-01-10 17:15:39 JST; 2 months 10 days ago
Docs: man:NetworkManager(8)
Main PID: 437 (NetworkManager)
Memory: 424.0K
CGroup: /system.slice/NetworkManager.service
├─437 /usr/sbin/NetworkManager --no-daemon
└─572 /sbin/dhclient -d -q -sf /usr/libexec/nm-dhcp-helper -pf /var/run/dhclient-ens160.pid -lf /var/lib/NetworkManager/dhclient-241ed966-e1c7-4d5c-a6a0-8a6dba457277-ens160.lease -cf /var/lib/NetworkManager/dhclient-ens160.conf ens160`,
out: "NetworkManager.service",
},
{
in: `Failed to get unit for PID 700: PID 700 does not belong to any loaded unit.`,
out: "",
},
}
r := newCentOS(config.ServerInfo{})
for _, tt := range tests {
actual := r.parseSystemctlStatus(tt.in)
if tt.out != actual {
t.Errorf("expected %v, actual %v", tt.out, actual)
}
}
}

120
scan/centos.go Normal file
View File

@@ -0,0 +1,120 @@
package scan
import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// inherit OsTypeInterface
type centos struct {
redhatBase
}
// NewAmazon is constructor
func newCentOS(c config.ServerInfo) *centos {
r := &centos{
redhatBase{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
VulnInfos: models.VulnInfos{},
},
},
sudo: rootPrivCentos{},
},
}
r.log = util.NewCustomLogger(c)
r.setServerInfo(c)
return r
}
func (o *centos) checkScanMode() error {
return nil
}
func (o *centos) checkDeps() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckDeps(o.depsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckDeps(o.depsFastRoot())
} else {
return o.execCheckDeps(o.depsDeep())
}
}
func (o *centos) depsFast() []string {
if o.getServerInfo().Mode.IsOffline() {
return []string{}
}
// repoquery
return []string{"yum-utils"}
}
func (o *centos) depsFastRoot() []string {
return []string{
"yum-utils",
"yum-plugin-ps",
}
}
func (o *centos) depsDeep() []string {
return []string{
"yum-utils",
"yum-plugin-ps",
"yum-plugin-changelog",
}
}
func (o *centos) checkIfSudoNoPasswd() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFastRoot())
} else {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsDeep())
}
}
func (o *centos) sudoNoPasswdCmdsFast() []cmd {
return []cmd{}
}
func (o *centos) sudoNoPasswdCmdsFastRoot() []cmd {
if o.getServerInfo().Mode.IsOffline() {
// yum ps needs internet connection
return []cmd{
{"stat /proc/1/exe", exitStatusZero},
{"needs-restarting", exitStatusZero},
{"which which", exitStatusZero},
}
}
return []cmd{
{"yum -q ps all --color=never", exitStatusZero},
{"stat /proc/1/exe", exitStatusZero},
{"needs-restarting", exitStatusZero},
{"which which", exitStatusZero},
}
}
func (o *centos) sudoNoPasswdCmdsDeep() []cmd {
return o.sudoNoPasswdCmdsFastRoot()
}
type rootPrivCentos struct{}
func (o rootPrivCentos) repoquery() bool {
return false
}
func (o rootPrivCentos) yumRepolist() bool {
return false
}
func (o rootPrivCentos) yumUpdateInfo() bool {
return false
}
func (o rootPrivCentos) yumChangelog() bool {
return false
}

View File

@@ -135,20 +135,47 @@ func trim(str string) string {
return strings.TrimSpace(str)
}
func (o *debian) checkScanMode() error {
return nil
}
func (o *debian) checkIfSudoNoPasswd() error {
if config.Conf.Deep || o.Distro.Family == config.Raspbian {
cmd := util.PrependProxyEnv("apt-get update")
if o.getServerInfo().Mode.IsFast() {
o.log.Infof("sudo ... No need")
return nil
}
cmds := []string{
"checkrestart",
"stat /proc/1/exe",
}
if !o.getServerInfo().Mode.IsOffline() {
cmds = append(cmds, "apt-get update")
}
for _, cmd := range cmds {
cmd = util.PrependProxyEnv(cmd)
o.log.Infof("Checking... sudo %s", cmd)
r := o.exec(cmd, sudo)
if !r.isSuccess() {
o.log.Errorf("sudo error on %s", r)
return fmt.Errorf("Failed to sudo: %s", r)
}
o.log.Infof("Sudo... Pass")
return nil
}
o.log.Infof("sudo ... No need")
initName, err := o.detectInitSystem()
if initName == upstart && err == nil {
cmd := util.PrependProxyEnv("initctl status --help")
o.log.Infof("Checking... sudo %s", cmd)
r := o.exec(cmd, sudo)
if !r.isSuccess() {
o.log.Errorf("sudo error on %s", r)
return fmt.Errorf("Failed to sudo: %s", r)
}
}
o.log.Infof("Sudo... Pass")
return nil
}
@@ -159,43 +186,57 @@ type dep struct {
additionalMsg string
}
func (o *debian) checkDependencies() error {
func (o *debian) checkDeps() error {
deps := []dep{}
if o.getServerInfo().Mode.IsDeep() || o.getServerInfo().Mode.IsFastRoot() {
// checkrestart
deps = append(deps, dep{
packName: "debian-goodies",
required: true,
logFunc: o.log.Errorf,
})
}
switch o.Distro.Family {
case config.Ubuntu, config.Raspbian:
o.log.Infof("Dependencies... No need")
return nil
case config.Debian:
if o.Distro.Family == config.Debian {
// https://askubuntu.com/a/742844
if !o.ServerInfo.IsContainer() {
deps = append(deps, dep{
packName: "reboot-notifier",
required: false,
logFunc: o.log.Warnf,
additionalMsg: ". If you want to detect whether not rebooted after kernel update.",
additionalMsg: ". Install it if you want to detect whether not rebooted after kernel update. To install `reboot-notifier` on Debian, see https://feeding.cloud.geek.nz/posts/introducing-reboot-notifier/",
})
}
if config.Conf.Deep {
// Changelogs will be fetched only in deep scan mode
if o.getServerInfo().Mode.IsDeep() {
// Debian needs aptitude to get changelogs.
// Because unable to get changelogs via apt-get changelog on Debian.
// Because unable to get changelogs via `apt-get changelog` on Debian.
deps = append(deps, dep{
packName: "aptitude",
required: true,
logFunc: o.log.Errorf,
})
}
default:
return fmt.Errorf("Not implemented yet: %s", o.Distro)
}
for _, dep := range deps {
cmd := "dpkg-query -W " + dep.packName
if r := o.exec(cmd, noSudo); !r.isSuccess() {
msg := fmt.Sprintf("%s is not installed", dep.packName)
cmd := fmt.Sprintf("%s %s", dpkgQuery, dep.packName)
msg := fmt.Sprintf("%s is not installed", dep.packName)
r := o.exec(cmd, noSudo)
if !r.isSuccess() {
if dep.additionalMsg != "" {
msg += dep.additionalMsg
}
dep.logFunc(msg)
if dep.required {
return fmt.Errorf(msg)
}
continue
}
_, status, _, _, _, _ := o.parseScannedPackagesLine(r.Stdout)
if status != "ii" {
if dep.additionalMsg != "" {
msg += dep.additionalMsg
}
@@ -204,12 +245,14 @@ func (o *debian) checkDependencies() error {
return fmt.Errorf(msg)
}
}
}
o.log.Infof("Dependencies... Pass")
return nil
}
func (o *debian) preCure() error {
o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
if err := o.detectIPAddr(); err != nil {
o.log.Debugf("Failed to detect IP addresses: %s", err)
}
@@ -218,6 +261,9 @@ func (o *debian) preCure() error {
}
func (o *debian) postScan() error {
if o.getServerInfo().Mode.IsDeep() || o.getServerInfo().Mode.IsFastRoot() {
return o.checkrestart()
}
return nil
}
@@ -252,11 +298,11 @@ func (o *debian) scanPackages() error {
o.Packages = installed
o.SrcPackages = srcPacks
if config.Conf.Offline {
if o.getServerInfo().Mode.IsOffline() {
return nil
}
if config.Conf.Deep || o.Distro.Family == config.Raspbian {
if o.getServerInfo().Mode.IsDeep() || o.Distro.Family == config.Raspbian {
unsecures, err := o.scanUnsecurePackages(updatable)
if err != nil {
o.log.Errorf("Failed to scan vulnerable packages: %s", err)
@@ -281,23 +327,63 @@ func (o *debian) rebootRequired() (bool, error) {
}
}
const dpkgQuery = `dpkg-query -W -f="\${binary:Package},\${db:Status-Abbrev},\${Version},\${Source},\${source:Version}\n"`
func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, models.SrcPackages, error) {
installed, updatable, srcPacks := models.Packages{}, models.Packages{}, models.SrcPackages{}
r := o.exec(`dpkg-query -W -f="\${binary:Package},\${db:Status-Abbrev},\${Version},\${Source},\${source:Version}\n"`, noSudo)
updatable := models.Packages{}
r := o.exec(dpkgQuery, noSudo)
if !r.isSuccess() {
return nil, nil, nil, fmt.Errorf("Failed to SSH: %s", r)
}
installed, srcPacks, err := o.parseInstalledPackages(r.Stdout)
if err != nil {
return nil, nil, nil, err
}
if o.getServerInfo().Mode.IsOffline() || o.getServerInfo().Mode.IsFast() {
return installed, updatable, srcPacks, nil
}
if err := o.aptGetUpdate(); err != nil {
return nil, nil, nil, err
}
updatableNames, err := o.getUpdatablePackNames()
if err != nil {
return nil, nil, nil, err
}
for _, name := range updatableNames {
for _, pack := range installed {
if pack.Name == name {
updatable[name] = pack
break
}
}
}
// Fill the candidate versions of upgradable packages
err = o.fillCandidateVersion(updatable)
if err != nil {
return nil, nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err)
}
installed.MergeNewVersion(updatable)
return installed, updatable, srcPacks, nil
}
func (o *debian) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
installed, srcPacks := models.Packages{}, models.SrcPackages{}
// e.g.
// curl,ii ,7.38.0-4+deb8u2,,7.38.0-4+deb8u2
// openssh-server,ii ,1:6.7p1-5+deb8u3,openssh,1:6.7p1-5+deb8u3
// tar,ii ,1.27.1-2+b1,tar (1.27.1-2),1.27.1-2
lines := strings.Split(r.Stdout, "\n")
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if trimmed := strings.TrimSpace(line); len(trimmed) != 0 {
name, status, version, srcName, srcVersion, err := o.parseScannedPackagesLine(trimmed)
if err != nil {
return nil, nil, nil, fmt.Errorf(
if err != nil || len(status) < 2 {
return nil, nil, fmt.Errorf(
"Debian: Failed to parse package line: %s", line)
}
@@ -343,35 +429,7 @@ func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, mode
for name := range installed {
delete(srcPacks, name)
}
if config.Conf.Offline {
return installed, updatable, srcPacks, nil
}
if err := o.aptGetUpdate(); err != nil {
return nil, nil, nil, err
}
updatableNames, err := o.getUpdatablePackNames()
if err != nil {
return nil, nil, nil, err
}
for _, name := range updatableNames {
for _, pack := range installed {
if pack.Name == name {
updatable[name] = pack
break
}
}
}
// Fill the candidate versions of upgradable packages
err = o.fillCandidateVersion(updatable)
if err != nil {
return nil, nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err)
}
installed.MergeNewVersion(updatable)
return installed, updatable, srcPacks, nil
return installed, srcPacks, nil
}
func (o *debian) parseScannedPackagesLine(line string) (name, status, version, srcName, srcVersion string, err error) {
@@ -382,7 +440,7 @@ func (o *debian) parseScannedPackagesLine(line string) (name, status, version, s
if i := strings.IndexRune(name, ':'); i >= 0 {
name = name[:i]
}
status = ss[1]
status = strings.TrimSpace(ss[1])
version = ss[2]
// remove version. ex: tar (1.27.1-2)
srcName = strings.Split(ss[3], " ")[0]
@@ -417,7 +475,7 @@ func (o *debian) scanUnsecurePackages(updatable models.Packages) (models.VulnInf
}
// Collect CVE information of upgradable packages
vulnInfos, err := o.scanVulnInfos(updatable, meta)
vulnInfos, err := o.scanChangelogs(updatable, meta)
if err != nil {
return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err)
}
@@ -547,7 +605,7 @@ type DetectedCveID struct {
Confidence models.Confidence
}
func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) {
func (o *debian) scanChangelogs(updatablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) {
type response struct {
pack *models.Package
DetectedCveIDs []DetectedCveID
@@ -583,7 +641,7 @@ func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta)
// if the changelog is not in cache or failed to get from local cache,
// get the changelog of the package via internet.
// After that, store it in the cache.
if cveIDs, pack, err := o.scanPackageCveIDs(p); err != nil {
if cveIDs, pack, err := o.fetchParseChangelog(p); err != nil {
errChan <- err
} else {
resChan <- response{pack, cveIDs}
@@ -639,7 +697,7 @@ func (o *debian) scanVulnInfos(updatablePacks models.Packages, meta *cache.Meta)
vinfos[cveID.CveID] = models.VulnInfo{
CveID: cveID.CveID,
Confidence: cveID.Confidence,
Confidences: models.Confidences{cveID.Confidence},
AffectedPackages: affected,
}
}
@@ -681,7 +739,7 @@ func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string
return changelog
}
func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, *models.Package, error) {
func (o *debian) fetchParseChangelog(pack models.Package) ([]DetectedCveID, *models.Package, error) {
cmd := ""
switch o.Distro.Family {
case config.Ubuntu, config.Raspbian:
@@ -760,7 +818,7 @@ func (o *debian) getCveIDsFromChangelog(
// Only logging the error.
o.log.Warnf("Failed to find the version in changelog: %s-%s", name, ver)
o.log.Debugf("Changelog of : %s-%s-%s", name, ver, changelog)
o.log.Debugf("Changelog of %s-%s: %s", name, ver, changelog)
// If the version is not in changelog, return entire changelog to put into cache
pack := o.Packages[name]
@@ -886,3 +944,145 @@ func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, err
}
return ver, fmt.Errorf("Unknown Format: %s", stdout)
}
func (o *debian) checkrestart() error {
initName, err := o.detectInitSystem()
if err != nil {
o.log.Warn(err)
// continue scanning
}
cmd := "LANGUAGE=en_US.UTF-8 checkrestart"
r := o.exec(cmd, sudo)
if !r.isSuccess() {
return fmt.Errorf(
"Failed to %s. status: %d, stdout: %s, stderr: %s",
cmd, r.ExitStatus, r.Stdout, r.Stderr)
}
packs, unknownServices := o.parseCheckRestart(r.Stdout)
pidService := map[string]string{}
if initName == upstart {
for _, s := range unknownServices {
cmd := "LANGUAGE=en_US.UTF-8 initctl status " + s
r := o.exec(cmd, sudo)
if !r.isSuccess() {
continue
}
if ss := strings.Fields(r.Stdout); len(ss) == 4 && ss[2] == "process" {
pidService[ss[3]] = s
}
}
}
for i, p := range packs {
pack := o.Packages[p.Name]
pack.NeedRestartProcs = p.NeedRestartProcs
o.Packages[p.Name] = pack
for j, proc := range p.NeedRestartProcs {
if proc.HasInit == false {
continue
}
packs[i].NeedRestartProcs[j].InitSystem = initName
if initName == systemd {
name, err := o.detectServiceName(proc.PID)
if err != nil {
o.log.Warn(err)
// continue scanning
}
packs[i].NeedRestartProcs[j].ServiceName = name
} else {
if proc.ServiceName == "" {
if ss := strings.Fields(r.Stdout); len(ss) == 4 && ss[2] == "process" {
if name, ok := pidService[ss[3]]; ok {
packs[i].NeedRestartProcs[j].ServiceName = name
}
}
}
}
}
o.Packages[p.Name] = p
}
return nil
}
func (o *debian) parseCheckRestart(stdout string) (models.Packages, []string) {
services := []string{}
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "service") && strings.HasSuffix(line, "restart") {
ss := strings.Fields(line)
if len(ss) != 3 {
continue
}
services = append(services, ss[1])
}
}
packs := models.Packages{}
packName := ""
hasInit := true
scanner = bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
if strings.HasSuffix(line, "do not seem to have an associated init script to restart them:") {
hasInit = false
continue
}
if strings.HasSuffix(line, ":") && len(strings.Fields(line)) == 1 {
packName = strings.TrimSuffix(line, ":")
continue
}
if strings.HasPrefix(line, "\t") {
ss := strings.Fields(line)
if len(ss) != 2 {
continue
}
serviceName := ""
for _, s := range services {
if packName == s {
serviceName = s
}
}
if p, ok := packs[packName]; ok {
p.NeedRestartProcs = append(p.NeedRestartProcs, models.NeedRestartProcess{
PID: ss[0],
Path: ss[1],
ServiceName: serviceName,
HasInit: hasInit,
})
packs[packName] = p
} else {
packs[packName] = models.Package{
Name: packName,
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: ss[0],
Path: ss[1],
ServiceName: serviceName,
HasInit: hasInit,
},
},
}
}
}
}
unknownServices := []string{}
for _, s := range services {
found := false
for _, p := range packs {
for _, proc := range p.NeedRestartProcs {
if proc.ServiceName == s {
found = true
}
}
}
if !found {
unknownServices = append(unknownServices, s)
}
}
return packs, unknownServices
}

View File

@@ -587,3 +587,139 @@ func TestParseAptCachePolicy(t *testing.T) {
}
}
}
func TestParseCheckRestart(t *testing.T) {
r := newDebian(config.ServerInfo{})
r.Distro = config.Distro{Family: "debian"}
var tests = []struct {
in string
out models.Packages
unknownServices []string
}{
{
in: `Found 27 processes using old versions of upgraded files
(19 distinct programs)
(15 distinct packages)
Of these, 14 seem to contain systemd service definitions or init scripts which can be used to restart them.
The following packages seem to have definitions that could be used
to restart their services:
varnish:
3490 /usr/sbin/varnishd
3704 /usr/sbin/varnishd
memcached:
3636 /usr/bin/memcached
openssh-server:
1252 /usr/sbin/sshd
1184 /usr/sbin/sshd
accountsservice:
462 /usr/lib/accountsservice/accounts-daemon
These are the systemd services:
systemctl restart accounts-daemon.service
These are the initd scripts:
service varnish restart
service memcached restart
service ssh restart
These processes (1) do not seem to have an associated init script to restart them:
util-linux:
3650 /sbin/agetty
3648 /sbin/agetty`,
out: models.NewPackages(
models.Package{
Name: "varnish",
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: "3490",
Path: "/usr/sbin/varnishd",
ServiceName: "varnish",
HasInit: true,
},
{
PID: "3704",
Path: "/usr/sbin/varnishd",
ServiceName: "varnish",
HasInit: true,
},
},
},
models.Package{
Name: "memcached",
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: "3636",
Path: "/usr/bin/memcached",
ServiceName: "memcached",
HasInit: true,
},
},
},
models.Package{
Name: "openssh-server",
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: "1252",
Path: "/usr/sbin/sshd",
ServiceName: "",
HasInit: true,
},
{
PID: "1184",
Path: "/usr/sbin/sshd",
ServiceName: "",
HasInit: true,
},
},
},
models.Package{
Name: "accountsservice",
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: "462",
Path: "/usr/lib/accountsservice/accounts-daemon",
ServiceName: "",
HasInit: true,
},
},
},
models.Package{
Name: "util-linux",
NeedRestartProcs: []models.NeedRestartProcess{
{
PID: "3650",
Path: "/sbin/agetty",
HasInit: false,
},
{
PID: "3648",
Path: "/sbin/agetty",
HasInit: false,
},
},
},
),
unknownServices: []string{"ssh"},
},
{
in: `Found 0 processes using old versions of upgraded files`,
out: models.Packages{},
unknownServices: []string{},
},
}
for _, tt := range tests {
packages, services := r.parseCheckRestart(tt.in)
for name, ePack := range tt.out {
if !reflect.DeepEqual(ePack, packages[name]) {
e := pp.Sprintf("%v", ePack)
a := pp.Sprintf("%v", packages[name])
t.Errorf("expected %s, actual %s", e, a)
}
}
if !reflect.DeepEqual(tt.unknownServices, services) {
t.Errorf("expected %s, actual %s", tt.unknownServices, services)
}
}
}

View File

@@ -370,7 +370,7 @@ func decorateCmd(c conf.ServerInfo, cmd string, sudo bool) string {
// }
if c.IsContainer() {
switch c.Containers.Type {
switch c.ContainerType {
case "", "docker":
cmd = fmt.Sprintf(`docker exec --user 0 %s %s -c '%s'`,
c.Container.ContainerID, dockerShell(c.Distro.Family), cmd)

View File

@@ -69,9 +69,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo false docker
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "docker"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "docker",
},
cmd: "ls",
sudo: false,
@@ -80,9 +80,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo true docker
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "docker"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "docker",
},
cmd: "ls",
sudo: true,
@@ -91,9 +91,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo false, docker
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "docker"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "docker",
},
cmd: "ls",
sudo: false,
@@ -102,9 +102,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true, docker
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "docker"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "docker",
},
cmd: "ls",
sudo: true,
@@ -113,9 +113,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true, docker
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "docker"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "docker",
},
cmd: "ls | grep hoge",
sudo: true,
@@ -125,9 +125,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo false lxd
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxd"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxd",
},
cmd: "ls",
sudo: false,
@@ -136,9 +136,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo true lxd
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxd"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxd",
},
cmd: "ls",
sudo: true,
@@ -147,9 +147,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo false, lxd
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxd"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxd",
},
cmd: "ls",
sudo: false,
@@ -158,9 +158,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true, lxd
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxd"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxd",
},
cmd: "ls",
sudo: true,
@@ -169,9 +169,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true lxd
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxd"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxd",
},
cmd: "ls | grep hoge",
sudo: true,
@@ -181,9 +181,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo false lxc
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxc"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxc",
},
cmd: "ls",
sudo: false,
@@ -192,9 +192,9 @@ func TestDecorateCmd(t *testing.T) {
// root sudo true lxc
{
conf: config.ServerInfo{
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxc"},
User: "root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxc",
},
cmd: "ls",
sudo: true,
@@ -203,9 +203,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo false, lxc
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxc"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxc",
},
cmd: "ls",
sudo: false,
@@ -214,9 +214,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true, lxc
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxc"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxc",
},
cmd: "ls",
sudo: true,
@@ -225,9 +225,9 @@ func TestDecorateCmd(t *testing.T) {
// non-root sudo true lxc
{
conf: config.ServerInfo{
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
Containers: config.Containers{Type: "lxc"},
User: "non-root",
Container: config.Container{ContainerID: "abc", Name: "def"},
ContainerType: "lxc",
},
cmd: "ls | grep hoge",
sudo: true,

View File

@@ -67,18 +67,26 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) {
return false, bsd
}
func (o *bsd) checkScanMode() error {
if o.getServerInfo().Mode.IsOffline() {
return fmt.Errorf("Remove offline scan mode, FreeBSD needs internet connection")
}
return nil
}
func (o *bsd) checkIfSudoNoPasswd() error {
// FreeBSD doesn't need root privilege
o.log.Infof("sudo ... No need")
return nil
}
func (o *bsd) checkDependencies() error {
func (o *bsd) checkDeps() error {
o.log.Infof("Dependencies... No need")
return nil
}
func (o *bsd) preCure() error {
o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
if err := o.detectIPAddr(); err != nil {
o.log.Debugf("Failed to detect IP addresses: %s", err)
}
@@ -158,6 +166,10 @@ func (o *bsd) scanPackages() error {
return nil
}
func (o *bsd) parseInstalledPackages(string) (models.Packages, models.SrcPackages, error) {
return nil, nil, nil
}
func (o *bsd) rebootRequired() (bool, error) {
r := o.exec("freebsd-version -k", noSudo)
if !r.isSuccess() {
@@ -245,7 +257,7 @@ func (o *bsd) scanUnsecurePackages() (models.VulnInfos, error) {
CveID: cveID,
AffectedPackages: affected,
DistroAdvisories: disAdvs,
Confidence: models.PkgAuditMatch,
Confidences: models.Confidences{models.PkgAuditMatch},
}
}
return vinfos, nil

167
scan/oracle.go Normal file
View File

@@ -0,0 +1,167 @@
package scan
import (
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// inherit OsTypeInterface
type oracle struct {
redhatBase
}
// NewAmazon is constructor
func newOracle(c config.ServerInfo) *oracle {
r := &oracle{
redhatBase{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
VulnInfos: models.VulnInfos{},
},
},
sudo: rootPrivOracle{},
},
}
r.log = util.NewCustomLogger(c)
r.setServerInfo(c)
return r
}
func (o *oracle) checkScanMode() error {
return nil
}
func (o *oracle) checkDeps() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckDeps(o.depsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckDeps(o.depsFastRoot())
} else {
return o.execCheckDeps(o.depsDeep())
}
}
func (o *oracle) depsFast() []string {
if o.getServerInfo().Mode.IsOffline() {
return []string{}
}
// repoquery
return []string{"yum-utils"}
}
func (o *oracle) depsFastRoot() []string {
if o.getServerInfo().Mode.IsOffline() {
//TODO
// return []string{"yum-plugin-ps"}
}
majorVersion, _ := o.Distro.MajorVersion()
switch majorVersion {
case 5:
return []string{
"yum-utils",
"yum-security",
}
case 6:
return []string{
"yum-utils",
"yum-plugin-security",
//TODO
// return []string{"yum-plugin-ps"}
}
default:
return []string{
"yum-utils",
//TODO
// return []string{"yum-plugin-ps"}
}
}
}
func (o *oracle) depsDeep() []string {
majorVersion, _ := o.Distro.MajorVersion()
switch majorVersion {
case 5:
return []string{
"yum-utils",
"yum-security",
"yum-changelog",
}
case 6:
return []string{
"yum-utils",
"yum-plugin-security",
"yum-plugin-changelog",
//TODO
// return []string{"yum-plugin-ps"}
}
default:
return []string{
"yum-utils",
"yum-plugin-changelog",
//TODO
// return []string{"yum-plugin-ps"}
}
}
}
func (o *oracle) checkIfSudoNoPasswd() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFastRoot())
} else {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsDeep())
}
}
func (o *oracle) sudoNoPasswdCmdsFast() []cmd {
return []cmd{}
}
func (o *oracle) sudoNoPasswdCmdsFastRoot() []cmd {
cmds := []cmd{{"needs-restarting", exitStatusZero}}
if o.getServerInfo().Mode.IsOffline() {
return cmds
}
majorVersion, _ := o.Distro.MajorVersion()
if majorVersion < 6 {
return []cmd{
{"yum repolist --color=never", exitStatusZero},
{"yum list-security --security --color=never", exitStatusZero},
{"yum info-security --color=never", exitStatusZero},
{"repoquery -h", exitStatusZero},
}
}
return append(cmds,
cmd{"yum repolist --color=never", exitStatusZero},
cmd{"yum updateinfo list updates --security --color=never", exitStatusZero},
cmd{"yum updateinfo updates --security --color=never", exitStatusZero},
cmd{"repoquery -h", exitStatusZero})
}
func (o *oracle) sudoNoPasswdCmdsDeep() []cmd {
return o.sudoNoPasswdCmdsFastRoot()
}
type rootPrivOracle struct{}
func (o rootPrivOracle) repoquery() bool {
return true
}
func (o rootPrivOracle) yumRepolist() bool {
return true
}
func (o rootPrivOracle) yumUpdateInfo() bool {
return true
}
// root privilege isn't needed
func (o rootPrivOracle) yumChangelog() bool {
return false
}

View File

@@ -48,11 +48,15 @@ func newPseudo(c config.ServerInfo) *pseudo {
return d
}
func (o *pseudo) checkScanMode() error {
return nil
}
func (o *pseudo) checkIfSudoNoPasswd() error {
return nil
}
func (o *pseudo) checkDependencies() error {
func (o *pseudo) checkDeps() error {
return nil
}
@@ -68,6 +72,10 @@ func (o *pseudo) scanPackages() error {
return nil
}
func (o *pseudo) parseInstalledPackages(string) (models.Packages, models.SrcPackages, error) {
return nil, nil, nil
}
func (o *pseudo) detectPlatform() {
o.setPlatform(models.Platform{Name: "other"})
return

View File

@@ -31,34 +31,11 @@ import (
ver "github.com/knqyf263/go-rpm-version"
)
// inherit OsTypeInterface
type redhat struct {
base
}
// NewRedhat is constructor
func newRedhat(c config.ServerInfo) *redhat {
r := &redhat{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
VulnInfos: models.VulnInfos{},
},
},
}
r.log = util.NewCustomLogger(c)
r.setServerInfo(c)
return r
}
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/redhat.rb
func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
red = newRedhat(c)
func detectRedhat(c config.ServerInfo) (bool, osTypeInterface) {
if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() {
red.setDistro(config.Fedora, "unknown")
util.Log.Warnf("Fedora not tested yet: %s", r)
return true, red
return true, &unknown{}
}
if r := exec(c, "ls /etc/oracle-release", noSudo); r.isSuccess() {
@@ -69,12 +46,13 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
if len(result) != 3 {
util.Log.Warnf("Failed to parse Oracle Linux version: %s", r)
return true, red
return true, newOracle(c)
}
ora := newOracle(c)
release := result[2]
red.setDistro(config.Oracle, release)
return true, red
ora.setDistro(config.Oracle, release)
return true, ora
}
}
@@ -86,14 +64,15 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
if len(result) != 3 {
util.Log.Warnf("Failed to parse CentOS version: %s", r)
return true, red
return true, newCentOS(c)
}
release := result[2]
switch strings.ToLower(result[1]) {
case "centos", "centos linux":
red.setDistro(config.CentOS, release)
return true, red
cent := newCentOS(c)
cent.setDistro(config.CentOS, release)
return true, cent
default:
util.Log.Warnf("Failed to parse CentOS: %s", r)
}
@@ -110,19 +89,22 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
if len(result) != 3 {
util.Log.Warnf("Failed to parse RedHat/CentOS version: %s", r)
return true, red
return true, newCentOS(c)
}
release := result[2]
switch strings.ToLower(result[1]) {
case "centos", "centos linux":
red.setDistro(config.CentOS, release)
cent := newCentOS(c)
cent.setDistro(config.CentOS, release)
return true, cent
default:
red.setDistro(config.RedHat, release)
// RHEL
rhel := newRHEL(c)
rhel.setDistro(config.RedHat, release)
return true, rhel
}
return true, red
}
return true, red
}
if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
@@ -132,6 +114,9 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
if strings.HasPrefix(r.Stdout, "Amazon Linux release 2") {
fields := strings.Fields(r.Stdout)
release = fmt.Sprintf("%s %s", fields[3], fields[4])
} else if strings.HasPrefix(r.Stdout, "Amazon Linux 2") {
fields := strings.Fields(r.Stdout)
release = strings.Join(fields[2:], " ")
} else {
fields := strings.Fields(r.Stdout)
if len(fields) == 5 {
@@ -139,57 +124,40 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
}
}
}
red.setDistro(family, release)
return true, red
amazon := newAmazon(c)
amazon.setDistro(family, release)
return true, amazon
}
util.Log.Debugf("Not RedHat like Linux. servername: %s", c.ServerName)
return false, red
return false, &unknown{}
}
func (o *redhat) checkIfSudoNoPasswd() error {
if !config.Conf.Deep || !o.sudo() {
o.log.Infof("sudo ... No need")
return nil
}
// inherit OsTypeInterface
type redhatBase struct {
base
sudo rootPriv
}
type cmd struct {
cmd string
expectedStatusCodes []int
}
var cmds []cmd
var zero = []int{0}
type rootPriv interface {
repoquery() bool
yumRepolist() bool
yumUpdateInfo() bool
yumChangelog() bool
}
switch o.Distro.Family {
case config.RedHat, config.Oracle:
majorVersion, err := o.Distro.MajorVersion()
if err != nil {
return fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
}
type cmd struct {
cmd string
expectedStatusCodes []int
}
if majorVersion < 6 {
cmds = []cmd{
{"yum --color=never repolist", zero},
{"yum --color=never list-security --security", zero},
{"yum --color=never info-security", zero},
}
} else {
cmds = []cmd{
{"yum --color=never repolist", zero},
{"yum --color=never --security updateinfo list updates", zero},
{"yum --color=never --security updateinfo updates", zero},
}
}
if o.Distro.Family == config.RedHat {
cmds = append(cmds, cmd{"repoquery -h", zero})
}
}
var exitStatusZero = []int{0}
func (o *redhatBase) execCheckIfSudoNoPasswd(cmds []cmd) error {
for _, c := range cmds {
cmd := util.PrependProxyEnv(c.cmd)
o.log.Infof("Checking... sudo %s", cmd)
r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
r := o.exec(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess(c.expectedStatusCodes...) {
o.log.Errorf("Check sudo or proxy settings: %s", r)
return fmt.Errorf("Failed to sudo: %s", r)
@@ -199,63 +167,7 @@ func (o *redhat) checkIfSudoNoPasswd() error {
return nil
}
// - Fast offline scan mode
// Amazon ... yum-utils
//
// - Fast scan mode
// All ... yum-utils
//
// - Deep scan mode
// CentOS 6,7 ... yum-utils, yum-plugin-changelog
// RHEL 5 (U1-) ... yum-utils, yum-security, yum-changelog
// RHEL 6 ... yum-utils, yum-security, yum-plugin-changelog
// RHEL 7 ... yum-utils, yum-plugin-changelog
// Amazon ... yum-utils
func (o *redhat) checkDependencies() error {
majorVersion, err := o.Distro.MajorVersion()
if err != nil {
msg := fmt.Sprintf("Not implemented yet: %s, err: %s", o.Distro, err)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
if o.Distro.Family == config.CentOS {
if majorVersion < 6 {
msg := fmt.Sprintf("CentOS %s is not supported", o.Distro.Release)
o.log.Errorf(msg)
return fmt.Errorf(msg)
}
}
packNames := []string{}
if config.Conf.Fast {
// Online fast scan needs yum-utils to issue repoquery cmd
packNames = append(packNames, "yum-utils")
} else if config.Conf.Offline {
switch o.Distro.Family {
case config.Amazon:
// Offline scan doesn't support Amazon Linux
packNames = append(packNames, "yum-utils")
}
} else {
// Deep Scan
switch o.Distro.Family {
case config.CentOS, config.Amazon:
packNames = append(packNames, "yum-utils", "yum-plugin-changelog")
case config.RedHat, config.Oracle:
switch majorVersion {
case 5:
packNames = append(packNames, "yum-utils", "yum-security", "yum-changelog")
case 6:
packNames = append(packNames, "yum-utils", "yum-plugin-security", "yum-plugin-changelog")
default:
packNames = append(packNames, "yum-utils", "yum-plugin-changelog")
}
default:
return fmt.Errorf("Not implemented yet: %s", o.Distro)
}
}
func (o *redhatBase) execCheckDeps(packNames []string) error {
for _, name := range packNames {
cmd := "rpm -q " + name
if r := o.exec(cmd, noSudo); !r.isSuccess() {
@@ -268,7 +180,7 @@ func (o *redhat) checkDependencies() error {
return nil
}
func (o *redhat) preCure() error {
func (o *redhatBase) preCure() error {
if err := o.detectIPAddr(); err != nil {
o.log.Debugf("Failed to detect IP addresses: %s", err)
}
@@ -276,16 +188,27 @@ func (o *redhat) preCure() error {
return nil
}
func (o *redhat) postScan() error {
func (o *redhatBase) postScan() error {
if o.isExecYumPS() {
if err := o.yumPS(); err != nil {
return fmt.Errorf("Failed to execute yum-ps: %s", err)
}
}
if o.isExecNeedsRestarting() {
if err := o.needsRestarting(); err != nil {
return fmt.Errorf("Failed to execute need-restarting: %s", err)
}
}
return nil
}
func (o *redhat) detectIPAddr() (err error) {
func (o *redhatBase) detectIPAddr() (err error) {
o.log.Infof("Scanning in %s", o.getServerInfo().Mode)
o.ServerInfo.IPv4Addrs, o.ServerInfo.IPv6Addrs, err = o.ip()
return err
}
func (o *redhat) scanPackages() error {
func (o *redhatBase) scanPackages() error {
installed, err := o.scanInstalledPackages()
if err != nil {
o.log.Errorf("Failed to scan installed packages: %s", err)
@@ -299,7 +222,7 @@ func (o *redhat) scanPackages() error {
}
o.Kernel.RebootRequired = rebootRequired
if config.Conf.Offline {
if o.getServerInfo().Mode.IsOffline() {
switch o.Distro.Family {
case config.Amazon:
// nop
@@ -307,6 +230,11 @@ func (o *redhat) scanPackages() error {
o.Packages = installed
return nil
}
} else if o.Distro.Family == config.RedHat {
if o.getServerInfo().Mode.IsFast() {
o.Packages = installed
return nil
}
}
updatable, err := o.scanUpdatablePackages()
@@ -326,7 +254,7 @@ func (o *redhat) scanPackages() error {
return nil
}
func (o *redhat) rebootRequired() (bool, error) {
func (o *redhatBase) rebootRequired() (bool, error) {
r := o.exec("rpm -q --last kernel", noSudo)
scanner := bufio.NewScanner(strings.NewReader(r.Stdout))
if !r.isSuccess(0, 1) {
@@ -340,7 +268,7 @@ func (o *redhat) rebootRequired() (bool, error) {
return running != lastInstalledKernelVer, nil
}
func (o *redhat) scanInstalledPackages() (models.Packages, error) {
func (o *redhatBase) scanInstalledPackages() (models.Packages, error) {
release, version, err := o.runningKernel()
if err != nil {
return nil, err
@@ -350,19 +278,28 @@ func (o *redhat) scanInstalledPackages() (models.Packages, error) {
Version: version,
}
installed := models.Packages{}
r := o.exec(rpmQa(o.Distro), noSudo)
if !r.isSuccess() {
return nil, fmt.Errorf("Scan packages failed: %s", r)
}
installed, _, err := o.parseInstalledPackages(r.Stdout)
if err != nil {
return nil, err
}
return installed, nil
}
func (o *redhatBase) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) {
installed := models.Packages{}
latestKernelRelease := ver.NewVersion("")
// openssl 0 1.0.1e 30.el6.11 x86_64
lines := strings.Split(r.Stdout, "\n")
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if trimed := strings.TrimSpace(line); len(trimed) != 0 {
pack, err := o.parseInstalledPackagesLine(line)
if err != nil {
return nil, err
return nil, nil, err
}
// Kernel package may be isntalled multiple versions.
@@ -370,19 +307,28 @@ func (o *redhat) scanInstalledPackages() (models.Packages, error) {
// pay attention only to the running kernel
isKernel, running := isRunningKernel(pack, o.Distro.Family, o.Kernel)
if isKernel {
if !running {
if o.Kernel.Release == "" {
// When the running kernel release is unknown,
// use the latest release among the installed release
kernelRelease := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release))
if kernelRelease.LessThan(latestKernelRelease) {
continue
}
latestKernelRelease = kernelRelease
} else if !running {
o.log.Debugf("Not a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
continue
} else {
o.log.Debugf("Found a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
}
o.log.Debugf("Found a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
}
installed[pack.Name] = pack
}
}
return installed, nil
return installed, nil, nil
}
func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error) {
func (o *redhatBase) parseInstalledPackagesLine(line string) (models.Package, error) {
fields := strings.Fields(line)
if len(fields) != 5 {
return models.Package{},
@@ -404,13 +350,13 @@ func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error)
}, nil
}
func (o *redhat) scanUpdatablePackages() (models.Packages, error) {
func (o *redhatBase) scanUpdatablePackages() (models.Packages, error) {
cmd := `repoquery --all --pkgnarrow=updates --qf="%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{REPO}"`
for _, repo := range o.getServerInfo().Enablerepo {
cmd += " --enablerepo=" + repo
}
r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
r := o.exec(util.PrependProxyEnv(cmd), o.sudo.repoquery())
if !r.isSuccess() {
return nil, fmt.Errorf("Failed to SSH: %s", r)
}
@@ -420,7 +366,7 @@ func (o *redhat) scanUpdatablePackages() (models.Packages, error) {
}
// parseUpdatablePacksLines parse the stdout of repoquery to get package name, candidate version
func (o *redhat) parseUpdatablePacksLines(stdout string) (models.Packages, error) {
func (o *redhatBase) parseUpdatablePacksLines(stdout string) (models.Packages, error) {
updatable := models.Packages{}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
@@ -444,7 +390,7 @@ func (o *redhat) parseUpdatablePacksLines(stdout string) (models.Packages, error
return updatable, nil
}
func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) {
func (o *redhatBase) parseUpdatablePacksLine(line string) (models.Package, error) {
fields := strings.Fields(line)
if len(fields) < 5 {
return models.Package{}, fmt.Errorf("Unknown format: %s, fields: %s", line, fields)
@@ -469,25 +415,109 @@ func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) {
return p, nil
}
func (o *redhat) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) {
if config.Conf.Deep && o.Distro.Family != config.Amazon {
//TODO Cache changelogs to bolt
func (o *redhatBase) isExecScanUsingYum() bool {
if o.getServerInfo().Mode.IsOffline() {
return false
}
if o.Distro.Family == config.CentOS {
// CentOS doesn't have security channel
return false
}
if o.getServerInfo().Mode.IsFastRoot() || o.getServerInfo().Mode.IsDeep() {
return true
}
return true
}
func (o *redhatBase) isExecFillChangelogs() bool {
if o.getServerInfo().Mode.IsOffline() {
return false
}
// Amazon linux has no changelos for updates
return o.getServerInfo().Mode.IsDeep() &&
o.Distro.Family != config.Amazon
}
func (o *redhatBase) isExecScanChangelogs() bool {
if o.getServerInfo().Mode.IsOffline() ||
o.getServerInfo().Mode.IsFast() ||
o.getServerInfo().Mode.IsFastRoot() {
return false
}
return true
}
func (o *redhatBase) isExecYumPS() bool {
// RedHat has no yum-ps
switch o.Distro.Family {
case config.RedHat,
config.OpenSUSE,
config.OpenSUSELeap,
config.SUSEEnterpriseServer,
config.SUSEEnterpriseDesktop,
config.SUSEOpenstackCloud:
return false
}
// yum ps needs internet connection
if o.getServerInfo().Mode.IsOffline() || o.getServerInfo().Mode.IsFast() {
return false
}
return true
}
func (o *redhatBase) isExecNeedsRestarting() bool {
switch o.Distro.Family {
case config.OpenSUSE,
config.OpenSUSELeap,
config.SUSEEnterpriseServer,
config.SUSEEnterpriseDesktop,
config.SUSEOpenstackCloud:
// TODO zypper ps
// https://github.com/future-architect/vuls/issues/696
return false
case config.RedHat, config.CentOS, config.Oracle:
majorVersion, err := o.Distro.MajorVersion()
if err != nil || majorVersion < 6 {
o.log.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
return false
}
if o.getServerInfo().Mode.IsOffline() {
return false
} else if o.getServerInfo().Mode.IsFastRoot() ||
o.getServerInfo().Mode.IsDeep() {
return true
}
return false
}
if o.getServerInfo().Mode.IsFast() {
return false
}
return true
}
func (o *redhatBase) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) {
if o.isExecFillChangelogs() {
if err := o.fillChangelogs(updatable); err != nil {
return nil, err
}
}
if o.Distro.Family != config.CentOS {
// Amazon, RHEL, Oracle Linux has yum updateinfo as default
// yum updateinfo can collenct vendor advisory information.
return o.scanCveIDsByCommands(updatable)
if o.isExecScanUsingYum() {
return o.scanUsingYum(updatable)
}
// Parse chnagelog because CentOS does not have security channel...
return o.scanCveIDsInChangelog(updatable)
if o.isExecScanChangelogs() {
return o.scanChangelogs(updatable)
}
return models.VulnInfos{}, nil
}
func (o *redhat) fillChangelogs(updatables models.Packages) error {
func (o *redhatBase) fillChangelogs(updatables models.Packages) error {
names := []string{}
for name := range updatables {
names = append(names, name)
@@ -516,7 +546,7 @@ func (o *redhat) fillChangelogs(updatables models.Packages) error {
return nil
}
func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string, error) {
func (o *redhatBase) getAvailableChangelogs(packNames []string) (map[string]string, error) {
yumopts := ""
if 0 < len(o.getServerInfo().Enablerepo) {
yumopts = " --enablerepo=" + strings.Join(o.getServerInfo().Enablerepo, ",")
@@ -527,10 +557,10 @@ func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string,
if o.hasYumColorOption() {
yumopts += " --color=never"
}
cmd := `yum changelog all %s updates %s | grep -A 1000000 "==================== Updated Packages ===================="`
cmd := `yum changelog all updates %s %s | grep -A 1000000 "==================== Updated Packages ===================="`
cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " "))
r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumChangelog())
if !r.isSuccess(0, 1) {
return nil, fmt.Errorf("Failed to SSH: %s", r)
}
@@ -539,7 +569,7 @@ func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string,
}
// Divide available change logs of all updatable packages into each package's changelog
func (o *redhat) divideChangelogsIntoEachPackages(stdout string) map[string]string {
func (o *redhatBase) divideChangelogsIntoEachPackages(stdout string) map[string]string {
changelogs := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(stdout))
@@ -580,7 +610,7 @@ func (o *redhat) divideChangelogsIntoEachPackages(stdout string) map[string]stri
return changelogs
}
func (o *redhat) fillDiffChangelogs(packNames []string) error {
func (o *redhatBase) fillDiffChangelogs(packNames []string) error {
changelogs, err := o.getAvailableChangelogs(packNames)
if err != nil {
return err
@@ -593,19 +623,16 @@ func (o *redhat) fillDiffChangelogs(packNames []string) error {
if index := strings.Index(p.NewVersion, ":"); 0 < index {
epoch := p.NewVersion[0:index]
ver := p.NewVersion[index+1 : len(p.NewVersion)]
epochNameVerRel = fmt.Sprintf("%s:%s-%s",
epoch, p.Name, ver)
epochNameVerRel = fmt.Sprintf("%s:%s-%s", epoch, p.Name, ver)
} else {
epochNameVerRel = fmt.Sprintf("%s-%s",
p.Name, p.NewVersion)
epochNameVerRel = fmt.Sprintf("%s-%s", p.Name, p.NewVersion)
}
return strings.HasPrefix(s, epochNameVerRel)
})
if found {
diff, err := o.getDiffChangelog(pack, changelogs[s])
var detectionMethod string
diff, err := o.getDiffChangelog(pack, changelogs[s])
if err == nil {
detectionMethod = models.ChangelogExactMatchStr
} else {
@@ -650,7 +677,7 @@ func (o *redhat) fillDiffChangelogs(packNames []string) error {
return nil
}
func (o *redhat) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) {
func (o *redhatBase) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) {
installedVer := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release))
scanner := bufio.NewScanner(strings.NewReader(availableChangelog))
diff := []string{}
@@ -702,7 +729,7 @@ func (o *redhat) getDiffChangelog(pack models.Package, availableChangelog string
return strings.TrimSpace(strings.Join(diff, "\n")), nil
}
func (o *redhat) scanCveIDsInChangelog(updatable models.Packages) (models.VulnInfos, error) {
func (o *redhatBase) scanChangelogs(updatable models.Packages) (models.VulnInfos, error) {
packCveIDs := make(map[string][]string)
for name := range updatable {
cveIDs := []string{}
@@ -739,7 +766,7 @@ func (o *redhat) scanCveIDsInChangelog(updatable models.Packages) (models.VulnIn
vinfos[cid] = models.VulnInfo{
CveID: cid,
AffectedPackages: models.PackageStatuses{{Name: name}},
Confidence: models.ChangelogExactMatch,
Confidences: models.Confidences{models.ChangelogExactMatch},
}
}
}
@@ -754,7 +781,7 @@ type distroAdvisoryCveIDs struct {
// Scaning unsecure packages using yum-plugin-security.
// Amazon, RHEL, Oracle Linux
func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInfos, error) {
func (o *redhatBase) scanUsingYum(updatable models.Packages) (models.VulnInfos, error) {
if o.Distro.Family == config.CentOS {
// CentOS has no security channel.
return nil, fmt.Errorf(
@@ -769,8 +796,8 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf
var cmd string
if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major > 5 {
cmd = "yum --color=never repolist"
r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
cmd = "yum repolist --color=never"
r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumRepolist())
if !r.isSuccess() {
return nil, fmt.Errorf("Failed to SSH: %s", r)
}
@@ -782,9 +809,9 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf
cmd += " --color=never"
}
} else {
cmd = "yum --color=never --security updateinfo list updates"
cmd = "yum updateinfo list updates --security --color=never"
}
r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
r := o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo())
if !r.isSuccess() {
return nil, fmt.Errorf("Failed to SSH: %s", r)
}
@@ -812,9 +839,9 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf
cmd += " --color=never"
}
} else {
cmd = "yum --color=never --security updateinfo updates"
cmd = "yum updateinfo updates --security --color=never"
}
r = o.exec(util.PrependProxyEnv(cmd), o.sudo())
r = o.exec(util.PrependProxyEnv(cmd), o.sudo.yumUpdateInfo())
if !r.isSuccess() {
return nil, fmt.Errorf("Failed to SSH: %s", r)
}
@@ -848,7 +875,7 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf
CveID: cveID,
DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
AffectedPackages: affected,
Confidence: models.YumUpdateSecurityMatch,
Confidences: models.Confidences{models.YumUpdateSecurityMatch},
}
}
vinfos[cveID] = vinfo
@@ -859,7 +886,7 @@ func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInf
var horizontalRulePattern = regexp.MustCompile(`^=+$`)
func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
func (o *redhatBase) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
sectionState := Outside
lines := strings.Split(stdout, "\n")
lines = append(lines, "=============")
@@ -905,7 +932,7 @@ func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveID
case config.CentOS:
// CentOS has no security channel.
return result, fmt.Errorf(
"yum updateinfo is not suppported on CentOS")
"yum updateinfo is not suppported on CentOS")
case config.RedHat, config.Amazon, config.Oracle:
// nop
}
@@ -981,7 +1008,7 @@ const (
Content = iota
)
func (o *redhat) changeSectionState(state int) (newState int) {
func (o *redhatBase) changeSectionState(state int) (newState int) {
switch state {
case Outside, Content:
newState = Header
@@ -991,19 +1018,19 @@ func (o *redhat) changeSectionState(state int) (newState int) {
return newState
}
func (o *redhat) isCvesHeaderLine(line string) bool {
func (o *redhatBase) isCvesHeaderLine(line string) bool {
return strings.Contains(line, "CVEs : ")
}
var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`)
func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
func (o *redhatBase) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
return yumCveIDPattern.FindAllString(line, -1)
}
var yumAdvisoryIDPattern = regexp.MustCompile(`^ *Update ID : (.*)$`)
func (o *redhat) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
func (o *redhatBase) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
result := yumAdvisoryIDPattern.FindStringSubmatch(line)
if len(result) != 2 {
return "", false
@@ -1013,17 +1040,17 @@ func (o *redhat) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID stri
var yumIssuedPattern = regexp.MustCompile(`^\s*Issued : (\d{4}-\d{2}-\d{2})`)
func (o *redhat) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
func (o *redhatBase) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
return o.parseYumUpdateinfoLineToGetDate(line, yumIssuedPattern)
}
var yumUpdatedPattern = regexp.MustCompile(`^\s*Updated : (\d{4}-\d{2}-\d{2})`)
func (o *redhat) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
func (o *redhatBase) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
return o.parseYumUpdateinfoLineToGetDate(line, yumUpdatedPattern)
}
func (o *redhat) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *regexp.Regexp) (date time.Time, found bool) {
func (o *redhatBase) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *regexp.Regexp) (date time.Time, found bool) {
result := regexpPattern.FindStringSubmatch(line)
if len(result) != 2 {
return date, false
@@ -1037,13 +1064,13 @@ func (o *redhat) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *reg
var yumDescriptionPattern = regexp.MustCompile(`^\s*Description : `)
func (o *redhat) isDescriptionLine(line string) bool {
func (o *redhatBase) isDescriptionLine(line string) bool {
return yumDescriptionPattern.MatchString(line)
}
var yumSeverityPattern = regexp.MustCompile(`^ *Severity : (.*)$`)
func (o *redhat) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
func (o *redhatBase) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
result := yumSeverityPattern.FindStringSubmatch(line)
if len(result) != 2 {
return "", false
@@ -1066,7 +1093,7 @@ func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool)
}
return advisoryIDPacks{}, false
}
func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
func (o *redhatBase) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
fields := strings.Split(nameVerRel, ".")
archTrimed := strings.Join(fields[0:len(fields)-1], ".")
@@ -1078,7 +1105,7 @@ func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string
}
// parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS, ELSA), packages
func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
func (o *redhatBase) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
result := []advisoryIDPacks{}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
@@ -1120,21 +1147,201 @@ func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacks
return result, nil
}
func (o *redhat) clone() osTypeInterface {
return o
}
func (o *redhat) sudo() bool {
switch o.Distro.Family {
case config.Amazon, config.CentOS:
return false
default:
// RHEL, Oracle
return config.Conf.Deep
func (o *redhatBase) yumPS() error {
cmd := "LANGUAGE=en_US.UTF-8 yum info yum"
r := o.exec(util.PrependProxyEnv(cmd), noSudo)
if !r.isSuccess() {
return fmt.Errorf("Failed to SSH: %s", r)
}
if !o.checkYumPsInstalled(r.Stdout) {
switch o.Distro.Family {
case config.RedHat, config.Oracle:
return nil
default:
return fmt.Errorf("yum-plugin-ps is not installed")
}
}
cmd = "LANGUAGE=en_US.UTF-8 yum -q ps all --color=never"
r = o.exec(util.PrependProxyEnv(cmd), sudo)
if !r.isSuccess() {
return fmt.Errorf("Failed to SSH: %s", r)
}
packs := o.parseYumPS(r.Stdout)
for name, pack := range packs {
p := o.Packages[name]
p.AffectedProcs = pack.AffectedProcs
o.Packages[name] = p
}
return nil
}
func (o *redhat) hasYumColorOption() bool {
func (o *redhatBase) checkYumPsInstalled(stdout string) bool {
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "Loaded plugins: ") {
if strings.Contains(line, " ps,") || strings.HasSuffix(line, " ps") {
return true
}
return false
}
}
return false
}
func (o *redhatBase) parseYumPS(stdout string) models.Packages {
packs := models.Packages{}
scanner := bufio.NewScanner(strings.NewReader(stdout))
isPackageLine, needToParseProcline := false, false
currentPackName := ""
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) == 0 ||
len(fields) == 1 && fields[0] == "ps" ||
len(fields) == 6 && fields[0] == "pid" {
continue
}
isPackageLine = !strings.HasPrefix(line, " ")
if isPackageLine {
if 1 < len(fields) && fields[1] == "Upgrade" {
needToParseProcline = true
// Search o.Packages to divide into name, version, release
name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
var epochNameVerRel string
if index := strings.Index(p.Version, ":"); 0 < index {
epoch := p.Version[0:index]
ver := p.Version[index+1 : len(p.Version)]
epochNameVerRel = fmt.Sprintf("%s:%s-%s-%s.%s",
epoch, p.Name, ver, p.Release, p.Arch)
} else {
epochNameVerRel = fmt.Sprintf("%s-%s-%s.%s",
p.Name, p.Version, p.Release, p.Arch)
}
return strings.HasPrefix(fields[0], epochNameVerRel)
})
if !found {
o.log.Errorf("`yum ps` package is not found: %s", line)
continue
}
packs[name] = pack
currentPackName = name
} else {
needToParseProcline = false
}
} else if needToParseProcline {
if 6 < len(fields) {
proc := models.AffectedProcess{
PID: fields[0],
Name: fields[1],
}
pack := packs[currentPackName]
pack.AffectedProcs = append(pack.AffectedProcs, proc)
packs[currentPackName] = pack
} else {
o.log.Errorf("`yum ps` Unknown Format: %s", line)
continue
}
}
}
return packs
}
func (o *redhatBase) needsRestarting() error {
initName, err := o.detectInitSystem()
if err != nil {
o.log.Warn(err)
// continue scanning
}
cmd := "LANGUAGE=en_US.UTF-8 needs-restarting"
r := o.exec(cmd, sudo)
if !r.isSuccess() {
return fmt.Errorf("Failed to SSH: %s", r)
}
procs := o.parseNeedsRestarting(r.Stdout)
for _, proc := range procs {
fqpn, err := o.procPathToFQPN(proc.Path)
if err != nil {
o.log.Warnf("Failed to detect a package name of need restarting process from the command path: %s, %s",
proc.Path, err)
continue
}
pack, err := o.Packages.FindByFQPN(fqpn)
if err != nil {
return err
}
if initName == systemd {
name, err := o.detectServiceName(proc.PID)
if err != nil {
o.log.Warn(err)
// continue scanning
}
proc.ServiceName = name
proc.InitSystem = systemd
}
pack.NeedRestartProcs = append(pack.NeedRestartProcs, proc)
o.Packages[pack.Name] = *pack
}
return nil
}
func (o *redhatBase) parseNeedsRestarting(stdout string) (procs []models.NeedRestartProcess) {
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
line := scanner.Text()
line = strings.Replace(line, "\x00", " ", -1) // for CentOS6.9
ss := strings.Split(line, " : ")
if len(ss) < 2 {
continue
}
// https://unix.stackexchange.com/a/419375
if ss[0] == "1" {
continue
}
path := ss[1]
if !strings.HasPrefix(path, "/") {
path = strings.Fields(path)[0]
// [ec2-user@ip-172-31-11-139 ~]$ sudo needs-restarting
// 2024 : auditd
// [ec2-user@ip-172-31-11-139 ~]$ type -p auditd
// /sbin/auditd
cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 which %s", path)
r := o.exec(cmd, sudo)
if !r.isSuccess() {
o.log.Warnf("Failed to exec which %s: %s", path, r)
continue
}
path = strings.TrimSpace(r.Stdout)
}
procs = append(procs, models.NeedRestartProcess{
PID: ss[0],
Path: path,
HasInit: true,
})
}
return
}
// procPathToFQPN returns Fully-Qualified-Package-Name from the command
func (o *redhatBase) procPathToFQPN(execCommand string) (string, error) {
execCommand = strings.Replace(execCommand, "\x00", " ", -1) // for CentOS6.9
path := strings.Fields(execCommand)[0]
cmd := `LANGUAGE=en_US.UTF-8 rpm -qf --queryformat "%{NAME}-%{EPOCH}:%{VERSION}-%{RELEASE}.%{ARCH}\n" ` + path
r := o.exec(cmd, noSudo)
if !r.isSuccess() {
return "", fmt.Errorf("Failed to SSH: %s", r)
}
fqpn := strings.TrimSpace(r.Stdout)
return strings.Replace(fqpn, "-(none):", "-", -1), nil
}
func (o *redhatBase) hasYumColorOption() bool {
cmd := "yum --help | grep color"
r := o.exec(util.PrependProxyEnv(cmd), noSudo)
return len(r.Stdout) > 0

View File

@@ -32,9 +32,90 @@ import (
// return t
// }
func TestParseScanedPackagesLineRedhat(t *testing.T) {
func TestParseInstalledPackagesLinesRedhat(t *testing.T) {
r := newRHEL(config.ServerInfo{})
r.Distro = config.Distro{Family: config.RedHat}
r := newRedhat(config.ServerInfo{})
var packagetests = []struct {
in string
kernel models.Kernel
packages models.Packages
}{
{
in: `openssl 0 1.0.1e 30.el6.11 x86_64
Percona-Server-shared-56 1 5.6.19 rel67.0.el6 x84_64
kernel 0 2.6.32 696.20.1.el6 x86_64
kernel 0 2.6.32 696.20.3.el6 x86_64
kernel 0 2.6.32 695.20.3.el6 x86_64`,
kernel: models.Kernel{},
packages: models.Packages{
"openssl": models.Package{
Name: "openssl",
Version: "1.0.1e",
Release: "30.el6.11",
},
"Percona-Server-shared-56": models.Package{
Name: "Percona-Server-shared-56",
Version: "1:5.6.19",
Release: "rel67.0.el6",
},
"kernel": models.Package{
Name: "kernel",
Version: "2.6.32",
Release: "696.20.3.el6",
},
},
},
{
in: `openssl 0 1.0.1e 30.el6.11 x86_64
Percona-Server-shared-56 1 5.6.19 rel67.0.el6 x84_64
kernel 0 2.6.32 696.20.1.el6 x86_64
kernel 0 2.6.32 696.20.3.el6 x86_64
kernel 0 2.6.32 695.20.3.el6 x86_64`,
kernel: models.Kernel{Release: "2.6.32-695.20.3.el6.x86_64"},
packages: models.Packages{
"openssl": models.Package{
Name: "openssl",
Version: "1.0.1e",
Release: "30.el6.11",
},
"Percona-Server-shared-56": models.Package{
Name: "Percona-Server-shared-56",
Version: "1:5.6.19",
Release: "rel67.0.el6",
},
"kernel": models.Package{
Name: "kernel",
Version: "2.6.32",
Release: "695.20.3.el6",
},
},
},
}
for _, tt := range packagetests {
r.Kernel = tt.kernel
packages, _, err := r.parseInstalledPackages(tt.in)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
for name, expectedPack := range tt.packages {
pack := packages[name]
if pack.Name != expectedPack.Name {
t.Errorf("name: expected %s, actual %s", expectedPack.Name, pack.Name)
}
if pack.Version != expectedPack.Version {
t.Errorf("version: expected %s, actual %s", expectedPack.Version, pack.Version)
}
if pack.Release != expectedPack.Release {
t.Errorf("release: expected %s, actual %s", expectedPack.Release, pack.Release)
}
}
}
}
func TestParseScanedPackagesLineRedhat(t *testing.T) {
r := newRHEL(config.ServerInfo{})
var packagetests = []struct {
in string
@@ -74,7 +155,7 @@ func TestParseScanedPackagesLineRedhat(t *testing.T) {
}
func TestChangeSectionState(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
oldState int
newState int
@@ -92,7 +173,7 @@ func TestChangeSectionState(t *testing.T) {
}
func TestParseYumUpdateinfoLineToGetCveIDs(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
out []string
@@ -122,7 +203,7 @@ func TestParseYumUpdateinfoLineToGetCveIDs(t *testing.T) {
}
func TestParseYumUpdateinfoToGetAdvisoryID(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
out string
@@ -160,7 +241,7 @@ func TestParseYumUpdateinfoLineToGetIssued(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2015-12-15")
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
out time.Time
@@ -195,10 +276,9 @@ func TestParseYumUpdateinfoLineToGetIssued(t *testing.T) {
}
func TestParseYumUpdateinfoLineToGetUpdated(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2015-12-15")
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
out time.Time
@@ -234,7 +314,7 @@ func TestParseYumUpdateinfoLineToGetUpdated(t *testing.T) {
}
func TestIsDescriptionLine(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
found bool
@@ -262,7 +342,7 @@ func TestIsDescriptionLine(t *testing.T) {
}
func TestParseYumUpdateinfoToGetSeverity(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
var tests = []struct {
in string
out string
@@ -337,7 +417,7 @@ Description : kernel-uek
`
issued, _ := time.Parse("2006-01-02", "2017-02-15")
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
r.Distro = config.Distro{Family: "oraclelinux"}
var tests = []struct {
@@ -458,7 +538,7 @@ Description : The sudo packages contain the sudo utility which allows system
issued, _ := time.Parse("2006-01-02", "2015-09-03")
updated, _ := time.Parse("2006-01-02", "2015-09-04")
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
r.Distro = config.Distro{Family: "redhat"}
var tests = []struct {
@@ -532,8 +612,7 @@ Description : The sudo packages contain the sudo utility which allows system
}
func TestParseYumUpdateinfoAmazon(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newAmazon(config.ServerInfo{})
r.Distro = config.Distro{Family: "redhat"}
issued, _ := time.Parse("2006-01-02", "2015-12-15")
@@ -617,7 +696,7 @@ Description : Package updates are available for Amazon Linux AMI that fix the
}
func TestParseYumCheckUpdateLine(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newCentOS(config.ServerInfo{})
r.Distro = config.Distro{Family: "centos"}
var tests = []struct {
in string
@@ -658,7 +737,7 @@ func TestParseYumCheckUpdateLine(t *testing.T) {
}
func TestParseYumCheckUpdateLines(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newCentOS(config.ServerInfo{})
r.Distro = config.Distro{Family: "centos"}
stdout := `audit-libs 0 2.3.7 5.el6 base
bash 0 4.1.2 33.el6_7.1 updates
@@ -739,7 +818,7 @@ pytalloc 0 2.0.7 2.el6 @CentOS 6.5/6.5`
}
func TestParseYumCheckUpdateLinesAmazon(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newAmazon(config.ServerInfo{})
r.Distro = config.Distro{Family: "amazon"}
stdout := `bind-libs 32 9.8.2 0.37.rc1.45.amzn1 amzn-main
java-1.7.0-openjdk 0 1.7.0.95 2.6.4.0.65.amzn1 amzn-main
@@ -795,7 +874,7 @@ if-not-architecture 0 100 200 amzn-main`
}
func TestParseYumUpdateinfoListAvailable(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
rhelStdout := `RHSA-2015:2315 Moderate/Sec. NetworkManager-1:1.0.6-27.el7.x86_64
RHSA-2015:2315 Moderate/Sec. NetworkManager-config-server-1:1.0.6-27.el7.x86_64
RHSA-2015:1705 Important/Sec. bind-libs-lite-32:9.9.4-18.el7_1.5.x86_64
@@ -859,7 +938,7 @@ updateinfo list done`
}
func TestExtractPackNameVerRel(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newAmazon(config.ServerInfo{})
var tests = []struct {
in string
out []string
@@ -894,7 +973,7 @@ func TestExtractPackNameVerRel(t *testing.T) {
}
func TestGetDiffChangelog(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newCentOS(config.ServerInfo{})
type in struct {
pack models.Package
changelog string
@@ -1245,7 +1324,7 @@ func TestGetDiffChangelog(t *testing.T) {
}
func TestDivideChangelogsIntoEachPackages(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newRHEL(config.ServerInfo{})
type in struct {
pack models.Package
changelog string
@@ -1297,3 +1376,389 @@ func TestDivideChangelogsIntoEachPackages(t *testing.T) {
}
}
func TestCheckYumPsInstalled(t *testing.T) {
r := newCentOS(config.ServerInfo{})
var tests = []struct {
in string
out bool
}{
{
in: `Loaded plugins: changelog, fastestmirror, ps, remove-with-leaves, show-leaves
Loading mirror speeds from cached hostfile
* base: ftp.tsukuba.wide.ad.jp
* extras: ftp.tsukuba.wide.ad.jp
* updates: ftp.tsukuba.wide.ad.jp
Installed Packages
Name : yum
Arch : noarch
Version : 3.4.3
Release : 150.el7.centos
Size : 5.5 M
Repo : installed
From repo : anaconda
Summary : RPM package installer/updater/manager
URL : http://yum.baseurl.org/
License : GPLv2+
Description : Yum is a utility that can check for and automatically download and
: install updated RPM packages. Dependencies are obtained and downloaded
: automatically, prompting the user for permission as necessary.
Available Packages
Name : yum
Arch : noarch
Version : 3.4.3
Release : 154.el7.centos.1
Size : 1.2 M
Repo : updates/7/x86_64
Summary : RPM package installer/updater/manager
URL : http://yum.baseurl.org/
License : GPLv2+
Description : Yum is a utility that can check for and automatically download and
: install updated RPM packages. Dependencies are obtained and downloaded
: automatically, prompting the user for permission as necessary.`,
out: true,
},
{
in: `Failed to set locale, defaulting to C
Loaded plugins: amazon-id, rhui-lb, search-disabled-repos
Installed Packages
Name : yum
Arch : noarch
Version : 3.4.3
Release : 154.el7
Size : 5.5 M
Repo : installed
From repo : rhui-REGION-rhel-server-releases
Summary : RPM package installer/updater/manager
URL : http://yum.baseurl.org/
License : GPLv2+
Description : Yum is a utility that can check for and automatically download and
: install updated RPM packages. Dependencies are obtained and downloaded
: automatically, prompting the user for permission as necessary.`,
out: false,
},
}
for _, tt := range tests {
ok := r.checkYumPsInstalled(tt.in)
if ok != tt.out {
t.Errorf("expected: %v\nactual: %v", tt.out, ok)
}
}
}
func TestParseYumPS(t *testing.T) {
r := newCentOS(config.ServerInfo{})
r.Distro = config.Distro{Family: "centos"}
r.Packages = models.NewPackages(
models.Package{
Name: "python",
Version: "2.7.5",
Release: "34.el7",
Arch: "x86_64",
},
models.Package{
Name: "util-linux",
Version: "2.23.2",
Release: "26.el7",
Arch: "x86_64",
},
models.Package{
Name: "wpa_supplicant",
Version: "1:2.0",
Release: "17.el7_1",
Arch: "x86_64",
},
models.Package{
Name: "yum",
Version: "3.4.3",
Release: "150.el7.centos",
Arch: "noarch",
},
)
var tests = []struct {
in string
out models.Packages
}{
{
` pid proc CPU RSS State uptime
python-2.7.5-34.el7.x86_64 Upgrade 2.7.5-48.el7.x86_64
741 tuned 1:54 16 MB Sleeping: 14 day(s) 21:52:32
38755 yum 0:00 42 MB Running: 00:00
util-linux-2.23.2-26.el7.x86_64 Upgrade 2.23.2-33.el7_3.2.x86_64
626 agetty 0:00 848 kB Sleeping: 14 day(s) 21:52:37
628 agetty 0:00 848 kB Sleeping: 14 day(s) 21:52:37
1:wpa_supplicant-2.0-17.el7_1.x86_64 Upgrade 1:2.0-21.el7_3.x86_64
638 wpa_supplicant 0:00 2.6 MB Sleeping: 14 day(s) 21:52:37
yum-3.4.3-150.el7.centos.noarch
38755 yum 0:00 42 MB Running: 00:00
ps
`,
models.NewPackages(
models.Package{
Name: "python",
Version: "2.7.5",
Release: "34.el7",
Arch: "x86_64",
// NewVersion: "2.7.5-",
// NewRelease: "48.el7.x86_64",
AffectedProcs: []models.AffectedProcess{
{
PID: "741",
Name: "tuned",
},
{
PID: "38755",
Name: "yum",
},
},
},
models.Package{
Name: "util-linux",
Version: "2.23.2",
Release: "26.el7",
Arch: "x86_64",
// NewVersion: "2.7.5",
// NewRelease: "48.el7.x86_64",
AffectedProcs: []models.AffectedProcess{
{
PID: "626",
Name: "agetty",
},
{
PID: "628",
Name: "agetty",
},
},
},
models.Package{
Name: "wpa_supplicant",
Version: "1:2.0",
Release: "17.el7_1",
Arch: "x86_64",
// NewVersion: "1:2.0",
// NewRelease: "21.el7_3.x86_64",
AffectedProcs: []models.AffectedProcess{
{
PID: "638",
Name: "wpa_supplicant",
},
},
},
),
},
{
` pid proc CPU RSS State uptime
acpid-2.0.19-6.7.amzn1.x86_64
2388 acpid 0:00 1.4 MB Sleeping: 21:08
at-3.1.10-48.15.amzn1.x86_64
2546 atd 0:00 164 kB Sleeping: 21:06
cronie-anacron-1.4.4-15.8.amzn1.x86_64
2637 anacron 0:00 1.5 MB Sleeping: 13:14
12:dhclient-4.1.1-51.P1.26.amzn1.x86_64
2061 dhclient 0:00 1.4 MB Sleeping: 21:10
2193 dhclient 0:00 2.1 MB Sleeping: 21:08
mingetty-1.08-5.9.amzn1.x86_64
2572 mingetty 0:00 1.4 MB Sleeping: 21:06
2575 mingetty 0:00 1.4 MB Sleeping: 21:06
2578 mingetty 0:00 1.5 MB Sleeping: 21:06
2580 mingetty 0:00 1.4 MB Sleeping: 21:06
2582 mingetty 0:00 1.4 MB Sleeping: 21:06
2584 mingetty 0:00 1.4 MB Sleeping: 21:06
openssh-server-6.6.1p1-33.66.amzn1.x86_64
2481 sshd 0:00 2.6 MB Sleeping: 21:07
python27-2.7.12-2.120.amzn1.x86_64
2649 yum 0:00 35 MB Running: 00:01
rsyslog-5.8.10-9.26.amzn1.x86_64
2261 rsyslogd 0:00 2.6 MB Sleeping: 21:08
udev-173-4.13.amzn1.x86_64
1528 udevd 0:00 2.5 MB Sleeping: 21:12
1652 udevd 0:00 2.1 MB Sleeping: 21:12
1653 udevd 0:00 2.0 MB Sleeping: 21:12
upstart-0.6.5-13.3.13.amzn1.x86_64
1 init 0:00 2.5 MB Sleeping: 21:13
util-linux-2.23.2-33.28.amzn1.x86_64
2569 agetty 0:00 1.6 MB Sleeping: 21:06
yum-3.4.3-150.70.amzn1.noarch
2649 yum 0:00 35 MB Running: 00:01
`,
models.Packages{},
},
}
for _, tt := range tests {
packages := r.parseYumPS(tt.in)
for name, ePack := range tt.out {
if !reflect.DeepEqual(ePack, packages[name]) {
e := pp.Sprintf("%v", ePack)
a := pp.Sprintf("%v", packages[name])
t.Errorf("expected %s, actual %s", e, a)
}
}
}
}
func TestParseNeedsRestarting(t *testing.T) {
r := newCentOS(config.ServerInfo{})
r.Distro = config.Distro{Family: "centos"}
var tests = []struct {
in string
out []models.NeedRestartProcess
}{
{
`1 : /usr/lib/systemd/systemd --switched-root --system --deserialize 21
437 : /usr/sbin/NetworkManager --no-daemon`,
[]models.NeedRestartProcess{
{
PID: "437",
Path: "/usr/sbin/NetworkManager --no-daemon",
HasInit: true,
},
},
},
}
for _, tt := range tests {
procs := r.parseNeedsRestarting(tt.in)
if !reflect.DeepEqual(tt.out, procs) {
t.Errorf("expected %#v, actual %#v", tt.out, procs)
}
}
}
func TestIsExecScanUsingYum(t *testing.T) {
r := newRHEL(config.ServerInfo{})
var tests = []struct {
modes []byte
family string
out bool
}{
{
modes: []byte{config.Offline},
out: false,
},
{
modes: []byte{},
family: config.CentOS,
out: false,
},
{
family: config.Amazon,
modes: []byte{config.FastRoot},
out: true,
},
{
family: config.Amazon,
modes: []byte{config.Deep},
out: true,
},
}
for i, tt := range tests {
r.Distro.Family = tt.family
mode := config.ScanMode{}
for _, m := range tt.modes {
mode.Set(m)
}
si := r.getServerInfo()
si.Mode = mode
r.setServerInfo(si)
out := r.isExecScanUsingYum()
if out != tt.out {
t.Errorf("[%d] expected %#v, actual %#v", i, tt.out, out)
}
}
}
func TestIsExecFillChangelogs(t *testing.T) {
r := newRHEL(config.ServerInfo{})
var tests = []struct {
modes []byte
family string
out bool
}{
{
modes: []byte{config.Offline},
out: false,
},
{
modes: []byte{config.Deep},
family: config.CentOS,
out: true,
},
{
family: config.Amazon,
modes: []byte{config.Deep},
out: false,
},
{
modes: []byte{config.Deep},
family: config.RedHat,
out: true,
},
}
for i, tt := range tests {
r.Distro.Family = tt.family
mode := config.ScanMode{}
for _, m := range tt.modes {
mode.Set(m)
}
si := r.getServerInfo()
si.Mode = mode
r.setServerInfo(si)
out := r.isExecFillChangelogs()
if out != tt.out {
t.Errorf("[%d] expected %#v, actual %#v", i, tt.out, out)
}
}
}
func TestIsScanChangelogs(t *testing.T) {
r := newCentOS(config.ServerInfo{})
var tests = []struct {
modes []byte
family string
out bool
}{
{
modes: []byte{config.Offline},
out: false,
},
{
modes: []byte{config.Fast},
out: false,
},
{
modes: []byte{config.FastRoot},
out: false,
},
{
modes: []byte{config.Deep},
family: config.RedHat,
out: true,
},
}
for i, tt := range tests {
r.Distro.Family = tt.family
mode := config.ScanMode{}
for _, m := range tt.modes {
mode.Set(m)
}
si := r.getServerInfo()
si.Mode = mode
r.setServerInfo(si)
out := r.isExecScanChangelogs()
if out != tt.out {
t.Errorf("[%d] expected %#v, actual %#v", i, tt.out, out)
}
}
}

159
scan/rhel.go Normal file
View File

@@ -0,0 +1,159 @@
package scan
import (
"fmt"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/util"
)
// inherit OsTypeInterface
type rhel struct {
redhatBase
}
// NewRHEL is constructor
func newRHEL(c config.ServerInfo) *rhel {
r := &rhel{
redhatBase{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
VulnInfos: models.VulnInfos{},
},
},
sudo: rootPrivRHEL{},
},
}
r.log = util.NewCustomLogger(c)
r.setServerInfo(c)
return r
}
func (o *rhel) checkScanMode() error {
return nil
}
func (o *rhel) checkDeps() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckDeps(o.depsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckDeps(o.depsFastRoot())
} else if o.getServerInfo().Mode.IsDeep() {
return o.execCheckDeps(o.depsDeep())
}
return fmt.Errorf("Unknown scan mode")
}
func (o *rhel) depsFast() []string {
return []string{}
}
func (o *rhel) depsFastRoot() []string {
if o.getServerInfo().Mode.IsOffline() {
return []string{}
}
majorVersion, _ := o.Distro.MajorVersion()
switch majorVersion {
case 5:
return []string{
"yum-utils",
"yum-security",
}
case 6:
return []string{
"yum-utils",
"yum-plugin-security",
}
default:
return []string{
"yum-utils",
}
}
}
func (o *rhel) depsDeep() []string {
majorVersion, _ := o.Distro.MajorVersion()
switch majorVersion {
case 5:
return []string{
"yum-utils",
"yum-security",
"yum-changelog",
}
case 6:
return []string{
"yum-utils",
"yum-plugin-security",
"yum-plugin-changelog",
}
default:
return []string{
"yum-utils",
"yum-plugin-changelog",
}
}
}
func (o *rhel) checkIfSudoNoPasswd() error {
if o.getServerInfo().Mode.IsFast() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFast())
} else if o.getServerInfo().Mode.IsFastRoot() {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsFastRoot())
} else {
return o.execCheckIfSudoNoPasswd(o.sudoNoPasswdCmdsDeep())
}
}
func (o *rhel) sudoNoPasswdCmdsFast() []cmd {
return []cmd{}
}
func (o *rhel) sudoNoPasswdCmdsFastRoot() []cmd {
if o.getServerInfo().Mode.IsOffline() {
return []cmd{}
}
majorVersion, _ := o.Distro.MajorVersion()
if majorVersion < 6 {
return []cmd{
// {"needs-restarting", exitStatusZero},
{"yum repolist --color=never", exitStatusZero},
{"yum list-security --security --color=never", exitStatusZero},
{"yum info-security --color=never", exitStatusZero},
{"repoquery -h", exitStatusZero},
}
}
return []cmd{
{"yum repolist --color=never", exitStatusZero},
{"yum updateinfo list updates --security --color=never", exitStatusZero},
{"yum updateinfo updates --security --color=never ", exitStatusZero},
{"repoquery -h", exitStatusZero},
{"needs-restarting", exitStatusZero},
}
}
func (o *rhel) sudoNoPasswdCmdsDeep() []cmd {
return append(o.sudoNoPasswdCmdsFastRoot(),
cmd{"yum changelog all updates --color=never", exitStatusZero})
}
type rootPrivRHEL struct{}
func (o rootPrivRHEL) repoquery() bool {
return true
}
func (o rootPrivRHEL) yumRepolist() bool {
return true
}
func (o rootPrivRHEL) yumUpdateInfo() bool {
return true
}
func (o rootPrivRHEL) yumChangelog() bool {
return true
}

View File

@@ -18,7 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package scan
import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
@@ -30,6 +32,13 @@ import (
"github.com/future-architect/vuls/util"
)
var (
errOSFamilyHeader = errors.New("X-Vuls-OS-Family header is required")
errOSReleaseHeader = errors.New("X-Vuls-OS-Release header is required")
errKernelVersionHeader = errors.New("X-Vuls-Kernel-Version header is required")
errServerNameHeader = errors.New("X-Vuls-Server-Name header is required")
)
var servers, errServers []osTypeInterface
// Base Interface of redhat, debian, freebsd
@@ -41,7 +50,8 @@ type osTypeInterface interface {
detectPlatform()
getPlatform() models.Platform
checkDependencies() error
checkScanMode() error
checkDeps() error
checkIfSudoNoPasswd() error
preCure() error
@@ -49,6 +59,8 @@ type osTypeInterface interface {
scanPackages() error
convertToModel() models.ScanResult
parseInstalledPackages(string) (models.Packages, models.SrcPackages, error)
runningContainers() ([]config.Container, error)
exitedContainers() ([]config.Container, error)
allContainers() ([]config.Container, error)
@@ -119,7 +131,11 @@ func detectOS(c config.ServerInfo) (osType osTypeInterface) {
}
// PrintSSHableServerNames print SSH-able servernames
func PrintSSHableServerNames() {
func PrintSSHableServerNames() bool {
if len(servers) == 0 {
util.Log.Error("No scannable servers")
return false
}
util.Log.Info("Scannable servers are below...")
for _, s := range servers {
if s.getServerInfo().IsContainer() {
@@ -132,6 +148,7 @@ func PrintSSHableServerNames() {
}
}
fmt.Printf("\n")
return true
}
// InitServers detect the kind of OS distribution of target servers
@@ -273,7 +290,7 @@ func detectContainerOSes(timeoutSec int) (actives, inactives []osTypeInterface)
func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeInterface) {
containerHostInfo := containerHost.getServerInfo()
if len(containerHostInfo.Containers.Includes) == 0 {
if len(containerHostInfo.ContainersIncluded) == 0 {
return
}
@@ -285,11 +302,10 @@ func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeIn
return append(oses, containerHost)
}
if containerHostInfo.Containers.Includes[0] == "${running}" {
if containerHostInfo.ContainersIncluded[0] == "${running}" {
for _, containerInfo := range running {
found := false
for _, ex := range containerHost.getServerInfo().Containers.Excludes {
for _, ex := range containerHost.getServerInfo().ContainersExcluded {
if containerInfo.Name == ex || containerInfo.ContainerID == ex {
found = true
}
@@ -319,7 +335,7 @@ func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeIn
}
var exited, unknown []string
for _, container := range containerHostInfo.Containers.Includes {
for _, container := range containerHostInfo.ContainersIncluded {
found := false
for _, c := range running {
if c.ContainerID == container || c.Name == container {
@@ -355,10 +371,21 @@ func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeIn
return oses
}
// CheckScanModes checks scan mode
func CheckScanModes() error {
for _, s := range servers {
if err := s.checkScanMode(); err != nil {
return fmt.Errorf("servers.%s.scanMode err: %s",
s.getServerInfo().GetServerName(), err)
}
}
return nil
}
// CheckDependencies checks dependencies are installed on target servers.
func CheckDependencies(timeoutSec int) {
parallelExec(func(o osTypeInterface) error {
return o.checkDependencies()
return o.checkDeps()
}, timeoutSec)
return
}
@@ -420,15 +447,92 @@ func Scan(timeoutSec int) error {
util.Log.Info("Scanning vulnerable OS packages...")
scannedAt := time.Now()
dir, err := ensureResultDir(scannedAt)
dir, err := EnsureResultDir(scannedAt)
if err != nil {
return err
}
if err := scanVulns(dir, scannedAt, timeoutSec); err != nil {
return err
return scanVulns(dir, scannedAt, timeoutSec)
}
// ViaHTTP scans servers by HTTP header and body
func ViaHTTP(header http.Header, body string) (models.ScanResult, error) {
family := header.Get("X-Vuls-OS-Family")
if family == "" {
return models.ScanResult{}, errOSFamilyHeader
}
return nil
release := header.Get("X-Vuls-OS-Release")
if release == "" {
return models.ScanResult{}, errOSReleaseHeader
}
kernelRelease := header.Get("X-Vuls-Kernel-Release")
if kernelRelease == "" {
util.Log.Warn("If X-Vuls-Kernel-Release is not specified, there is a possibility of false detection")
}
kernelVersion := header.Get("X-Vuls-Kernel-Version")
if family == config.Debian && kernelVersion == "" {
return models.ScanResult{}, errKernelVersionHeader
}
serverName := header.Get("X-Vuls-Server-Name")
if config.Conf.ToLocalFile && serverName == "" {
return models.ScanResult{}, errServerNameHeader
}
distro := config.Distro{
Family: family,
Release: release,
}
kernel := models.Kernel{
Release: kernelRelease,
Version: kernelVersion,
}
base := base{
Distro: distro,
osPackages: osPackages{
Kernel: kernel,
},
log: util.Log,
}
var osType osTypeInterface
switch family {
case config.Debian, config.Ubuntu:
osType = &debian{base: base}
case config.RedHat:
osType = &rhel{
redhatBase: redhatBase{base: base},
}
case config.CentOS:
osType = &centos{
redhatBase: redhatBase{base: base},
}
default:
return models.ScanResult{}, fmt.Errorf("Server mode for %s is not implemented yet", family)
}
installedPackages, srcPackages, err := osType.parseInstalledPackages(body)
if err != nil {
return models.ScanResult{}, err
}
result := models.ScanResult{
ServerName: serverName,
Family: family,
Release: release,
RunningKernel: models.Kernel{
Release: kernelRelease,
Version: kernelVersion,
},
Packages: installedPackages,
SrcPackages: srcPackages,
ScannedCves: models.VulnInfos{},
}
return result, nil
}
func setupChangelogCache() error {
@@ -440,7 +544,7 @@ func setupChangelogCache() error {
break
case config.Ubuntu, config.Debian:
//TODO changelopg cache for RedHat, Oracle, Amazon, CentOS is not implemented yet.
if config.Conf.Deep {
if s.getServerInfo().Mode.IsDeep() {
needToSetupCache = true
}
break
@@ -466,9 +570,13 @@ func scanVulns(jsonDir string, scannedAt time.Time, timeoutSec int) error {
return o.postScan()
}, timeoutSec)
hostname, _ := os.Hostname()
for _, s := range append(servers, errServers...) {
r := s.convertToModel()
r.ScannedAt = scannedAt
r.ScannedVersion = config.Version
r.ScannedRevision = config.Revision
r.ScannedBy = hostname
r.Config.Scan = config.Conf
results = append(results, r)
}
@@ -487,7 +595,8 @@ func scanVulns(jsonDir string, scannedAt time.Time, timeoutSec int) error {
return nil
}
func ensureResultDir(scannedAt time.Time) (currentDir string, err error) {
// EnsureResultDir ensures the directory for scan results
func EnsureResultDir(scannedAt time.Time) (currentDir string, err error) {
jsonDirName := scannedAt.Format(time.RFC3339)
resultsDir := config.Conf.ResultsDir

View File

@@ -1 +1,138 @@
package scan
import (
"net/http"
"testing"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
)
func TestViaHTTP(t *testing.T) {
r := newRHEL(config.ServerInfo{})
r.Distro = config.Distro{Family: config.RedHat}
var tests = []struct {
header map[string]string
body string
packages models.Packages
expectedResult models.ScanResult
wantErr error
}{
{
header: map[string]string{
"X-Vuls-OS-Release": "6.9",
"X-Vuls-Kernel-Release": "2.6.32-695.20.3.el6.x86_64",
},
wantErr: errOSFamilyHeader,
},
{
header: map[string]string{
"X-Vuls-OS-Family": "redhat",
"X-Vuls-Kernel-Release": "2.6.32-695.20.3.el6.x86_64",
},
wantErr: errOSReleaseHeader,
},
{
header: map[string]string{
"X-Vuls-OS-Family": "debian",
"X-Vuls-OS-Release": "8",
"X-Vuls-Kernel-Release": "2.6.32-695.20.3.el6.x86_64",
},
wantErr: errKernelVersionHeader,
},
{
header: map[string]string{
"X-Vuls-OS-Family": "centos",
"X-Vuls-OS-Release": "6.9",
"X-Vuls-Kernel-Release": "2.6.32-695.20.3.el6.x86_64",
},
body: `openssl 0 1.0.1e 30.el6.11 x86_64
Percona-Server-shared-56 1 5.6.19 rel67.0.el6 x84_64
kernel 0 2.6.32 696.20.1.el6 x86_64
kernel 0 2.6.32 696.20.3.el6 x86_64
kernel 0 2.6.32 695.20.3.el6 x86_64`,
expectedResult: models.ScanResult{
Family: "centos",
Release: "6.9",
RunningKernel: models.Kernel{
Release: "2.6.32-695.20.3.el6.x86_64",
},
Packages: models.Packages{
"openssl": models.Package{
Name: "openssl",
Version: "1.0.1e",
Release: "30.el6.11",
},
"Percona-Server-shared-56": models.Package{
Name: "Percona-Server-shared-56",
Version: "1:5.6.19",
Release: "rel67.0.el6",
},
"kernel": models.Package{
Name: "kernel",
Version: "2.6.32",
Release: "695.20.3.el6",
},
},
},
},
{
header: map[string]string{
"X-Vuls-OS-Family": "debian",
"X-Vuls-OS-Release": "8.10",
"X-Vuls-Kernel-Release": "3.16.0-4-amd64",
"X-Vuls-Kernel-Version": "3.16.51-2",
},
body: "",
expectedResult: models.ScanResult{
Family: "debian",
Release: "8.10",
RunningKernel: models.Kernel{
Release: "3.16.0-4-amd64",
Version: "3.16.51-2",
},
},
},
}
for _, tt := range tests {
header := http.Header{}
for k, v := range tt.header {
header.Set(k, v)
}
result, err := ViaHTTP(header, tt.body)
if err != tt.wantErr {
t.Errorf("error: expected %s, actual: %s", tt.wantErr, err)
}
if result.Family != tt.expectedResult.Family {
t.Errorf("os family: expected %s, actual %s", tt.expectedResult.Family, result.Family)
}
if result.Release != tt.expectedResult.Release {
t.Errorf("os release: expected %s, actual %s", tt.expectedResult.Release, result.Release)
}
if result.RunningKernel.Release != tt.expectedResult.RunningKernel.Release {
t.Errorf("kernel release: expected %s, actual %s",
tt.expectedResult.RunningKernel.Release, result.RunningKernel.Release)
}
if result.RunningKernel.Version != tt.expectedResult.RunningKernel.Version {
t.Errorf("kernel version: expected %s, actual %s",
tt.expectedResult.RunningKernel.Version, result.RunningKernel.Version)
}
for name, expectedPack := range tt.expectedResult.Packages {
pack := result.Packages[name]
if pack.Name != expectedPack.Name {
t.Errorf("name: expected %s, actual %s", expectedPack.Name, pack.Name)
}
if pack.Version != expectedPack.Version {
t.Errorf("version: expected %s, actual %s", expectedPack.Version, pack.Version)
}
if pack.Release != expectedPack.Release {
t.Errorf("release: expected %s, actual %s", expectedPack.Release, pack.Release)
}
}
}
}

View File

@@ -13,13 +13,13 @@ import (
// inherit OsTypeInterface
type suse struct {
redhat
redhatBase
}
// NewRedhat is constructor
func newSUSE(c config.ServerInfo) *suse {
r := &suse{
redhat: redhat{
redhatBase: redhatBase{
base: base{
osPackages: osPackages{
Packages: models.Packages{},
@@ -98,7 +98,11 @@ func (o *suse) parseOSRelease(content string) (name string, ver string) {
return name, result[1]
}
func (o *suse) checkDependencies() error {
func (o *suse) checkScanMode() error {
return nil
}
func (o *suse) checkDeps() error {
o.log.Infof("Dependencies... No need")
return nil
}
@@ -122,6 +126,10 @@ func (o *suse) scanPackages() error {
return err
}
o.Kernel.RebootRequired = rebootRequired
if o.getServerInfo().Mode.IsOffline() {
o.Packages = installed
return nil
}
updatable, err := o.scanUpdatablePackages()
if err != nil {
@@ -135,20 +143,20 @@ func (o *suse) scanPackages() error {
}
func (o *suse) rebootRequired() (bool, error) {
r := o.exec("rpm -q --last kernel-default | head -n1", noSudo)
r := o.exec("rpm -q --last kernel-default", noSudo)
if !r.isSuccess() {
return false, fmt.Errorf("Failed to detect the last installed kernel : %v", r)
o.log.Warnf("Failed to detect the last installed kernel : %v", r)
// continue scanning
return false, nil
}
stdout := strings.Fields(r.Stdout)[0]
return !strings.Contains(stdout, strings.TrimSuffix(o.Kernel.Release, "-default")), nil
}
func (o *suse) scanUpdatablePackages() (models.Packages, error) {
cmd := ""
if v, _ := o.Distro.MajorVersion(); v < 12 {
cmd = "zypper -q lu"
} else {
cmd = "zypper --no-color -q lu"
cmd := "zypper -q lu"
if o.hasZypperColorOption() {
cmd = "zypper -q --no-color lu"
}
r := o.exec(cmd, noSudo)
if !r.isSuccess() {
@@ -188,3 +196,9 @@ func (o *suse) parseZypperLUOneLine(line string) (*models.Package, error) {
Arch: fs[10],
}, nil
}
func (o *suse) hasZypperColorOption() bool {
cmd := "zypper --help | grep color"
r := o.exec(util.PrependProxyEnv(cmd), noSudo)
return len(r.Stdout) > 0
}

View File

@@ -17,16 +17,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package scan
import "github.com/future-architect/vuls/models"
// inherit OsTypeInterface
type unknown struct {
base
}
func (o *unknown) checkScanMode() error {
return nil
}
func (o *unknown) checkIfSudoNoPasswd() error {
return nil
}
func (o *unknown) checkDependencies() error {
func (o *unknown) checkDeps() error {
return nil
}
@@ -41,3 +47,7 @@ func (o *unknown) postScan() error {
func (o *unknown) scanPackages() error {
return nil
}
func (o *unknown) parseInstalledPackages(string) (models.Packages, models.SrcPackages, error) {
return nil, nil, nil
}

View File

@@ -70,7 +70,7 @@ func TestIsRunningKernelSUSE(t *testing.T) {
}
func TestIsRunningKernelRedHatLikeLinux(t *testing.T) {
r := newRedhat(config.ServerInfo{})
r := newAmazon(config.ServerInfo{})
r.Distro = config.Distro{Family: config.Amazon}
kernel := models.Kernel{

106
server/server.go Normal file
View File

@@ -0,0 +1,106 @@
/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Architect, Inc. Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package server
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"time"
c "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/scan"
"github.com/future-architect/vuls/util"
)
// VulsHandler is used for vuls server mode
type VulsHandler struct {
DBclient report.DBClient
}
func (h VulsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
result := models.ScanResult{ScannedCves: models.VulnInfos{}}
contentType := r.Header.Get("Content-Type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
util.Log.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if mediatype == "application/json" {
if err = json.NewDecoder(r.Body).Decode(&result); err != nil {
util.Log.Error(err)
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
} else if mediatype == "text/plain" {
buf := new(bytes.Buffer)
io.Copy(buf, r.Body)
if result, err = scan.ViaHTTP(r.Header, buf.String()); err != nil {
util.Log.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
util.Log.Error(mediatype)
http.Error(w, fmt.Sprintf("Invalid Content-Type: %s", contentType), http.StatusUnsupportedMediaType)
return
}
if err := report.FillCveInfo(h.DBclient, &result, []string{}); err != nil {
util.Log.Error(err)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
// report
reports := []report.ResultWriter{
report.HTTPResponseWriter{Writer: w},
}
if c.Conf.ToLocalFile {
scannedAt := result.ScannedAt
if scannedAt.IsZero() {
scannedAt = time.Now().Truncate(1 * time.Hour)
}
dir, err := scan.EnsureResultDir(scannedAt)
if err != nil {
util.Log.Errorf("Failed to ensure the result directory: %s", err)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
reports = append(reports, report.LocalFileWriter{
CurrentDir: dir,
})
}
for _, w := range reports {
if err := w.Write(result); err != nil {
util.Log.Errorf("Failed to report: %s", err)
return
}
}
}

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package util
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -32,6 +33,13 @@ import (
// Log for localhsot
var Log *logrus.Entry
func init() {
log := logrus.New()
log.Out = ioutil.Discard
fields := logrus.Fields{"prefix": ""}
Log = log.WithFields(fields)
}
// NewCustomLogger creates logrus
func NewCustomLogger(c config.ServerInfo) *logrus.Entry {
log := logrus.New()
@@ -50,7 +58,7 @@ func NewCustomLogger(c config.ServerInfo) *logrus.Entry {
if _, err := os.Stat(logDir); os.IsNotExist(err) {
if err := os.Mkdir(logDir, 0700); err != nil {
log.Errorf("Failed to create log directory: %s", err)
log.Errorf("Failed to create log directory. path: %s, err: %s", logDir, err)
}
}

View File

@@ -34,7 +34,7 @@ func GenWorkers(num int) chan<- func() {
defer func() {
if p := recover(); p != nil {
log := NewCustomLogger(config.ServerInfo{})
log.Debugf("Panic: %s", p)
log.Errorf("run time panic: %v", p)
}
}()
for f := range tasks {
@@ -70,7 +70,7 @@ func URLPathJoin(baseURL string, paths ...string) (string, error) {
if err != nil {
return "", err
}
url.Path += strings.Join(trimedPaths, "/")
url.Path += "/" + strings.Join(trimedPaths, "/")
return url.String(), nil
}