From 203c54c6b3b8edf3d7785c27ffe1b235e66a892d Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 17 Nov 2016 14:24:31 +0900 Subject: [PATCH] Add report subcommand, change scan options. Bump up ver #239 --- Makefile | 2 +- README.ja.md | 412 ++++++++++++++---------- README.md | 464 ++++++++++++++++----------- commands/configtest.go | 2 +- commands/discover.go | 2 +- commands/history.go | 9 +- commands/prepare.go | 5 +- commands/report.go | 386 ++++++++++++++++++++++ commands/scan.go | 249 +-------------- commands/tui.go | 109 +++++-- commands/util.go | 225 +++++++++++++ config/config.go | 123 +++++-- config/tomlloader.go | 4 +- cveapi/cve_client.go | 13 +- glide.lock | 14 +- glide.yaml | 6 +- img/vuls-architecture.graphml | 495 ++++++++++++++++++++++------- img/vuls-architecture.png | Bin 78951 -> 92769 bytes img/vuls-scan-flow.png | Bin 83283 -> 74102 bytes main.go | 3 +- models/models.go | 249 +++++++++++---- models/models_test.go | 87 ++++- report/azureblob.go | 153 ++++----- report/{mail.go => email.go} | 50 ++- report/json.go | 150 --------- report/localfile.go | 111 +++++++ report/logrus.go | 56 ---- report/s3.go | 125 +++++--- report/slack.go | 30 +- report/stdout.go | 33 +- report/textfile.go | 64 ---- report/tui.go | 46 +-- report/util.go | 214 ++++++++----- report/writer.go | 27 +- report/xml.go | 54 ---- scan/base.go | 97 +----- scan/debian.go | 79 +++-- scan/freebsd.go | 33 +- scan/redhat.go | 97 +++--- scan/serverapi.go | 142 ++++----- scan/serverapi_test.go | 157 --------- setup/docker/README.md | 18 +- setup/docker/vuls/latest/README.md | 19 +- 43 files changed, 2698 insertions(+), 1916 deletions(-) create mode 100644 commands/report.go create mode 100644 commands/util.go rename report/{mail.go => email.go} (62%) delete mode 100644 report/json.go create mode 100644 report/localfile.go delete mode 100644 report/logrus.go delete mode 100644 report/textfile.go delete mode 100644 report/xml.go diff --git a/Makefile b/Makefile index f47ffa49c3..f30593ca4d 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ fmtcheck: pretest: lint vet fmtcheck test: pretest - $(foreach pkg,$(PKGS),go test -v $(pkg) || exit;) + $(foreach pkg,$(PKGS),go test -cover -v $(pkg) || exit;) unused : $(foreach pkg,$(PKGS),unused $(pkg);) diff --git a/README.ja.md b/README.ja.md index c659953644..7a4069ca00 100644 --- a/README.ja.md +++ b/README.ja.md @@ -89,6 +89,7 @@ Hello Vulsチュートリアルでは手動でのセットアップ方法で説 1. 設定 1. Prepare 1. Scan +1. Reporting 1. TUI(Terminal-Based User Interface)で結果を参照する 1. Web UI([VulsRepo](https://github.com/usiusi360/vulsrepo))で結果を参照する @@ -125,7 +126,7 @@ Vulsセットアップに必要な以下のソフトウェアをインストー - SQLite3 or MySQL - git - gcc -- go v1.7.1 or later +- go v1.7.1 or later (The latest version is recommended) - https://golang.org/doc/install ```bash @@ -203,6 +204,7 @@ Vulsの設定ファイルを作成する(TOMLフォーマット) 設定ファイルのチェックを行う ``` +$ cd $HOME $ cat config.toml [servers] @@ -224,42 +226,82 @@ $ vuls prepare ## Step8. Start Scanning + +``` +$ vuls scan +... snip ... + +Scan Summary +============ +172-31-4-82 amazon 2015.09 94 CVEs 103 updatable packages + +``` + +## Step9. Reporting + +View one-line summary + +``` +$ vuls report -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 + +One Line Summary +================ +172-31-4-82 Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages + +``` + +View short summary. + ``` -$ vuls scan -cve-dictionary-dbpath=$PWD/cve.sqlite3 -report-json -INFO[0000] Start scanning (config: /home/ec2-user/config.toml) -INFO[0000] Start scanning -INFO[0000] config: /home/ec2-user/config.toml -INFO[0000] cve-dictionary: /home/ec2-user/cve.sqlite3 +$ vuls report -format-short-text -cvedb-path=$PWD/cve.sqlite3 +172-31-4-8 (amazon 2015.09) +=========================== +Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages + +CVE-2016-0705 10.0 (High) Double free vulnerability in the dsa_priv_decode function in + crypto/dsa/dsa_ameth.c in OpenSSL 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g + allows remote attackers to cause a denial of service (memory corruption) or + possibly have unspecified other impact via a malformed DSA private key. + http://www.cvedetails.com/cve/CVE-2016-0705 + http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-0705 + libssl1.0.0-1.0.2f-2ubuntu1 -> libssl1.0.0-1.0.2g-1ubuntu4.5 + openssl-1.0.2f-2ubuntu1 -> openssl-1.0.2g-1ubuntu4.5 ... snip ... +```` + +View full report. + +``` +$ vuls report -format-full-text -cvedb-path=$PWD/cve.sqlite3 172-31-4-82 (amazon 2015.09) ============================ -CVE-2016-0494 10.0 Unspecified vulnerability in the Java SE and Java SE Embedded components in Oracle - Java SE 6u105, 7u91, and 8u66 and Java SE Embedded 8u65 allows remote attackers to - affect confidentiality, integrity, and availability via unknown vectors related to - 2D. -... snip ... +Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages -CVE-2016-0494 + +CVE-2016-0705 ------------- Score 10.0 (High) Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Summary Unspecified vulnerability in the Java SE and Java SE Embedded components in Oracle Java SE 6u105, - 7u91, and 8u66 and Java SE Embedded 8u65 allows remote attackers to affect confidentiality, - integrity, and availability via unknown vectors related to 2D. -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-0494 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0494 -CVE Details http://www.cvedetails.com/cve/CVE-2016-0494 -CVSS Calculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-0494&vector=(AV:N/AC:L/Au:N/C:C/I:C/A:C) -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-0494 -ALAS-2016-643 https://alas.aws.amazon.com/ALAS-2016-643.html -Package/CPE java-1.7.0-openjdk-1.7.0.91-2.6.2.2.63.amzn1 -> java-1.7.0-openjdk-1:1.7.0.95-2.6.4.0.65.amzn1 +Summary Double free vulnerability in the dsa_priv_decode function in + crypto/dsa/dsa_ameth.c in OpenSSL 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g + allows remote attackers to cause a denial of service (memory corruption) or + possibly have unspecified other impact via a malformed DSA private key. +CWE https://cwe.mitre.org/data/definitions/.html +NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-0705 +MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0705 +CVE Details http://www.cvedetails.com/cve/CVE-2016-0705 +CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-0705&vector=(AV:N/AC:L/... +Ubuntu-CVE http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-0705 +Package libssl1.0.0-1.0.2f-2ubuntu1 -> libssl1.0.0-1.0.2g-1ubuntu4.5 + openssl-1.0.2f-2ubuntu1 -> openssl-1.0.2g-1ubuntu4.5 +... snip ... ``` -## Step9. TUI +## Step10. TUI Vulsにはスキャン結果の詳細を参照できるイカしたTUI(Terminal-Based User Interface)が付属している。 @@ -269,7 +311,7 @@ $ vuls tui ![Vuls-TUI](img/hello-vuls-tui.png) -## Step10. Web UI +## Step11. Web UI [VulsRepo](https://github.com/usiusi360/vulsrepo)はスキャン結果をビボットテーブルのように分析可能にするWeb UIである。 [Online Demo](http://usiusi360.github.io/vulsrepo/)があるので試してみて。 @@ -369,7 +411,7 @@ iconEmoji = ":ghost:" authUser = "username" notifyUsers = ["@username"] -[mail] +[email] smtpAddr = "smtp.gmail.com" smtpPort = "587" user = "username" @@ -450,7 +492,7 @@ host = "172.31.4.82" - Mail section ``` - [mail] + [email] smtpAddr = "smtp.gmail.com" smtpPort = "587" user = "username" @@ -571,7 +613,7 @@ Prepareサブコマンドは、Vuls内部で利用する以下のパッケージ | CentOS | 5| yum-changelog | | CentOS | 6, 7| yum-plugin-changelog | | Amazon | All | - | -| RHEL | 4, 5, 6, 7 | - | +| RHEL | 6, 7 | - | | FreeBSD | 10 | - | @@ -603,90 +645,31 @@ prepare: $ vuls scan -help scan: scan - [-lang=en|ja] [-config=/path/to/config.toml] [-results-dir=/path/to/results] - [-cve-dictionary-dbtype=sqlite3|mysql] - [-cve-dictionary-dbpath=/path/to/cve.sqlite3 or mysql connection string] - [-cve-dictionary-url=http://127.0.0.1:1323] - [-cache-dbpath=/path/to/cache.db] - [-cvss-over=7] - [-ignore-unscored-cves] + [-cachedb-path=/path/to/cache.db] [-ssh-external] [-containers-only] [-skip-broken] - [-report-azure-blob] - [-report-json] - [-report-mail] - [-report-s3] - [-report-slack] - [-report-text] - [-report-xml] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] [-debug] - [-debug-sql] - [-aws-profile=default] - [-aws-region=us-west-2] - [-aws-s3-bucket=bucket_name] - [-azure-account=accout] - [-azure-key=key] - [-azure-container=container] - [SERVER]... - + [SERVER]... -ask-key-password Ask ssh privatekey password before scanning - -aws-profile string - AWS Profile to use (default "default") - -aws-region string - AWS Region to use (default "us-east-1") - -aws-s3-bucket string - S3 bucket name - -azure-account string - Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified - -azure-container string - Azure storage container name - -azure-key string - Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified - -cache-dbpath string - /path/to/cache.db (local cache of changelog for Ubuntu/Debian) (default "$PWD/cache.db") + -cachedb-path string + /path/to/cache.db (local cache of changelog for Ubuntu/Debian) -config string - /path/to/toml (default "$PWD/config.toml") + /path/to/toml -containers-only - Scan concontainers Only. Default: Scan both of hosts and containers - -cve-dictionary-dbpath string - /path/to/sqlite3 (For get cve detail from cve.sqlite3) - -cve-dictionary-dbtype string - DB type for fetching CVE dictionary (sqlite3 or mysql) (default "sqlite3") - -cve-dictionary-url string - http://CVE.Dictionary (default "http://127.0.0.1:1323") - -cvss-over float - -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) + Scan containers only. Default: Scan both of hosts and containers -debug debug mode - -debug-sql - SQL debug mode -http-proxy string http://proxy-url:port (default: empty) - -ignore-unscored-cves - Don't report the unscored CVEs - -lang string - [en|ja] (default "en") - -report-json - Write report to JSON files ($PWD/results/current) - -report-mail - Send report via Email - -report-s3 - Write report to S3 (bucket/yyyyMMdd_HHmm) - -report-slack - Send report via Slack - -report-text - Write report to text files ($PWD/results/current) - -report-xml - Write report to XML files ($PWDresults/current) -results-dir string - /path/to/results (default "$PWD/results") + /path/to/results -skip-broken [For CentOS] yum update changelog with --skip-broken option -ssh-external @@ -714,38 +697,167 @@ Defaults:vuls !requiretty | empty password | - | | | with password | required | or use ssh-agent | -## -report-json , -report-text , -report-xml option - -結果をファイルに出力したい場合に指定する。出力先は、`$PWD/result/current/` -`servername.(json|txt|xml)`には、サーバごとのスキャン結果が出力される。 - ## Example: Scan all servers defined in config file ``` -$ vuls scan \ - -report-slack \ - -report-mail \ - -cvss-over=7 \ - -ask-key-password \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 +$ vuls scan -ask-key-password ``` この例では、 - SSH公開鍵認証(秘密鍵パスフレーズ)を指定 - configに定義された全サーバをスキャン -- レポートをslack, emailに送信 -- CVSSスコアが 7.0 以上の脆弱性のみレポート -- go-cve-dictionaryにはHTTPではなくDBに直接アクセス(go-cve-dictionaryをサーバモードで起動しない) ## Example: Scan specific servers ``` -$ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ - server1 server2 +$ vuls scan server1 server2 ``` この例では、 - SSH公開鍵認証(秘密鍵パスフレーズなし) - ノーパスワードでsudoが実行可能 - configで定義されているサーバの中の、server1, server2のみスキャン +## Example: Scan Docker containers + +DockerコンテナはSSHデーモンを起動しないで運用するケースが一般的。 + [Docker Blog:Why you don't need to run SSHd in your Docker containers](https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/) + +Vulsは、DockerホストにSSHで接続し、`docker exec`でDockerコンテナにコマンドを発行して脆弱性をスキャンする。 +詳細は、[Architecture section](https://github.com/future-architect/vuls#architecture)を参照 + +- 全ての起動中のDockerコンテナをスキャン + `"${running}"` をcontainersに指定する + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["${running}"] + ``` + +- あるコンテナのみスキャン + コンテナID、または、コンテナ名を、containersに指定する。 + 以下の例では、`container_name_a`と、`4aa37a8b63b9`のコンテナのみスキャンする + スキャン実行前に、コンテナが起動中か確認すること。もし起動してない場合はエラーメッセージを出力してスキャンを中断する。 + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["container_name_a", "4aa37a8b63b9"] + ``` +- コンテナのみをスキャンする場合(ホストはスキャンしない) + --containers-onlyオプションを指定する + + +# Usage: Report + +``` +report: + report + [-lang=en|ja] + [-config=/path/to/config.toml] + [-results-dir=/path/to/results] + [-refresh-cve] + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-cvss-over=7] + [-ignore-unscored-cves] + [-to-email] + [-to-slack] + [-to-localfile] + [-to-s3] + [-to-azure-blob] + [-format-json] + [-format-xml] + [-format-one-line-text] + [-format-short-text] + [-format-full-text] + [-gzip] + [-aws-profile=default] + [-aws-region=us-west-2] + [-aws-s3-bucket=bucket_name] + [-azure-account=accout] + [-azure-key=key] + [-azure-container=container] + [-http-proxy=http://192.168.0.1:8080] + [-debug] + [-debug-sql] + + [SERVER]... + -aws-profile string + AWS profile to use (default "default") + -aws-region string + AWS region to use (default "us-east-1") + -aws-s3-bucket string + S3 bucket name + -azure-account string + Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified + -azure-container string + Azure storage container name + -azure-key string + Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified + -config string + /path/to/toml + -cvedb-path string + /path/to/sqlite3 (For get cve detail from cve.sqlite3) + -cvedb-type string + DB type for fetching CVE dictionary (sqlite3 or mysql) (default "sqlite3") + -cvedb-url string + http://cve-dictionary.com:8080 or mysql connection string + -cvss-over float + -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) + -debug + debug mode + -debug-sql + SQL debug mode + -format-full-text + Detail report in plain text + -format-json + JSON format + -format-one-line-text + One line summary in plain text + -format-short-text + Summary in plain text + -format-xml + XML format + -gzip + gzip compression + -http-proxy string + http://proxy-url:port (default: empty) + -ignore-unscored-cves + Don't report the unscored CVEs + -lang string + [en|ja] (default "en") + -refresh-cve + Refresh CVE information in JSON file under results dir + -results-dir string + /path/to/results + -to-azure-blob + Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json/xml/txt) + -to-email + Send report via Email + -to-localfile + Write report to localfile + -to-s3 + Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/xml/txt) + -to-slack + Send report via Slack +``` + +## Example: Send scan results to Slack +``` +$ vuls report \ + -to-slack \ + -cvss-over=7 \ + -cvedb-path=$PWD/cve.sqlite3 +``` +With this sample command, it will .. +- Slack通知 +- CVSS score が 7.0以上のもののみ通知 + ## Example: Put results in S3 bucket 事前にAWS関連の設定を行う @@ -755,15 +867,14 @@ $ vuls scan \ ``` $ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ - -report-s3 + -cvedb-path=$PWD/cve.sqlite3 \ + -to-s3 \ + -format-json \ -aws-region=ap-northeast-1 \ -aws-s3-bucket=vuls \ -aws-profile=default ``` この例では、 -- SSH公開鍵認証(秘密鍵パスフレーズなし) -- configに定義された全サーバをスキャン - 結果をJSON形式でS3に格納する。 - バケット名 ... vuls - リージョン ... ap-northeast-1 @@ -772,20 +883,19 @@ $ vuls scan \ ## Example: Put results in Azure Blob storage 事前にAzure Blob関連の設定を行う -- Containerを作成 +- Azure Blob Containerを作成 ``` $ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ - -report-azure-blob \ + -cvedb-path=$PWD/cve.sqlite3 \ + -to-azure-blob \ + -format-xml \ -azure-container=vuls \ -azure-account=test \ -azure-key=access-key-string ``` この例では、 -- SSH公開鍵認証(秘密鍵パスフレーズなし) -- configに定義された全サーバをスキャン -- 結果をJSON形式でAzure Blobに格納する。 +- 結果をXML形式でBlobに格納する。 - コンテナ名 ... vuls - ストレージアカウント名 ... test - アクセスキー ... access-key-string @@ -802,7 +912,7 @@ $ vuls scan \ ## Example: IgnoreCves -Slack, Mail, テキスト出力しないくないCVE IDがある場合は、設定ファイルに定義することでレポートされなくなる。 +Slack, EMail, テキスト出力しないくないCVE IDがある場合は、設定ファイルに定義することでレポートされなくなる。 ただ、JSONファイルには以下のように出力される。 - config.toml @@ -938,43 +1048,6 @@ VulsとDependency Checkの連携すると以下の利点がある - Dependency Checkは日本語レポートに対応していない -# Usage: Scan Docker containers - -DockerコンテナはSSHデーモンを起動しないで運用するケースが一般的。 - [Docker Blog:Why you don't need to run SSHd in your Docker containers](https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/) - -Vulsは、DockerホストにSSHで接続し、`docker exec`でDockerコンテナにコマンドを発行して脆弱性をスキャンする。 -詳細は、[Architecture section](https://github.com/future-architect/vuls#architecture)を参照 - -- 全ての起動中のDockerコンテナをスキャン - `"${running}"` をcontainersに指定する - ``` - [servers] - - [servers.172-31-4-82] - host = "172.31.4.82" - user = "ec2-user" - keyPath = "/home/username/.ssh/id_rsa" - containers = ["${running}"] - ``` - -- あるコンテナのみスキャン - コンテナID、または、コンテナ名を、containersに指定する。 - 以下の例では、`container_name_a`と、`4aa37a8b63b9`のコンテナのみスキャンする - スキャン実行前に、コンテナが起動中か確認すること。もし起動してない場合はエラーメッセージを出力してスキャンを中断する。 - ``` - [servers] - - [servers.172-31-4-82] - host = "172.31.4.82" - user = "ec2-user" - keyPath = "/home/username/.ssh/id_rsa" - containers = ["container_name_a", "4aa37a8b63b9"] - ``` -- コンテナのみをスキャンする場合(ホストはスキャンしない) - --containers-onlyオプションを指定する - - # Usage: TUI ## Display the latest scan results @@ -982,13 +1055,26 @@ Vulsは、DockerホストにSSHで接続し、`docker exec`でDockerコンテナ ``` $ vuls tui -h tui: - tui [-results-dir=/path/to/results] + tui + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-results-dir=/path/to/results] + [-refresh-cve] + [-debug-sql] - -results-dir string - /path/to/results (default "$PWD/results") + -cvedb-path string + /path/to/sqlite3 (For get cve detail from cve.sqlite3) + -cvedb-type string + DB type for fetching CVE dictionary (sqlite3 or mysql) + -cvedb-url string + http://cve-dictionary.com:8080 or mysql connection string -debug-sql - debug SQL - + debug SQL + -refresh-cve + Refresh CVE information in JSON file under results dir + -results-dir string + /path/to/results ``` Key binding is below. diff --git a/README.md b/README.md index 0a28a6e6ea..48715b4b18 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Vuls is a tool created to solve the problems listed above. It has the following - Auto generation of configuration file template - Auto detection of servers set using CIDR, generate configuration file template - Email and Slack notification is possible (supports Japanese language) -- Scan result is viewable on accessory software, TUI Viewer terminal or Web UI ([VulsRepo](https://github.com/usiusi360/vulsrepo)). +- Scan result is viewable on accessory software, TUI Viewer on terminal or Web UI ([VulsRepo](https://github.com/usiusi360/vulsrepo)). ---- @@ -69,16 +69,13 @@ Vuls is a tool created to solve the problems listed above. It has the following # Setup Vuls -There are 3 ways to setup Vuls. +There are 2 ways to setup Vuls. - Docker container Dockernized-Vuls with vulsrepo UI in it. You can run install and run Vuls on your machine with only a few commands. see https://github.com/future-architect/vuls/tree/master/setup/docker -- Chef -see https://github.com/sadayuki-matsuno/vuls-cookbook - - Manually Hello Vuls Tutorial shows how to setup vuls manually. @@ -97,6 +94,7 @@ This can be done in the following steps. 1. Configuration 1. Prepare 1. Scan +1. Reporting 1. TUI(Terminal-Based User Interface) 1. Web UI ([VulsRepo](https://github.com/usiusi360/vulsrepo)) @@ -133,7 +131,7 @@ Vuls requires the following packages. - SQLite3 or MySQL - git - gcc -- go v1.7.1 or later +- go v1.7.1 or later (The latest version is recommended) - https://golang.org/doc/install ```bash @@ -200,6 +198,7 @@ Create a config file(TOML format). Then check the config. ``` +$ cd $HOME $ cat config.toml [servers] @@ -222,51 +221,90 @@ see [Usage: Prepare](https://github.com/future-architect/vuls#usage-prepare) ## Step8. Start Scanning ``` -$ vuls scan -cve-dictionary-dbpath=$PWD/cve.sqlite3 -report-json -INFO[0000] Start scanning (config: /home/ec2-user/config.toml) -INFO[0000] Start scanning -INFO[0000] config: /home/ec2-user/config.toml -INFO[0000] cve-dictionary: /home/ec2-user/cve.sqlite3 +$ vuls scan +... snip ... + +Scan Summary +============ +172-31-4-82 amazon 2015.09 94 CVEs 103 updatable packages + +``` + +## Step9. Reporting + +View one-line summary + +``` +$ vuls report -format-one-line-text -cvedb-path=$PWD/cve.sqlite3 + +One Line Summary +================ +172-31-4-82 Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages + +``` + +View short summary. + +``` +$ vuls report -format-short-text +172-31-4-8 (amazon 2015.09) +=========================== +Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages + +CVE-2016-0705 10.0 (High) Double free vulnerability in the dsa_priv_decode function in + crypto/dsa/dsa_ameth.c in OpenSSL 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g + allows remote attackers to cause a denial of service (memory corruption) or + possibly have unspecified other impact via a malformed DSA private key. + http://www.cvedetails.com/cve/CVE-2016-0705 + http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-0705 + libssl1.0.0-1.0.2f-2ubuntu1 -> libssl1.0.0-1.0.2g-1ubuntu4.5 + openssl-1.0.2f-2ubuntu1 -> openssl-1.0.2g-1ubuntu4.5 ... snip ... +```` + +View full report. + +``` +$ vuls report -format-full-text 172-31-4-82 (amazon 2015.09) ============================ -CVE-2016-0494 10.0 Unspecified vulnerability in the Java SE and Java SE Embedded components in Oracle - Java SE 6u105, 7u91, and 8u66 and Java SE Embedded 8u65 allows remote attackers to - affect confidentiality, integrity, and availability via unknown vectors related to - 2D. -... snip ... +Total: 94 (High:19 Medium:54 Low:7 ?:14) 103 updatable packages -CVE-2016-0494 + +CVE-2016-0705 ------------- Score 10.0 (High) Vector (AV:N/AC:L/Au:N/C:C/I:C/A:C) -Summary Unspecified vulnerability in the Java SE and Java SE Embedded components in Oracle Java SE 6u105, - 7u91, and 8u66 and Java SE Embedded 8u65 allows remote attackers to affect confidentiality, - integrity, and availability via unknown vectors related to 2D. -NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-0494 -MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0494 -CVE Details http://www.cvedetails.com/cve/CVE-2016-0494 -CVSS Calculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-0494&vector=(AV:N/AC:L/Au:N/C:C/I:C/A:C) -RHEL-CVE https://access.redhat.com/security/cve/CVE-2016-0494 -ALAS-2016-643 https://alas.aws.amazon.com/ALAS-2016-643.html -Package/CPE java-1.7.0-openjdk-1.7.0.91-2.6.2.2.63.amzn1 -> java-1.7.0-openjdk-1:1.7.0.95-2.6.4.0.65.amzn1 +Summary Double free vulnerability in the dsa_priv_decode function in + crypto/dsa/dsa_ameth.c in OpenSSL 1.0.1 before 1.0.1s and 1.0.2 before 1.0.2g + allows remote attackers to cause a denial of service (memory corruption) or + possibly have unspecified other impact via a malformed DSA private key. +CWE https://cwe.mitre.org/data/definitions/.html +NVD https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-0705 +MITRE https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0705 +CVE Details http://www.cvedetails.com/cve/CVE-2016-0705 +CVSS Claculator https://nvd.nist.gov/cvss/v2-calculator?name=CVE-2016-0705&vector=(AV:N/AC:L/... +Ubuntu-CVE http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-0705 +Package libssl1.0.0-1.0.2f-2ubuntu1 -> libssl1.0.0-1.0.2g-1ubuntu4.5 + openssl-1.0.2f-2ubuntu1 -> openssl-1.0.2g-1ubuntu4.5 +... snip ... ``` -## Step9. TUI +## Step10. TUI Vuls has Terminal-Based User Interface to display the scan result. ``` -$ vuls tui +$ vuls tui ``` ![Vuls-TUI](img/hello-vuls-tui.png) -## Step10. Web UI +## Step11. Web UI [VulsRepo](https://github.com/usiusi360/vulsrepo) is a awesome Web UI for Vuls. Check it out the [Online Demo](http://usiusi360.github.io/vulsrepo/). @@ -288,11 +326,8 @@ see https://github.com/future-architect/vuls/tree/master/setup/docker ## Scanning Flow ![Vuls-Scan-Flow](img/vuls-scan-flow.png) -- Scan vulnerabilities on the servers via SSH and create a list of the CVE ID +- Scan vulnerabilities on the servers via SSH and collect a list of the CVE ID - To scan Docker containers, Vuls connect via ssh to the Docker host and then `docker exec` to the containers. So, no need to run sshd daemon on the containers. -- Fetch more detailed information of the detected CVE from go-cve-dictionary -- Send a report by Slack and Email -- Write scan results to JSON file to show the latest report on your terminal ---- # Performance Considerations @@ -323,16 +358,20 @@ High speed scan and resource usage is light because Vuls can get CVE IDs by usin # Use Cases -## Scan all servers +## Scan All Servers ![Vuls-Usecase1](img/vuls-usecase-elb-rails-rds-all.png) -## Scan a single server +## Scan a Single Server web/app server in the same configuration under the load balancer ![Vuls-Usecase2](img/vuls-usecase-elb-rails-rds-single.png) +## Scan Staging Environment + +If there is a staging environment with the same configuration as the production environment, you can scan the server in staging environment + ---- # Support OS @@ -373,7 +412,7 @@ iconEmoji = ":ghost:" authUser = "username" notifyUsers = ["@username"] -[mail] +[email] smtpAddr = "smtp.gmail.com" smtpPort = "587" user = "username" @@ -457,9 +496,9 @@ You can customize your configuration using this template. If you set `["@foo", "@bar"]` to notifyUsers, @foo @bar will be included in text. So @foo, @bar can receive mobile push notifications on their smartphone. -- Mail section +- EMail section ``` - [mail] + [email] smtpAddr = "smtp.gmail.com" smtpPort = "587" user = "username" @@ -577,7 +616,7 @@ Prepare subcommand installs required packages on each server. | CentOS | 5| yum-changelog | | CentOS | 6, 7| yum-plugin-changelog | | Amazon | All | - | -| RHEL | 4, 5, 6, 7 | - | +| RHEL | 6, 7 | - | | FreeBSD | 10 | - | @@ -610,94 +649,34 @@ prepare: # Usage: Scan ``` - $ vuls scan -help scan: scan - [-lang=en|ja] [-config=/path/to/config.toml] [-results-dir=/path/to/results] - [-cve-dictionary-dbtype=sqlite3|mysql] - [-cve-dictionary-dbpath=/path/to/cve.sqlite3 or mysql connection string] - [-cve-dictionary-url=http://127.0.0.1:1323] - [-cache-dbpath=/path/to/cache.db] - [-cvss-over=7] - [-ignore-unscored-cves] + [-cachedb-path=/path/to/cache.db] [-ssh-external] [-containers-only] [-skip-broken] - [-report-azure-blob] - [-report-json] - [-report-mail] - [-report-s3] - [-report-slack] - [-report-text] - [-report-xml] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] [-debug] - [-debug-sql] - [-aws-profile=default] - [-aws-region=us-west-2] - [-aws-s3-bucket=bucket_name] - [-azure-account=accout] - [-azure-key=key] - [-azure-container=container] - [SERVER]... - + [SERVER]... -ask-key-password Ask ssh privatekey password before scanning - -aws-profile string - AWS Profile to use (default "default") - -aws-region string - AWS Region to use (default "us-east-1") - -aws-s3-bucket string - S3 bucket name - -azure-account string - Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified - -azure-container string - Azure storage container name - -azure-key string - Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified - -cache-dbpath string - /path/to/cache.db (local cache of changelog for Ubuntu/Debian) (default "$PWD/cache.db") + -cachedb-path string + /path/to/cache.db (local cache of changelog for Ubuntu/Debian) -config string - /path/to/toml (default "$PWD/config.toml") + /path/to/toml -containers-only - Scan concontainers Only. Default: Scan both of hosts and containers - -cve-dictionary-dbpath string - /path/to/sqlite3 (For get cve detail from cve.sqlite3) - -cve-dictionary-dbtype string - DB type for fetching CVE dictionary (sqlite3 or mysql) (default "sqlite3") - -cve-dictionary-url string - http://CVE.Dictionary (default "http://127.0.0.1:1323") - -cvss-over float - -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) + Scan containers only. Default: Scan both of hosts and containers -debug debug mode - -debug-sql - SQL debug mode -http-proxy string http://proxy-url:port (default: empty) - -ignore-unscored-cves - Don't report the unscored CVEs - -lang string - [en|ja] (default "en") - -report-json - Write report to JSON files ($PWD/results/current) - -report-mail - Send report via Email - -report-s3 - Write report to S3 (bucket/yyyyMMdd_HHmm) - -report-slack - Send report via Slack - -report-text - Write report to text files ($PWD/results/current) - -report-xml - Write report to XML files ($PWDresults/current) -results-dir string - /path/to/results (default "$PWD/results") + /path/to/results -skip-broken [For CentOS] yum update changelog with --skip-broken option -ssh-external @@ -726,73 +705,200 @@ Defaults:vuls !requiretty | empty password | - | | | with password | required | or use ssh-agent | -## -report-json , -report-text , -report-xml option - -At the end of the scan, scan results will be available in the `$PWD/result/current/` directory. -`servername.(json|txt|xml)` includes the scan result of the server. - ## Example: Scan all servers defined in config file ``` -$ vuls scan \ - --report-slack \ - --report-mail \ - --cvss-over=7 \ - -ask-key-password \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 +$ vuls scan -ask-key-password ``` With this sample command, it will .. - Ask SSH key password before scanning - Scan all servers defined in config file -- Send scan results to slack and email -- Only Report CVEs that CVSS score is over 7 -- Print scan result to terminal ## Example: Scan specific servers ``` -$ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ - server1 server2 +$ vuls scan server1 server2 ``` With this sample command, it will .. - Use SSH Key-Based authentication with empty password (without -ask-key-password option) - Scan only 2 servers (server1, server2) -- Print scan result to terminal + +## Example: Scan Docker containers + +It is common that keep Docker containers running without SSHd daemon. +see [Docker Blog:Why you don't need to run SSHd in your Docker containers](https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/) + +Vuls scans Docker containers via `docker exec` instead of SSH. +For more details, see [Architecture section](https://github.com/future-architect/vuls#architecture) + +- To scan all of running containers + `"${running}"` needs to be set in the containers item. + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["${running}"] + ``` + +- To scan specific containers + The container ID or container name needs to be set in the containers item. + In the following example, only `container_name_a` and `4aa37a8b63b9` will be scanned. + Be sure to check these containers are running state before scanning. + If specified containers are not running, Vuls gives up scanning with printing error message. + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["container_name_a", "4aa37a8b63b9"] + ``` +- To scan containers only + - --containers-only option is available. + +---- + +# Usage: Report + +``` +report: + report + [-lang=en|ja] + [-config=/path/to/config.toml] + [-results-dir=/path/to/results] + [-refresh-cve] + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-cvss-over=7] + [-ignore-unscored-cves] + [-to-email] + [-to-slack] + [-to-localfile] + [-to-s3] + [-to-azure-blob] + [-format-json] + [-format-xml] + [-format-one-line-text] + [-format-short-text] + [-format-full-text] + [-gzip] + [-aws-profile=default] + [-aws-region=us-west-2] + [-aws-s3-bucket=bucket_name] + [-azure-account=accout] + [-azure-key=key] + [-azure-container=container] + [-http-proxy=http://192.168.0.1:8080] + [-debug] + [-debug-sql] + + [SERVER]... + -aws-profile string + AWS profile to use (default "default") + -aws-region string + AWS region to use (default "us-east-1") + -aws-s3-bucket string + S3 bucket name + -azure-account string + Azure account name to use. AZURE_STORAGE_ACCOUNT environment variable is used if not specified + -azure-container string + Azure storage container name + -azure-key string + Azure account key to use. AZURE_STORAGE_ACCESS_KEY environment variable is used if not specified + -config string + /path/to/toml + -cvedb-path string + /path/to/sqlite3 (For get cve detail from cve.sqlite3) + -cvedb-type string + DB type for fetching CVE dictionary (sqlite3 or mysql) (default "sqlite3") + -cvedb-url string + http://cve-dictionary.com:8080 or mysql connection string + -cvss-over float + -cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all)) + -debug + debug mode + -debug-sql + SQL debug mode + -format-full-text + Detail report in plain text + -format-json + JSON format + -format-one-line-text + One line summary in plain text + -format-short-text + Summary in plain text + -format-xml + XML format + -gzip + gzip compression + -http-proxy string + http://proxy-url:port (default: empty) + -ignore-unscored-cves + Don't report the unscored CVEs + -lang string + [en|ja] (default "en") + -refresh-cve + Refresh CVE information in JSON file under results dir + -results-dir string + /path/to/results + -to-azure-blob + Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json/xml/txt) + -to-email + Send report via Email + -to-localfile + Write report to localfile + -to-s3 + Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/xml/txt) + -to-slack + Send report via Slack +``` + +## Example: Send scan results to Slack +``` +$ vuls report \ + -to-slack \ + -cvss-over=7 \ + -cvedb-path=$PWD/cve.sqlite3 +``` +With this sample command, it will .. +- Send scan results to slack +- Only Report CVEs that CVSS score is over 7 ## Example: Put results in S3 bucket -To put results in S3 bucket, configure following settings in AWS before scanning. +To put results in S3 bucket, configure following settings in AWS before reporting. - Create S3 bucket. see [Creating a Bucket](http://docs.aws.amazon.com/AmazonS3/latest/UG/CreatingaBucket.html) - Create access key. The access key must have read and write access to the AWS S3 bucket. see [Managing Access Keys for IAM Users](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) - Configure the security credentials. see [Configuring the AWS Command Line Interface](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) ``` -$ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ - -report-s3 \ +$ vuls report \ + -cvedb-path=$PWD/cve.sqlite3 \ + -to-s3 \ + -format-json \ -aws-region=ap-northeast-1 \ -aws-s3-bucket=vuls \ -aws-profile=default ``` With this sample command, it will .. -- Use SSH Key-Based authentication with empty password (without -ask-key-password option) -- Scan all servers defined in config file - Put scan result(JSON) in S3 bucket. The bucket name is "vuls" in ap-northeast-1 and profile is "default" ## Example: Put results in Azure Blob storage -To put results in Azure Blob Storage, configure following settings in Azure before scanning. -- Create a container +To put results in Azure Blob Storage, configure following settings in Azure before reporting. +- Create a Azure Blob container ``` $ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ + -cvedb-path=$PWD/cve.sqlite3 \ -report-azure-blob \ -azure-container=vuls \ -azure-account=test \ -azure-key=access-key-string ``` With this sample command, it will .. -- Use SSH Key-Based authentication with empty password (without -ask-key-password option) -- Scan all servers defined in config file - Put scan result(JSON) in Azure Blob Storage. The container name is "vuls", storage account is "test" and accesskey is "access-key-string" account and access key can be defined in environment variables. @@ -800,14 +906,14 @@ account and access key can be defined in environment variables. $ export AZURE_STORAGE_ACCOUNT=test $ export AZURE_STORAGE_ACCESS_KEY=access-key-string $ vuls scan \ - -cve-dictionary-dbpath=$PWD/cve.sqlite3 \ + -cvedb-path=$PWD/cve.sqlite3 \ -report-azure-blob \ -azure-container=vuls ``` ## Example: IgnoreCves -Define ignoreCves in config if you don't want to report(slack, mail, text...) specific CVE IDs. But these ignoreCves will be output to JSON file like below. +Define ignoreCves in config if you don't want to report(Slack, EMail, Text...) specific CVE IDs. But these ignoreCves will be output to JSON file like below. - config.toml ```toml @@ -886,8 +992,8 @@ optional = [ ``` $ vuls scan \ - -cve-dictionary-dbtype=mysql \ - -cve-dictionary-dbpath="user:pass@tcp(localhost:3306)/dbname?parseTime=true" + -cvedb-type=mysql \ + -cvedb-url="user:pass@tcp(localhost:3306)/dbname?parseTime=true" ``` ---- @@ -941,42 +1047,6 @@ How to integrate Vuls with OWASP Dependency Check ``` -# Usage: Scan Docker containers - -It is common that keep Docker containers running without SSHd daemon. -see [Docker Blog:Why you don't need to run SSHd in your Docker containers](https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/) - -Vuls scans Docker containers via `docker exec` instead of SSH. -For more details, see [Architecture section](https://github.com/future-architect/vuls#architecture) - -- To scan all of running containers - `"${running}"` needs to be set in the containers item. - ``` - [servers] - - [servers.172-31-4-82] - host = "172.31.4.82" - user = "ec2-user" - keyPath = "/home/username/.ssh/id_rsa" - containers = ["${running}"] - ``` - -- To scan specific containers - The container ID or container name needs to be set in the containers item. - In the following example, only `container_name_a` and `4aa37a8b63b9` will be scanned. - Be sure to check these containers are running state before scanning. - If specified containers are not running, Vuls gives up scanning with printing error message. - ``` - [servers] - - [servers.172-31-4-82] - host = "172.31.4.82" - user = "ec2-user" - keyPath = "/home/username/.ssh/id_rsa" - containers = ["container_name_a", "4aa37a8b63b9"] - ``` -- To scan containers only - - --containers-only option is available. # Usage: TUI @@ -984,15 +1054,27 @@ For more details, see [Architecture section](https://github.com/future-architect ## Display the latest scan results ``` -$ vuls tui -h tui: - tui [-results-dir=/path/to/results] + tui + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-results-dir=/path/to/results] + [-refresh-cve] + [-debug-sql] - -results-dir string - /path/to/results (default "$PWD/results") + -cvedb-path string + /path/to/sqlite3 (For get cve detail from cve.sqlite3) (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/cve.sqlite3") + -cvedb-type string + DB type for fetching CVE dictionary (sqlite3 or mysql) (default "sqlite3") + -cvedb-url string + http://cve-dictionary.com:8080 or mysql connection string -debug-sql - debug SQL - + debug SQL + -refresh-cve + Refresh CVE information in JSON file under results dir + -results-dir string + /path/to/results (default "/Users/kotakanbe/go/src/github.com/future-architect/vuls/results") ``` Key binding is below. @@ -1011,18 +1093,14 @@ For details, see https://github.com/future-architect/vuls/blob/master/report/tui - Display the list of scan results. ``` $ vuls history -20160524_1950 scanned 1 servers: amazon2 -20160524_1940 scanned 2 servers: amazon1, romantic_goldberg -``` - -- Display the result of scan 20160524_1949 -``` -$ vuls tui 20160524_1950 +2016-12-30T10:34:38+09:00 1 servers: u16 +2016-12-28T19:15:19+09:00 1 servers: ama +2016-12-28T19:10:03+09:00 1 servers: cent6 ``` -- Display the result of scan 20160524_1948 +- Display the result of scan 2016-12-30T10:34:38+09:00 ``` -$ vuls tui 20160524_1940 +$ vuls tui 2016-12-30T10:34:38+09:00 ``` # Display the previous scan results using peco @@ -1040,10 +1118,10 @@ Run go-cve-dictionary as server mode before scanning on 192.168.10.1 $ go-cve-dictionary server -bind=192.168.10.1 -port=1323 ``` -Run Vuls with -cve-dictionary-url option. +Run Vuls with -cvedb-url option. ``` -$ vuls scan -cve-dictionary-url=http://192.168.0.1:1323 +$ vuls scan -cvedb-url=http://192.168.0.1:1323 ``` # Usage: Update NVD Data diff --git a/commands/configtest.go b/commands/configtest.go index e80f6f4f8a..2bcda75089 100644 --- a/commands/configtest.go +++ b/commands/configtest.go @@ -146,7 +146,7 @@ func (p *ConfigtestCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa Log := util.NewCustomLogger(c.ServerInfo{}) Log.Info("Validating Config...") - if !c.Conf.Validate() { + if !c.Conf.ValidateOnConfigtest() { return subcommands.ExitUsageError } diff --git a/commands/discover.go b/commands/discover.go index 4aa32e2f57..7d674250ee 100644 --- a/commands/discover.go +++ b/commands/discover.go @@ -98,7 +98,7 @@ iconEmoji = ":ghost:" authUser = "username" notifyUsers = ["@username"] -[mail] +[email] smtpAddr = "smtp.gmail.com" smtpPort = "587" user = "username" diff --git a/commands/history.go b/commands/history.go index 9593286650..1c6acf4b94 100644 --- a/commands/history.go +++ b/commands/history.go @@ -27,7 +27,6 @@ import ( "strings" c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/report" "github.com/google/subcommands" ) @@ -70,11 +69,11 @@ func (p *HistoryCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{ c.Conf.ResultsDir = p.resultsDir var err error - var jsonDirs report.JSONDirs - if jsonDirs, err = report.GetValidJSONDirs(); err != nil { + var dirs jsonDirs + if dirs, err = lsValidJSONDirs(); err != nil { return subcommands.ExitFailure } - for _, d := range jsonDirs { + for _, d := range dirs { var files []os.FileInfo if files, err = ioutil.ReadDir(d); err != nil { return subcommands.ExitFailure @@ -89,7 +88,7 @@ func (p *HistoryCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{ } splitPath := strings.Split(d, string(os.PathSeparator)) timeStr := splitPath[len(splitPath)-1] - fmt.Printf("%s scanned %d servers: %s\n", + fmt.Printf("%s %d servers: %s\n", timeStr, len(hosts), strings.Join(hosts, ", "), diff --git a/commands/prepare.go b/commands/prepare.go index 02389ea4c4..cf1519acce 100644 --- a/commands/prepare.go +++ b/commands/prepare.go @@ -50,8 +50,9 @@ func (*PrepareCmd) Synopsis() string { return `Install required packages to scan. CentOS: yum-plugin-security, yum-plugin-changelog Amazon: None - RHEL: TODO + RHEL: None Ubuntu: None + Debian: aptitude ` } @@ -155,7 +156,7 @@ func (p *PrepareCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{ c.Conf.AssumeYes = p.assumeYes logrus.Info("Validating Config...") - if !c.Conf.Validate() { + if !c.Conf.ValidateOnPrepare() { return subcommands.ExitUsageError } // Set up custom logger diff --git a/commands/report.go b/commands/report.go new file mode 100644 index 0000000000..17095dccf5 --- /dev/null +++ b/commands/report.go @@ -0,0 +1,386 @@ +/* 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 . +*/ + +package commands + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + + c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/cveapi" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/report" + "github.com/future-architect/vuls/util" + "github.com/google/subcommands" + "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 + refreshCve bool + + cvssScoreOver float64 + ignoreUnscoredCves bool + httpProxy string + + cvedbtype string + cvedbpath string + cveDictionaryURL string + + toSlack bool + toEMail bool + toLocalFile bool + toS3 bool + toAzureBlob bool + + formatJSON bool + formatXML bool + formatOneLineText bool + formatShortText bool + formatFullText bool + + gzip bool + + awsProfile string + awsS3Bucket string + awsRegion string + + azureAccount string + azureKey string + azureContainer string +} + +// Name return subcommand name +func (*ReportCmd) Name() string { return "report" } + +// Synopsis return synopsis +func (*ReportCmd) Synopsis() string { return "Reporting" } + +// Usage return usage +func (*ReportCmd) Usage() string { + return `report: + report + [-lang=en|ja] + [-config=/path/to/config.toml] + [-results-dir=/path/to/results] + [-refresh-cve] + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-cvss-over=7] + [-ignore-unscored-cves] + [-to-email] + [-to-slack] + [-to-localfile] + [-to-s3] + [-to-azure-blob] + [-format-json] + [-format-xml] + [-format-one-line-text] + [-format-short-text] + [-format-full-text] + [-gzip] + [-aws-profile=default] + [-aws-region=us-west-2] + [-aws-s3-bucket=bucket_name] + [-azure-account=accout] + [-azure-key=key] + [-azure-container=container] + [-http-proxy=http://192.168.0.1:8080] + [-debug] + [-debug-sql] + + [SERVER]... +` +} + +// 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") + + 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.BoolVar( + &p.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 or mysql)") + + 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.com:8080 or mysql connection string") + + f.Float64Var( + &p.cvssScoreOver, + "cvss-over", + 0, + "-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))") + + f.BoolVar( + &p.ignoreUnscoredCves, + "ignore-unscored-cves", + false, + "Don't report the unscored CVEs") + + f.StringVar( + &p.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.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.toEMail, "to-email", false, "Send report via Email") + f.BoolVar(&p.toLocalFile, + "to-localfile", + false, + fmt.Sprintf("Write report to localfile")) + + f.BoolVar(&p.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.BoolVar(&p.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") +} + +// Execute execute +func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + c.Conf.Debug = p.debug + c.Conf.DebugSQL = p.debugSQL + Log := util.NewCustomLogger(c.ServerInfo{}) + + if err := c.Load(p.configPath, ""); err != nil { + Log.Errorf("Error loading %s, %s", p.configPath, err) + return subcommands.ExitUsageError + } + + c.Conf.Lang = p.lang + c.Conf.ResultsDir = p.resultsDir + c.Conf.CveDBType = p.cvedbtype + c.Conf.CveDBPath = p.cvedbpath + c.Conf.CveDictionaryURL = p.cveDictionaryURL + c.Conf.CvssScoreOver = p.cvssScoreOver + c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves + c.Conf.HTTPProxy = p.httpProxy + + jsonDir, err := jsonDir(f.Args()) + if err != nil { + log.Errorf("Failed to read from JSON: %s", err) + return subcommands.ExitFailure + } + + c.Conf.FormatXML = p.formatXML + c.Conf.FormatJSON = p.formatJSON + c.Conf.FormatOneLineText = p.formatOneLineText + c.Conf.FormatShortText = p.formatShortText + c.Conf.FormatFullText = p.formatFullText + + c.Conf.GZIP = p.gzip + + // report + reports := []report.ResultWriter{ + report.StdoutWriter{}, + } + + if p.toSlack { + reports = append(reports, report.SlackWriter{}) + } + + if p.toEMail { + reports = append(reports, report.EMailWriter{}) + } + + if p.toLocalFile { + reports = append(reports, report.LocalFileWriter{ + CurrentDir: jsonDir, + }) + } + + if p.toS3 { + c.Conf.AwsRegion = p.awsRegion + c.Conf.AwsProfile = p.awsProfile + c.Conf.S3Bucket = p.awsS3Bucket + if err := report.CheckIfBucketExists(); err != nil { + Log.Errorf("Check if there is a bucket beforehand: %s, err: %s", c.Conf.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") + } + + c.Conf.AzureKey = p.azureKey + if len(c.Conf.AzureKey) == 0 { + c.Conf.AzureKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY") + } + + c.Conf.AzureContainer = p.azureContainer + if len(c.Conf.AzureContainer) == 0 { + Log.Error("Azure storage container name is requied with --azure-container option") + return subcommands.ExitUsageError + } + if err := report.CheckIfAzureContainerExists(); err != nil { + Log.Errorf("Check if there is a container beforehand: %s, err: %s", c.Conf.AzureContainer, err) + return subcommands.ExitUsageError + } + reports = append(reports, report.AzureBlobWriter{}) + } + + if !(p.formatJSON || p.formatOneLineText || + p.formatShortText || p.formatFullText || p.formatXML) { + c.Conf.FormatShortText = true + } + + Log.Info("Validating Config...") + if !c.Conf.ValidateOnReport() { + return subcommands.ExitUsageError + } + if ok, err := cveapi.CveClient.CheckHealth(); !ok { + Log.Errorf("CVE HTTP server is not running. err: %s", err) + Log.Errorf("Run go-cve-dictionary as server mode before reporting or run with --cvedb-path option") + return subcommands.ExitFailure + } + if c.Conf.CveDictionaryURL != "" { + Log.Infof("cve-dictionary: %s", c.Conf.CveDictionaryURL) + } else { + if c.Conf.CveDBType == "sqlite3" { + Log.Infof("cve-dictionary: %s", c.Conf.CveDBPath) + } + } + + history, err := loadOneScanHistory(jsonDir) + + var results []models.ScanResult + for _, r := range history.ScanResults { + if p.refreshCve || needToRefreshCve(r) { + Log.Debugf("need to refresh") + if c.Conf.CveDBType == "sqlite3" { + if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + c.Conf.CveDBPath) + return subcommands.ExitFailure + } + } + + filled, err := fillCveInfoFromCveDB(r) + if err != nil { + Log.Errorf("Failed to fill CVE information: %s", err) + return subcommands.ExitFailure + } + filled.Lang = c.Conf.Lang + + if err := overwriteJSONFile(jsonDir, filled); err != nil { + Log.Errorf("Failed to write JSON: %s", err) + return subcommands.ExitFailure + } + results = append(results, filled) + } else { + Log.Debugf("no need to refresh") + results = append(results, r) + } + } + + var res models.ScanResults + for _, r := range results { + res = append(res, r.FilterByCvssOver()) + } + for _, w := range reports { + if err := w.Write(res...); err != nil { + Log.Errorf("Failed to report: %s", err) + return subcommands.ExitFailure + } + } + return subcommands.ExitSuccess +} diff --git a/commands/scan.go b/commands/scan.go index 17ebd7fa38..f06316bbdd 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -25,12 +25,9 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/Sirupsen/logrus" c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" - "github.com/future-architect/vuls/report" "github.com/future-architect/vuls/scan" "github.com/future-architect/vuls/util" "github.com/google/subcommands" @@ -39,46 +36,15 @@ import ( // ScanCmd is Subcommand of host discovery mode type ScanCmd struct { - lang string - debug bool - debugSQL bool - - configPath string - - resultsDir string - cvedbtype string - cvedbpath string - cveDictionaryURL string - cacheDBPath string - - cvssScoreOver float64 - ignoreUnscoredCves bool - - httpProxy string - askSudoPassword bool - askKeyPassword bool - + debug bool + configPath string + resultsDir string + cacheDBPath string + httpProxy string + askKeyPassword bool containersOnly bool skipBroken bool - - // reporting - reportSlack bool - reportMail bool - reportJSON bool - reportText bool - reportS3 bool - reportAzureBlob bool - reportXML bool - - awsProfile string - awsS3Bucket string - awsRegion string - - azureAccount string - azureKey string - azureContainer string - - sshExternal bool + sshExternal bool } // Name return subcommand name @@ -91,35 +57,15 @@ func (*ScanCmd) Synopsis() string { return "Scan vulnerabilities" } func (*ScanCmd) Usage() string { return `scan: scan - [-lang=en|ja] [-config=/path/to/config.toml] [-results-dir=/path/to/results] - [-cve-dictionary-dbtype=sqlite3|mysql] - [-cve-dictionary-dbpath=/path/to/cve.sqlite3 or mysql connection string] - [-cve-dictionary-url=http://127.0.0.1:1323] - [-cache-dbpath=/path/to/cache.db] - [-cvss-over=7] - [-ignore-unscored-cves] + [-cachedb-path=/path/to/cache.db] [-ssh-external] [-containers-only] [-skip-broken] - [-report-azure-blob] - [-report-json] - [-report-mail] - [-report-s3] - [-report-slack] - [-report-text] - [-report-xml] [-http-proxy=http://192.168.0.1:8080] [-ask-key-password] [-debug] - [-debug-sql] - [-aws-profile=default] - [-aws-region=us-west-2] - [-aws-s3-bucket=bucket_name] - [-azure-account=accout] - [-azure-key=key] - [-azure-container=container] [SERVER]... ` @@ -127,9 +73,7 @@ func (*ScanCmd) Usage() string { // SetFlags set flag func (p *ScanCmd) 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") wd, _ := os.Getwd() @@ -139,44 +83,13 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { defaultResultsDir := filepath.Join(wd, "results") f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results") - f.StringVar( - &p.cvedbtype, - "cve-dictionary-dbtype", - "sqlite3", - "DB type for fetching CVE dictionary (sqlite3 or mysql)") - - f.StringVar( - &p.cvedbpath, - "cve-dictionary-dbpath", - "", - "/path/to/sqlite3 (For get cve detail from cve.sqlite3)") - - defaultURL := "http://127.0.0.1:1323" - f.StringVar( - &p.cveDictionaryURL, - "cve-dictionary-url", - defaultURL, - "http://CVE.Dictionary") - defaultCacheDBPath := filepath.Join(wd, "cache.db") f.StringVar( &p.cacheDBPath, - "cache-dbpath", + "cachedb-path", defaultCacheDBPath, "/path/to/cache.db (local cache of changelog for Ubuntu/Debian)") - f.Float64Var( - &p.cvssScoreOver, - "cvss-over", - 0, - "-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))") - - f.BoolVar( - &p.ignoreUnscoredCves, - "ignore-unscored-cves", - false, - "Don't report the unscored CVEs") - f.BoolVar( &p.sshExternal, "ssh-external", @@ -202,55 +115,12 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { "http://proxy-url:port (default: empty)", ) - f.BoolVar(&p.reportSlack, "report-slack", false, "Send report via Slack") - f.BoolVar(&p.reportMail, "report-mail", false, "Send report via Email") - f.BoolVar(&p.reportJSON, - "report-json", - false, - fmt.Sprintf("Write report to JSON files (%s/results/current)", wd), - ) - f.BoolVar(&p.reportText, - "report-text", - false, - fmt.Sprintf("Write report to text files (%s/results/current)", wd), - ) - f.BoolVar(&p.reportXML, - "report-xml", - false, - fmt.Sprintf("Write report to XML files (%s/results/current)", wd), - ) - - f.BoolVar(&p.reportS3, - "report-s3", - false, - "Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json)", - ) - 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.BoolVar(&p.reportAzureBlob, - "report-azure-blob", - false, - "Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json)", - ) - 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( &p.askKeyPassword, "ask-key-password", false, "Ask ssh privatekey password before scanning", ) - - f.BoolVar( - &p.askSudoPassword, - "ask-sudo-password", - false, - "[Deprecated] THIS OPTION WAS REMOVED FOR SECURITY REASONS. Define NOPASSWD in /etc/sudoers on target servers and use SSH key-based authentication", - ) } // Execute execute @@ -264,10 +134,6 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) return subcommands.ExitFailure } } - if p.askSudoPassword { - logrus.Errorf("[Deprecated] -ask-sudo-password WAS REMOVED FOR SECURITY REASONS. Define NOPASSWD in /etc/sudoers on target servers and use SSH key-based authentication") - return subcommands.ExitFailure - } c.Conf.Debug = p.debug err = c.Load(p.configPath, keyPass) @@ -278,13 +144,6 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) logrus.Info("Start scanning") logrus.Infof("config: %s", p.configPath) - if p.cvedbpath != "" { - if p.cvedbtype == "sqlite3" { - logrus.Infof("cve-dictionary: %s", p.cvedbpath) - } - } else { - logrus.Infof("cve-dictionary: %s", p.cveDictionaryURL) - } var servernames []string if 0 < len(f.Args()) { @@ -324,91 +183,21 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } logrus.Debugf("%s", pp.Sprintf("%v", target)) - c.Conf.Lang = p.lang - c.Conf.DebugSQL = p.debugSQL - // logger Log := util.NewCustomLogger(c.ServerInfo{}) - scannedAt := time.Now() - - // report - reports := []report.ResultWriter{ - report.StdoutWriter{}, - report.LogrusWriter{}, - } - if p.reportSlack { - reports = append(reports, report.SlackWriter{}) - } - if p.reportMail { - reports = append(reports, report.MailWriter{}) - } - if p.reportJSON { - reports = append(reports, report.JSONWriter{ScannedAt: scannedAt}) - } - if p.reportText { - reports = append(reports, report.TextFileWriter{ScannedAt: scannedAt}) - } - if p.reportXML { - reports = append(reports, report.XMLWriter{ScannedAt: scannedAt}) - } - if p.reportS3 { - c.Conf.AwsRegion = p.awsRegion - c.Conf.AwsProfile = p.awsProfile - c.Conf.S3Bucket = p.awsS3Bucket - if err := report.CheckIfBucketExists(); err != nil { - Log.Errorf("Failed to access to the S3 bucket. err: %s", err) - Log.Error("Ensure the bucket or check AWS config before scanning") - return subcommands.ExitUsageError - } - reports = append(reports, report.S3Writer{}) - } - if p.reportAzureBlob { - c.Conf.AzureAccount = p.azureAccount - if len(c.Conf.AzureAccount) == 0 { - c.Conf.AzureAccount = 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") - } - - c.Conf.AzureContainer = p.azureContainer - if len(c.Conf.AzureContainer) == 0 { - Log.Error("Azure storage container name is requied with --azure-container option") - return subcommands.ExitUsageError - } - if err := report.CheckIfAzureContainerExists(); err != nil { - Log.Errorf("Failed to access to the Azure Blob container. err: %s", err) - Log.Error("Ensure the container or check Azure config before scanning") - return subcommands.ExitUsageError - } - reports = append(reports, report.AzureBlobWriter{}) - } c.Conf.ResultsDir = p.resultsDir - c.Conf.CveDBType = p.cvedbtype - c.Conf.CveDBPath = p.cvedbpath - c.Conf.CveDictionaryURL = p.cveDictionaryURL c.Conf.CacheDBPath = p.cacheDBPath - c.Conf.CvssScoreOver = p.cvssScoreOver - c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves c.Conf.SSHExternal = p.sshExternal c.Conf.HTTPProxy = p.httpProxy c.Conf.ContainersOnly = p.containersOnly c.Conf.SkipBroken = p.skipBroken Log.Info("Validating Config...") - if !c.Conf.Validate() { + if !c.Conf.ValidateOnScan() { return subcommands.ExitUsageError } - if ok, err := cveapi.CveClient.CheckHealth(); !ok { - Log.Errorf("CVE HTTP server is not running. err: %s", err) - Log.Errorf("Run go-cve-dictionary as server mode or specify -cve-dictionary-dbpath option") - return subcommands.ExitFailure - } - Log.Info("Detecting Server/Contianer OS... ") if err := scan.InitServers(Log); err != nil { Log.Errorf("Failed to init servers: %s", err) @@ -431,21 +220,9 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } return subcommands.ExitFailure } - - scanResults, err := scan.GetScanResults() - if err != nil { - Log.Fatal(err) - return subcommands.ExitFailure - } - - Log.Info("Reporting...") - filtered := scanResults.FilterByCvssOver() - for _, w := range reports { - if err := w.Write(filtered); err != nil { - Log.Fatalf("Failed to report, err: %s", err) - return subcommands.ExitFailure - } - } + fmt.Printf("\n\n\n") + fmt.Println("To view the detail, vuls tui is useful.") + fmt.Println("To send a report, run vuls report -h.") return subcommands.ExitSuccess } diff --git a/commands/tui.go b/commands/tui.go index 1b9cd53fc7..19a4d74041 100644 --- a/commands/tui.go +++ b/commands/tui.go @@ -20,13 +20,12 @@ package commands import ( "context" "flag" - "io/ioutil" "os" "path/filepath" - "strings" log "github.com/Sirupsen/logrus" c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/report" "github.com/google/subcommands" ) @@ -36,6 +35,11 @@ type TuiCmd struct { lang string debugSQL bool resultsDir string + + refreshCve bool + cvedbtype string + cvedbpath string + cveDictionaryURL string } // Name return subcommand name @@ -47,7 +51,13 @@ func (*TuiCmd) Synopsis() string { return "Run Tui view to anayze vulnerabilites // Usage return usage func (*TuiCmd) Usage() string { return `tui: - tui [-results-dir=/path/to/results] + tui + [-cvedb-type=sqlite3|mysql] + [-cvedb-path=/path/to/cve.sqlite3] + [-cvedb-url=http://127.0.0.1:1323 or mysql connection string] + [-results-dir=/path/to/results] + [-refresh-cve] + [-debug-sql] ` } @@ -58,9 +68,33 @@ func (p *TuiCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.debugSQL, "debug-sql", false, "debug SQL") wd, _ := os.Getwd() - defaultResultsDir := filepath.Join(wd, "results") f.StringVar(&p.resultsDir, "results-dir", defaultResultsDir, "/path/to/results") + + f.BoolVar( + &p.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 or mysql)") + + 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.com:8080 or mysql connection string") } // Execute execute @@ -68,38 +102,53 @@ func (p *TuiCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s c.Conf.Lang = "en" c.Conf.DebugSQL = p.debugSQL c.Conf.ResultsDir = p.resultsDir + c.Conf.CveDBType = p.cvedbtype + c.Conf.CveDBPath = p.cvedbpath + c.Conf.CveDictionaryURL = p.cveDictionaryURL - var jsonDirName string - var err error - if 0 < len(f.Args()) { - var jsonDirs report.JSONDirs - if jsonDirs, err = report.GetValidJSONDirs(); err != nil { - return subcommands.ExitFailure - } - for _, d := range jsonDirs { - splitPath := strings.Split(d, string(os.PathSeparator)) - if splitPath[len(splitPath)-1] == f.Args()[0] { - jsonDirName = f.Args()[0] - break + log.Info("Validating Config...") + if !c.Conf.ValidateOnTui() { + return subcommands.ExitUsageError + } + + jsonDir, err := jsonDir(f.Args()) + if err != nil { + log.Errorf("Failed to read json dir under results: %s", err) + return subcommands.ExitFailure + } + + history, err := loadOneScanHistory(jsonDir) + if err != nil { + log.Errorf("Failed to read from JSON: %s", err) + return subcommands.ExitFailure + } + + var results []models.ScanResult + for _, r := range history.ScanResults { + if p.refreshCve || needToRefreshCve(r) { + if c.Conf.CveDBType == "sqlite3" { + if _, err := os.Stat(c.Conf.CveDBPath); os.IsNotExist(err) { + log.Errorf("SQLite3 DB(CVE-Dictionary) is not exist: %s", + c.Conf.CveDBPath) + return subcommands.ExitFailure + } } - } - if len(jsonDirName) == 0 { - log.Errorf("First Argument have to be JSON directory name : %s", err) - return subcommands.ExitFailure - } - } else { - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - bytes, err := ioutil.ReadAll(os.Stdin) + + filled, err := fillCveInfoFromCveDB(r) if err != nil { - log.Errorf("Failed to read stdin: %s", err) + log.Errorf("Failed to fill CVE information: %s", err) return subcommands.ExitFailure } - fields := strings.Fields(string(bytes)) - if 0 < len(fields) { - jsonDirName = fields[0] + + if err := overwriteJSONFile(jsonDir, filled); err != nil { + log.Errorf("Failed to write JSON: %s", err) + return subcommands.ExitFailure } + results = append(results, filled) + } else { + results = append(results, r) } } - return report.RunTui(jsonDirName) + history.ScanResults = results + return report.RunTui(history) } diff --git a/commands/util.go b/commands/util.go new file mode 100644 index 0000000000..6a7f345108 --- /dev/null +++ b/commands/util.go @@ -0,0 +1,225 @@ +/* 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 . +*/ + +package commands + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/cveapi" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/report" + "github.com/future-architect/vuls/util" +) + +// jsonDirPattern is file name pattern of JSON directory +// 2016-11-16T10:43:28+09:00 +// 2016-11-16T10:43:28Z +var jsonDirPattern = regexp.MustCompile( + `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`) + +// JSONDirs is array of json files path. +type jsonDirs []string + +// sort as recent directories are at the head +func (d jsonDirs) Len() int { + return len(d) +} +func (d jsonDirs) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} +func (d jsonDirs) Less(i, j int) bool { + return d[j] < d[i] +} + +// getValidJSONDirs return valid json directory as array +// Returned array is sorted so that recent directories are at the head +func lsValidJSONDirs() (dirs jsonDirs, err error) { + var dirInfo []os.FileInfo + if dirInfo, err = ioutil.ReadDir(c.Conf.ResultsDir); err != nil { + err = fmt.Errorf("Failed to read %s: %s", c.Conf.ResultsDir, err) + return + } + for _, d := range dirInfo { + if d.IsDir() && jsonDirPattern.MatchString(d.Name()) { + jsonDir := filepath.Join(c.Conf.ResultsDir, d.Name()) + dirs = append(dirs, jsonDir) + } + } + sort.Sort(dirs) + return +} + +// jsonDir returns +// If there is an arg, check if it is a valid format and return the corresponding path under results. +// If passed via PIPE (such as history subcommand), return that path. +// Otherwise, returns the path of the latest directory +func jsonDir(args []string) (string, error) { + var err error + var dirs jsonDirs + + if 0 < len(args) { + if dirs, err = lsValidJSONDirs(); err != nil { + return "", err + } + + path := filepath.Join(c.Conf.ResultsDir, args[0]) + for _, d := range dirs { + ss := strings.Split(d, string(os.PathSeparator)) + timedir := ss[len(ss)-1] + if timedir == args[0] { + return path, nil + } + } + + return "", fmt.Errorf("Invalid path: %s", path) + } + + // PIPE + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("Failed to read stdin: %s", err) + } + fields := strings.Fields(string(bytes)) + if 0 < len(fields) { + return filepath.Join(c.Conf.ResultsDir, fields[0]), nil + } + return "", fmt.Errorf("Stdin is invalid: %s", string(bytes)) + } + + // returns latest dir when no args or no PIPE + if dirs, err = lsValidJSONDirs(); err != nil { + return "", err + } + if len(dirs) == 0 { + return "", fmt.Errorf("No results under %s", + c.Conf.ResultsDir) + } + return dirs[0], nil +} + +// loadOneScanHistory read JSON data +func loadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err error) { + var results []models.ScanResult + var files []os.FileInfo + if files, err = ioutil.ReadDir(jsonDir); err != nil { + err = fmt.Errorf("Failed to read %s: %s", jsonDir, err) + return + } + for _, f := range files { + if filepath.Ext(f.Name()) != ".json" { + continue + } + var r models.ScanResult + var data []byte + path := filepath.Join(jsonDir, f.Name()) + if data, err = ioutil.ReadFile(path); err != nil { + err = fmt.Errorf("Failed to read %s: %s", path, err) + return + } + if json.Unmarshal(data, &r) != nil { + err = fmt.Errorf("Failed to parse %s: %s", path, err) + return + } + results = append(results, r) + } + if len(results) == 0 { + err = fmt.Errorf("There is no json file under %s", jsonDir) + return + } + + scanHistory = models.ScanHistory{ + ScanResults: results, + } + return +} + +func fillCveInfoFromCveDB(r models.ScanResult) (filled models.ScanResult, err error) { + sInfo := c.Conf.Servers[r.ServerName] + vs, err := scanVulnByCpeNames(sInfo.CpeNames, r.ScannedCves) + if err != nil { + return + } + r.ScannedCves = vs + filled, err = r.FillCveDetail() + if err != nil { + return + } + return +} + +func overwriteJSONFile(dir string, r models.ScanResult) error { + before := c.Conf.FormatJSON + c.Conf.FormatJSON = true + w := report.LocalFileWriter{CurrentDir: dir} + if err := w.Write(r); err != nil { + return fmt.Errorf("Failed to write summary report: %s", err) + } + c.Conf.FormatJSON = before + return nil +} + +func scanVulnByCpeNames(cpeNames []string, scannedVulns []models.VulnInfo) ([]models.VulnInfo, + error) { + // To remove duplicate + set := map[string]models.VulnInfo{} + for _, v := range scannedVulns { + set[v.CveID] = v + } + + for _, name := range cpeNames { + details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name) + if err != nil { + return nil, err + } + for _, detail := range details { + if val, ok := set[detail.CveID]; ok { + names := val.CpeNames + names = util.AppendIfMissing(names, name) + val.CpeNames = names + set[detail.CveID] = val + } else { + set[detail.CveID] = models.VulnInfo{ + CveID: detail.CveID, + CpeNames: []string{name}, + } + } + } + } + + vinfos := []models.VulnInfo{} + for key := range set { + vinfos = append(vinfos, set[key]) + } + return vinfos, nil +} + +func needToRefreshCve(r models.ScanResult) bool { + return r.Lang != c.Conf.Lang || len(r.KnownCves) == 0 && + len(r.UnknownCves) == 0 && + len(r.IgnoredCves) == 0 +} diff --git a/config/config.go b/config/config.go index c21c6d22d7..f50d861c07 100644 --- a/config/config.go +++ b/config/config.go @@ -35,7 +35,7 @@ type Config struct { DebugSQL bool Lang string - Mail smtpConf + EMail smtpConf Slack SlackConf Default ServerInfo Servers map[string]ServerInfo @@ -56,6 +56,14 @@ type Config struct { CveDBPath string CacheDBPath string + FormatXML bool + FormatJSON bool + FormatOneLineText bool + FormatShortText bool + FormatFullText bool + + GZIP bool + AwsProfile string AwsRegion string S3Bucket string @@ -63,15 +71,44 @@ type Config struct { AzureAccount string AzureKey string AzureContainer string +} + +// ValidateOnConfigtest validates +func (c Config) ValidateOnConfigtest() bool { + errs := []error{} + + if runtime.GOOS == "windows" && c.SSHExternal { + errs = append(errs, fmt.Errorf("-ssh-external cannot be used on windows")) + } + + _, err := valid.ValidateStruct(c) + if err != nil { + errs = append(errs, err) + } - // CpeNames []string - // SummaryMode bool + for _, err := range errs { + log.Error(err) + } + + return len(errs) == 0 } -// Validate configuration -func (c Config) Validate() bool { +// ValidateOnPrepare validates configuration +func (c Config) ValidateOnPrepare() bool { + return c.ValidateOnConfigtest() +} + +// ValidateOnScan validates configuration +func (c Config) ValidateOnScan() bool { errs := []error{} + if len(c.ResultsDir) != 0 { + if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { + errs = append(errs, fmt.Errorf( + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) + } + } + if runtime.GOOS == "windows" && c.SSHExternal { errs = append(errs, fmt.Errorf("-ssh-external cannot be used on windows")) } @@ -83,9 +120,34 @@ func (c Config) Validate() bool { } } - // If no valid DB type is set, default to sqlite3 - if c.CveDBType == "" { - c.CveDBType = "sqlite3" + 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)) + } + } + + _, err := valid.ValidateStruct(c) + if 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{} + + if len(c.ResultsDir) != 0 { + if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { + errs = append(errs, fmt.Errorf( + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) + } } if c.CveDBType != "sqlite3" && c.CveDBType != "mysql" { @@ -94,18 +156,9 @@ func (c Config) Validate() bool { } if c.CveDBType == "sqlite3" { - if len(c.CveDBPath) != 0 { - if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { - errs = append(errs, fmt.Errorf( - "SQLite3 DB(Cve Dictionary) path must be a *Absolute* file path. -cve-dictionary-dbpath: %s", c.CveDBPath)) - } - } - } - - if len(c.CacheDBPath) != 0 { - if ok, _ := valid.IsFilePath(c.CacheDBPath); !ok { + if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { errs = append(errs, fmt.Errorf( - "Cache DB path must be a *Absolute* file path. -cache-dbpath: %s", c.CacheDBPath)) + "SQLite3 DB(CVE-Dictionary) path must be a *Absolute* file path. -cve-dictionary-dbpath: %s", c.CveDBPath)) } } @@ -114,7 +167,7 @@ func (c Config) Validate() bool { errs = append(errs, err) } - if mailerrs := c.Mail.Validate(); 0 < len(mailerrs) { + if mailerrs := c.EMail.Validate(); 0 < len(mailerrs) { errs = append(errs, mailerrs...) } @@ -129,6 +182,36 @@ func (c Config) Validate() bool { return len(errs) == 0 } +// ValidateOnTui validates configuration +func (c Config) ValidateOnTui() bool { + errs := []error{} + + if len(c.ResultsDir) != 0 { + if ok, _ := valid.IsFilePath(c.ResultsDir); !ok { + errs = append(errs, fmt.Errorf( + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) + } + } + + if c.CveDBType != "sqlite3" && c.CveDBType != "mysql" { + errs = append(errs, fmt.Errorf( + "CVE DB type must be either 'sqlite3' or 'mysql'. -cve-dictionary-dbtype: %s", c.CveDBType)) + } + + if c.CveDBType == "sqlite3" { + if ok, _ := valid.IsFilePath(c.CveDBPath); !ok { + errs = append(errs, fmt.Errorf( + "SQLite3 DB(CVE-Dictionary) path must be a *Absolute* file path. -cve-dictionary-dbpath: %s", c.CveDBPath)) + } + } + + for _, err := range errs { + log.Error(err) + } + + return len(errs) == 0 +} + // smtpConf is smtp config type smtpConf struct { SMTPAddr string diff --git a/config/tomlloader.go b/config/tomlloader.go index abb7a9ede6..f4c4f6af80 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -43,7 +43,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { return err } - Conf.Mail = conf.Mail + Conf.EMail = conf.EMail Conf.Slack = conf.Slack d := conf.Default @@ -119,7 +119,7 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { return fmt.Errorf( "Failed to read OWASP Dependency Check XML: %s", err) } - log.Infof("Loaded from OWASP Dependency Check XML: %s", + log.Debugf("Loaded from OWASP Dependency Check XML: %s", s.ServerName) s.CpeNames = append(s.CpeNames, cpes...) } diff --git a/cveapi/cve_client.go b/cveapi/cve_client.go index 44f5f282b6..4560c43a18 100644 --- a/cveapi/cve_client.go +++ b/cveapi/cve_client.go @@ -48,7 +48,7 @@ func (api *cvedictClient) initialize() { } func (api cvedictClient) CheckHealth() (ok bool, err error) { - if config.Conf.CveDBPath != "" { + if config.Conf.CveDictionaryURL == "" { log.Debugf("get cve-dictionary from %s", config.Conf.CveDBType) return true, nil } @@ -71,7 +71,7 @@ type response struct { } func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDetails, err error) { - if config.Conf.CveDBPath != "" { + if config.Conf.CveDictionaryURL == "" { return api.FetchCveDetailsFromCveDB(cveIDs) } @@ -129,7 +129,6 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDet fmt.Errorf("Failed to fetch CVE. err: %v", errs) } - // order by CVE ID desc sort.Sort(cveDetails) return } @@ -137,7 +136,11 @@ func (api cvedictClient) FetchCveDetails(cveIDs []string) (cveDetails cve.CveDet func (api cvedictClient) FetchCveDetailsFromCveDB(cveIDs []string) (cveDetails cve.CveDetails, err error) { log.Debugf("open cve-dictionary db (%s)", config.Conf.CveDBType) cveconfig.Conf.DBType = config.Conf.CveDBType - cveconfig.Conf.DBPath = config.Conf.CveDBPath + if config.Conf.CveDBType == "sqlite3" { + cveconfig.Conf.DBPath = config.Conf.CveDBPath + } else { + cveconfig.Conf.DBPath = config.Conf.CveDictionaryURL + } cveconfig.Conf.DebugSQL = config.Conf.DebugSQL if err := cvedb.OpenDB(); err != nil { return []cve.CveDetail{}, @@ -194,7 +197,7 @@ type responseGetCveDetailByCpeName struct { } func (api cvedictClient) FetchCveDetailsByCpeName(cpeName string) ([]cve.CveDetail, error) { - if config.Conf.CveDBPath != "" { + if config.Conf.CveDictionaryURL == "" { return api.FetchCveDetailsByCpeNameFromDB(cpeName) } diff --git a/glide.lock b/glide.lock index ccbff0ca69..3f2cee6efa 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: ca64aef6e9e94c7be91f79b88edb847363c8a5bd48da4ad27784e9342c8db6e2 -updated: 2016-11-14T17:14:19.692072231Z +hash: c3167d83e68562cd7ef73f138ce60cb9e60b72b50394e8615388d1f3ba9fbef2 +updated: 2017-01-02T09:37:09.437363123+09:00 imports: - name: github.com/asaskevich/govalidator version: 7b3beb6df3c42abd3509abfc3bcacc0fbfb7c877 @@ -46,7 +46,7 @@ imports: - name: github.com/go-ini/ini version: 6e4869b434bd001f6983749881c7ead3545887d8 - name: github.com/go-sql-driver/mysql - version: 2a6c6079c7eff49a7e9d641e109d922f124a3e4c + version: d512f204a577a4ab037a1816604c48c9c13210be - name: github.com/google/subcommands version: a71b91e238406bd68766ee52db63bebedce0e9f6 - name: github.com/gosuri/uitable @@ -60,6 +60,7 @@ imports: version: 39165d498058a823126af3cbf4d2a3b0e1acf11e subpackages: - dialects/mysql + - dialects/sqlite - name: github.com/jinzhu/inflection version: 74387dc39a75e970e7a3ae6a3386b5bd2e5c5cff - name: github.com/jmespath/go-jmespath @@ -69,7 +70,7 @@ imports: - name: github.com/k0kubun/pp version: f5dce6ed0ccf6c350f1679964ff6b61f3d6d2033 - name: github.com/kotakanbe/go-cve-dictionary - version: 2dd369d26145eab2178f6d509821fcabc9391627 + version: 7eb1f1a2e7e436177570bf234e21c2ed9489d3fb subpackages: - config - db @@ -89,7 +90,7 @@ imports: - name: github.com/mattn/go-runewidth version: 737072b4e32b7a5018b4a7125da8d12de90e8045 - name: github.com/mattn/go-sqlite3 - version: e5a3c16c5c1d80b24f633e68aecd6b0702786d3d + version: 5510da399572b4962c020184bb291120c0a412e2 - name: github.com/mgutz/ansi version: c286dcecd19ff979eeb73ea444e479b903f2cfcb - name: github.com/moul/http2curl @@ -112,9 +113,8 @@ imports: - ssh/agent - ssh/terminal - name: golang.org/x/net - version: cf4effbb9db1f3ef07f7e1891402991b6afbb276 + version: 1d7a0b2100da090d8b02afcfb42f97e2c77e71a4 subpackages: - - context - publicsuffix - name: golang.org/x/sys version: 9bb9f0998d48b31547d975974935ae9b48c7a03c diff --git a/glide.yaml b/glide.yaml index 736d995a70..ce42b29f1f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -15,16 +15,15 @@ import: - package: github.com/boltdb/bolt - package: github.com/cenkalti/backoff - package: github.com/google/subcommands - branch: context - package: github.com/gosuri/uitable - package: github.com/howeyc/gopass -- package: github.com/jinzhu/gorm - package: github.com/jroimartin/gocui - package: github.com/k0kubun/pp - package: github.com/kotakanbe/go-cve-dictionary subpackages: - config - db + - log - models - package: github.com/kotakanbe/go-pingscanner - package: github.com/kotakanbe/logrus-prefixed-formatter @@ -35,6 +34,3 @@ import: subpackages: - ssh - ssh/agent -- package: golang.org/x/net - subpackages: - - context diff --git a/img/vuls-architecture.graphml b/img/vuls-architecture.graphml index 6b6f723742..0639fa6c7d 100644 --- a/img/vuls-architecture.graphml +++ b/img/vuls-architecture.graphml @@ -37,7 +37,7 @@ - + Vulnerbility Database @@ -63,7 +63,7 @@ - + JVN @@ -81,7 +81,7 @@ - + NVD @@ -103,7 +103,7 @@ - + Distribution Support @@ -129,7 +129,7 @@ - + apptitude @@ -147,7 +147,7 @@ changelog - + yum @@ -165,7 +165,7 @@ changelog - + RHSA (RedHat) @@ -183,7 +183,7 @@ ALAS (Amazon) - + FreeBSD Support @@ -202,7 +202,7 @@ ALAS (Amazon) - + @@ -222,14 +222,14 @@ ALAS (Amazon) - + - Vuls + Vuls - + @@ -248,10 +248,10 @@ ALAS (Amazon) - - + + - Report + Scan @@ -265,10 +265,10 @@ ALAS (Amazon) - - + + - TUI View + Report @@ -282,10 +282,11 @@ ALAS (Amazon) - + - Scan + VulsRepo +(WebUI) @@ -299,11 +300,10 @@ ALAS (Amazon) - - + + - Web View -(Vulsrepo) + TUI @@ -319,7 +319,7 @@ ALAS (Amazon) - + System Operator @@ -339,7 +339,7 @@ ALAS (Amazon) - + @@ -353,7 +353,7 @@ ALAS (Amazon) - + @@ -468,14 +468,14 @@ ALAS (Amazon) - + - Docker Containers + Docker Containers - + @@ -534,14 +534,14 @@ Container - + - Linux/FreeBSD Servers + Linux/FreeBSD - + @@ -599,7 +599,7 @@ Container - + results dir @@ -625,7 +625,7 @@ Container - + JSON @@ -642,7 +642,7 @@ Container - + JSON @@ -659,7 +659,7 @@ Container - + JSON @@ -675,6 +675,104 @@ Container + + + + + + + + + + + + + + + + + + + Azure +BLOB + + + + + + + + + + + + + + + + + + .xml + + + + + + + + + + + + + + + + + .txt + + + + + + + + + + + + + + + + + .json + + + + + + + + + + + + + + + + + .gz + + + + + + + + + @@ -700,7 +798,7 @@ Vulnerability data - HTTP + HTTP @@ -712,17 +810,17 @@ Vulnerability data - + - HTTP or --cve-dictoianry-dbpath option + HTTP - + @@ -730,17 +828,17 @@ Vulnerability data - + - + - HTTP + WebUI - + @@ -748,17 +846,17 @@ Vulnerability data - + - + - send + SSH - + @@ -766,17 +864,17 @@ Vulnerability data - + - Generate + SSH - + @@ -784,17 +882,17 @@ Vulnerability data - + - View Detail Information + docker exec - + @@ -802,17 +900,37 @@ Vulnerability data - + + + + + + + + + + + + + + + + + + + + + - + - SSH + Insert - + @@ -820,17 +938,17 @@ Vulnerability data - + - SSH + Notify - + @@ -838,17 +956,17 @@ Vulnerability data - + - docker exec + Select - + @@ -856,7 +974,7 @@ Vulnerability data - + @@ -866,7 +984,8 @@ Vulnerability data - + + @@ -876,25 +995,19 @@ Vulnerability data - + + - Insert - - - - - - - - + + @@ -904,50 +1017,36 @@ Vulnerability data - + + - Notify - - - - - - - - + + - - - - - - - - - - + + - Select + Put @@ -959,17 +1058,8 @@ Vulnerability data - - - - - - - - - - - + + @@ -979,7 +1069,7 @@ Vulnerability data - + @@ -990,13 +1080,22 @@ Vulnerability data - + + View Results + on Terminal + + + + + + + @@ -1185,6 +1284,180 @@ Vulnerability data style="fill:#373d47;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path46-3" inkscape:connector-curvature="0" /></g></svg> + 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 diff --git a/img/vuls-architecture.png b/img/vuls-architecture.png index eeeb7d2e016aab741e38e18bd515945cb118e58c..08a81457c77eb8c4d8c53f5f5a832b1e6eb86b3f 100644 GIT binary patch literal 92769 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 literal 78951 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 z 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 B_t*dc diff --git a/img/vuls-scan-flow.png b/img/vuls-scan-flow.png index a82b9403b8c04ce4200536659d7b2d5072d24018..f4959b35a3b61ffde18132baf7306930d613dafd 100644 GIT binary patch delta 6902 zcmYLOc|278_iuSB0g%BgMPB&|WvXo*X z`#w_!sVv!-P-dhd{3i9het(^NU-xrA%URy%ocE_c?!b?xf9~_M@9SXOw`b3ug0bPd zw?rO@>=oQ6B2lBYH|rosGBdN^5^hiD5Hj8?^j_E0^nct(x%Hl>A2s}{pFr?(;d=a` zRnU}IOp?d)XuhOg97ELiA+)Zmu}^yD`zcEAX5AO@>j))aRvo5ZZyI?nQqRZ>EYLWDS#q#T`A%hG9ovo`Xrd zQRQGCFSMIFM;%eBPLo3K+&_w5Y<)`=JOyku-h-B^0r!4nYU@pC-@)GH?L1R%)&#{0 z=pA>~)3g%!XRb9>!V#Dy3cbU_g&vEGf@CM0yoz*R8C{&3huP}4o9>NDqn39JQ7bwG zSTf#4lBB!IfoAuHe>_W}YAW!GL?2x!0^I9GvE9^z2Z6FY*J4XQ;DkRQ3$~|BwNYKJ zJ?#*7M^Xwjq!=Dfrbc+14Sx!4sC^64 zIj&AuJ564nA@aZ#P{O;5<==-YgvM63zTAxKQj@tNWLO$1J=u2VtImbbuR=fdaR|L! z6=F=;q~la@)>c*)=H|Ii3w7mAkfla?5XRXkW*_-{bS}|ypiNo|`%nR6=o``RaM z?j`q{S}1i}$f-(2%|vXpnR3Y557kBPqhl6qUpM?(K-^-A2N~zcycEU^{8cOFE;NDz zMylQK3}5Xw_fx7lKE_yXh?x!oqjM>ulkV>Br9z)VfgBGtEd^AZ0;7+7$7qK56CU=d zKfV6n&v@YnkISFbL{6%l0(xa|Pm6b`>%dZ=XFAZ^QPEowCbOLYJ%&ryBWW!??_1YK z5lGY>5p*k(BreP{=YIyvg|bDLAL&Llxu!&`5_r5!F7|%|zcC76nW=BhF2b$HHV&Z) zJb_1)Qf-rUBEASR3o8P;?`o4z9umsxOG&r>=|SgoCVV{ojLx3;7-rxw2EwYlBFQP0 zT`$L$^7dqAT6kP}Y2=`rTpzj}SGNak?2)09gvp!pRu6dtUN{}fy;DLBOpCca@vvgh zS25e~EHZTuh>{~a&0IB9R}ZaoFIAt$Nk+XflKHoR?Nw;+N8|o`M$vGMA&+DVk!>O7 zPDtbGz#_|pxRH6P1lsdqA18o?03Sbh`ei3x=i&#wqC;o6%4R=xcFKH^y-YNtU4Ww$ zk(3WKI|t_;gUtjBjsz)!#&Lhehbm}HX!aJt_Z*nr@+ds=myN!@zKy%)I|mb+#q~pp ziy@=m1~z=7vos00uL9b;+DuGLoWGI$xpLyFQr(;zN+CxH+))Ab6;RQ@0z?=HnLGt`{1$Xh2!mA9!Mtn9=sIKU<5@H z9Ckour?ePjyx$4rTzP~}2)^un?E^=g$e{QJBdG(0QXyKH+Ew~Q)htz34Xu~~Yhbdk zn~S#XMZeJ|zIX6}=v=wwBmgjWo1RMlgxju9fUfN#8w~+Wldxfef_mpJjjcZKX68qo z&zbr3(G1v{e>Pc8-n^@Q7~L6PHws2hb#=+?0E9v}zGTuj)dp&2nhpw8v;S;fsi01N zRaZ2;WNrO1zJ2!5)LILsM4)0MOqeJwrlhGN@v`K5-dmK|b;HHE{Nusi=rF&SJ?L1r z{kiRCfR)8i{f=5{{lhC&{`5qxua&1(+9`J(4?9Q^c+Ly!1WDCBORTS*m;o_Jn$KvH z@uvpY2dFSDKth#N_f^ivEVurlg4tS6fzT~y%v3vjQa8VcWOBwzKP(Q4{uzS5&4yO4 z2@GNYem6Ekwecz#s~mr#OL0NX5<#}EpT4Mxzdb$8mm@#=FF>9OmKR0Ji_{rh>$x;G zo*oT(GuK{k^k?^N^Q7B=CQb_z3w&vfvE3!&s=t0PB$P?#U?pXx;8ve27l|5M;)Cd` zJFGx+DPP*#&VJ3tZgqm;&b60y3ZMA}NAebReU#3df&yBMzw`JYCmYba!`Ldx+pcJg1ji8 z2h-^P92T=RBQS5JN+xmN6V}>dZ>0`j=l^;pOir{WyWan`f4roHm2v-4q}TD%FBo3- zKVU3|K67yjB(M83yll)|SG|Yz&;+1UzDxp;xEFud7=rnD_<1+@iM={2I&t`aS2A~w zrDxMuS7g8}yBgV@ehzf0pf&*4pwVxEzKf(xTaCIO%krh6L({R)K0xc*3{Q<@h0XxA zcT)G#RSjm&#)0#6N=FEV`gq}`nEt&BeR6!4Qa#gElLWOiez|*G^%+gNgZ)r3b{5EV zTnqkrmpj%*qA7m0o+%zl5Iz%{Y@1ASak??xMg~Ps0cFchb8DBiX^(&Pt#Eym-HB5` z73Ig^7a!7^G`ZtfJ@^MrJS!}O&>TQQto8x^X-}I=XL4Wlxz0^zaTW71m%=$u+rB1{ zpw6!!lk@a9V+szoPvKvVT`Sl+%b zt#CMlZr$WcJUm&vHhU6!ds)HD9 z*oO>-f3wpdhd0$(P6Y!#J+=E!gl8LM0NDZ19~i=`^M=#fn<#4nZh%^=qq`c z55*}jvY{jPJKoQ#`z-WP0*xEvdIa-P8#%{Hp>?jsbwkJEpxKI%ckh%~v9SUQ=)O^2 zDK2b)+YUqyjCg)OvKh*rCYcBi|J9@HOyKd8bMV8BghoxyU(qD2{$Rl&{_E!{js~FO|RoqRxI45^G)=1FLQN{ zUEry^wZp_rb*~k{u@Cdpzz)ADB;)g1_zT0Kp6ByFKcW$Gbz#~kWMwFyU|p_r1826U zJo{BqPg1#^9V#iLETvV>MXvPc}@2D^1ok3c$nS0HOb}U56v*=tw zP3Z-w0&DDvA_ry5e67ue&%*?BS_DyEN9&%(y0Geiv}kCW<7AW=@eTk_=_;8bVdISE zYBQ|fUwC)DFrz?{z^~-w<%~c_>~pv;klm#s;m*gxiiqVvwd&wW&Q(#=sZ!U?tC1&K zO^G)A(a>JdSxT|_&K-$ovD&?5Gjt`!gnVwxqiyHTDZ=dcSFawd^v-?=?4+Iu=lqEK z&_^*H8~^z>>Q4OyDSl;A3x(=CcH&Z1dpiRi|gTK>|U7k2ogJL?FEaMumLIPk7N< zTdo}xeje+86-c}kzt7hjLt8q`-r3ZUG)TOMc0K%kqiKNpSDoUgPkFerjNqt->OmdJ z+wIhel0vANkZ@dGdYf=;@EUIz|okA~hSix1_ zeor;T^oFM|3X7dj&;VO=6sq$J?JtLPy~yEYKJbl()LS`H&snu4>&)N(Bmi_@)OnJ0+XOmAuE*G7e&uXIRH<)^=}0pQxB}#PF{Q6 zD2>y)&g5s(L*Cq&{IOLu)%$cp_3b;jkKjigVJ%}ME%*9k*v(I3qa<53)vp3@z`>=BKl+j#e(NIHHldqdKPp zl;N1QJL6_7cv)D;d>v+SuTaSGdR_C*t+|N;8K6*b)uapoNJNV&JT4cwHg5zoi0Hf| zF|B^xci∓=IfwrI`pLORIh=jrLwW^hf2lw`(~-gmfG^Z^!+RSrU80wvvXMG6rweiN_+m?-U!L~ zS|CL0y&R(PO`AiOX-9AfpbN^%6shUYS9cp*z0qUp8CoA>OTm#F{X&B~h2kItr7jr1 zn4G7w^uaJfuuJ+%4(O+t^;u=0!)=}GJa7BGHFiS}>lb4`IEC}_uR3{U+kaFXX)!V> zpCs6Go>0|{Ogo1REZEZLSe2dPy~;0V*o~X=l!QG0*CNA8^ET=SZV#!e1UCT^mEx=l z);V{n(#>SQvm|#>@^g6T9sJmda4spy#I?f)|MIMHU_=|(_JX3Jp6f~Np@6ZB51qqU zSj%>qF;IsIO&}Q`y=mnxF#-;5N|Mg0M4wN*h&NsR2rt3$F)T-b%B zS;TZYsBEbk@pS$zomhFqF@30pU8k977h~lsp}cay1duKEnM_p;VZtaqw=;irscfUs zbQxQl0^>Y*SLnOWF8eb+=yVMBj%V z>07giRyRvC1VO*ZngwHm$>u59W8$mG+?-~N{CnwQse+E`fbC=G<`lz2T*4Dus9QVZ zVXGcjgJoB0sn1d?{|W9#Tm=rGxmlh5VWsK^i4bm%bV}aK^wgoDet7Ekcvv#P2S)fu zqyq&JshUe~*W3e>Qi?q17MF8L=a81Pw`(Js2DzPdqSr|Y$`ljxD;%veKOUB=n{nK( zVRcbfX2@^k#q6`p!ajW612XH3e`tX?=p?IM1>Y+&PDcH>%WC_VQlM!dmqgP-hyVPQ zAD-0AQzhNfLyZ$wN?ie&n3oY}9rM!|D(Ee-s=hjHHT>#A!lno#DP_lw&~^)Awc4g6 zh3c3@=ywC`j1|wsC6)nQQm_|A_{(ReL2%>Z2xL zD_vJlA$0|wTSn?NB?zJ4{!W?26|jx;8!+iaFab){4ez3wLKL9TN*GqKVh z3M5MwkMf*?U}aj*t3bh~q@|`1T2HY$vk)?F+M$fa7rrC`e#_PN*wguTTkVM;WQs_yR;PP^_)Izqge#8zZq^IJ^5zz zSsMh66Q8<83Ax{(JzwaWCb zE*?gc+N)UoYMM2yYN8Ye00|PUR25r7VJsL0O^p0STpo88?;=U?8)0tsY3Za0j7Cxj z9K_*j-;mI)b_8_IW9H&it6i2}Vv(FX-(YWz&5hwTxmFe@G19q^x$KbC|G}=wR3sEo!!c+Q1#r}p&*X3Ls(Ju2foj}W1^u0pMOXl)UWE9CA7H}g45*0Zb=b< zjHgFh21F{xm$K0F+J)n-EK+0S`!TB=`On|N)BY6`JH|uAz&0pZeo5#vWB2;YJSE5jB4CjdAs_3x5pS>pU|UGmT%p^+jr`qWP0fWj7ylIBgKo zx7Fl2$6XXpi>uGwMhU3O$3b^;r`~f^=ZpgtK@5iD^fgw>Qd}(w zDuT7-`_~H-x;g;WOk@>Z(f)bZi6lsPbT6x-ia=|uAPdT;ONS0CFudKQUxA;0#^!RN z4}<6ttt}ROWnUibqEHuK;?th~49i{OX+;_!`s`>jC;F;wEG-ur{-emwjaT;>9vzlqQpa7@RX(!-iOJvo?cEj+O;}|`B>E1^!2e5 z0h(t5riwqmPeV|M9gcU#s{Rp1?-#RhCvYVhP4QZS&z@zjoa;8lf_)9}4K>uQUjTBx zpq`okXzreF)WHIOA1Q($rTVA`%eQx{b+1N6%cnxbQm5!>* zWG|Osb!RXANPCBYGsd#^b?#TphX+EfaGjgNT(?uL+NumLpLJCnP_E%Reun-8aDVX9 zd%eXsXCIp%Mx*oIYiO4Nu>~eq;_g!3k$lcMl|WCFaBp3l32C%#gr;29>+@u~ za3sNlpO!#U@UF}}^?mvsQQ%2`=Iy-q%GH5v9DMDduc z9)pqQa!dVF85JFd6ufz)eBYm4nBo+9=@_l>NOP&YS;Ur{F9o|Gg>JaV6#wIl`~-us zHmXB5b|YQ4wTUVSvRLx}HJg^J!E8lT2yr;qV$|Q6?nlF<6X+P(2o(}-`aDz($Z{V- zv(Lf1@UF!d2yvimar6w)R=nl!{!&or0zd61`qHz+d@onY^hnoYahP-=Qyg>BPCgTo z{}abpnDk%*i~Qzcnt3IwsSsR1?I)@#gxkNfEOn1lF|8MY3Hwcd=SONwasHuJqM`ji z#y4`6sw4OGA=W-6Ks-6veY;f!Mv)=kgIHU@r?{ySB9rNuP?xN~pE_B9fq%cF5}MjbQ%@iy{rwdTl)-gzo?|IpBH&9?5z z#x9Wmx*}~6XvLfFWMF;T|Ksa5331-q^S(M+*b=8kYUMVI&woQgR-4An5FK`pbw3~8 zbwkhLG#wK(k%{$b!<%!#Y_}675)+{PIBoC`nV+A`Qpy4o&%;@szWUczf^2|$rDzgU z{P1x*c`3q+cZw+Eh1owrTpR+AW9HvNky_gR%jBvAMV(p)7RI*(`;#xt5dHnhygdjB zMJGx6e}QC1P_$2O$D5Ossts8CQ`{KAgFHfiw9Imnab~_bOIPr}S%a4Td;Kx-cL@+n zb(m}+bb-6Jl!ZAR6Y&PgyZcvIGQ^nKb+#ffb;*3As-He|Ot5sBVK9M=0^ z`G=qReev^~r(qLO;I&|l-F?`VyMe@d|4;5Na`fyj46w%im4A%Sx^XevIn-Q9`QWK> T=o#yBYY#@x;RgDRS-r3PSVl(bOAmZH+_NmRFG(5?xc0S`kC|t=@WrPE zAGu5O24(IyCaX5^8JZPQ-Rx<+pKu3C4vypr;^bEvVVnlQM`6R(x{!Csrc((r6|jPL zVOWn}*@V(f{sMn`sP4#1$k9w33|1id2nv-!AarObPVc|uhwayWI(1OolM#T;1-H1hjXL^le>>gSpKmdC3op1^snIh(*mQ$PtZ zwFQtUR&Y4?(a`2!aaopgz+M6&;E+gk@j#*SJ;OS6f@hNmUKKEWo#X~4yHR`HFu3Dq zDZ_XqmQOgf5ryJbfn?2Q5p;-1(xjyM@!P&)DiC)i1R?`tk%|<+`bJ~#RI1#5WirR% zk=Z!=JA?P{1dxbem0KZzn_70`G;WAU#m=|K_-8CL(B}Sn%ByJ=Si3EQ@9V=ya1kOkC8A^ zLicAu6cJv$v76fb5>V3ae5>F3kw8#AZ23DqNz2p$EBzq1TLRhEPfb`)-!#V#nO2AU zsGFcJw@$JT_3>B_Em$!!oQSNEMVAbm;&icx))I2Q9c zZCdeJ;AGddff^yWDnfH3q$jYRn0UCKFe_$0%sG;KQt%apvg)73#hosEzXj{bxS+As56iVc(TC z3QbQYxc^K)=_iXhUV@g!%CO-#>rSO^Jv^xr^d z(W&pK5f5+k9cg`7`LDV$Vh=vG%JBMnZ^CqOQ&`6|f zfKfQbS2wsLXP+Wmb!Ov{hN7ZKMrsU!P_~a9JC?*_V=CuH^C+4%5h^8xeN-Z7|G}n> z{t_6r{uMsuPMPZlmx9+>kt ziMyNe3hL&~O-G4w11AF(#Y)55^78WA^pE5X)XNiRZs&VijqD$(*Y(o-U}K!Bd54FT zB?;Btz7AIp{N)?@=`3Zd>Iy#F<@+u~(85Ftzv>lR0Z!*?bf5?yD|WVw7@KlgVH8rD z9K)N#4ljr&aIGsm;MI{$P@3{s5Of;*@e(VG$6k~(zd4T&3y$Vv*C>mk%%c>5`DSwy3}JmkOR09$ni*kHirmI+?lW(n_nOw* z<`$Vjm6a>c<;xf9{$)o;<_Z^1c{1O@ja1Oz1P-oVYJuGo46gV`6S7=0Tfqw9zb809 zB{RD)1_Lu5cl=K@4o7z)<&hHa`jWc_UmU^xVO=BMeqnWZ5EK+-G(++~ZG_zrz z(v|3M8uueJeAKsT;h~zFW{0(Gv)nGB{l{{^z}wr8TZnT7nLqk{J~LsGwp{iv$|(Z| z@=aMU*t}-&J&OVWyKu^vbnG>{c>B$Q;e-jQ_>>2#7I>_#azx(jx26JZ{;BV28!@fW zh2@hED3sxz>$Y3hkdi1@CbK%U%%#Th&MV0 zOis=S`KQo>`H8hF(g+`<)wrt|cLwlYOvtg%6NNHfv7|Ndtd!GdP*==rk%$okmNE8@ z8t6flBMLV#-1_91uic?R!M_$B3M9LM4U8D`DT7B5`OFl4;L zbQgBD_wiv}`vcD0-Kh(!W0eQHV!n)HY3bSG(mKmMb$Sohy!<6%D&9`|cBlikl=q+k z!mAD^yBKSR9GlWYX~gx|HMd;`aiJ>t6&6g6C_x%gqt0G{y*6BR(wZOPHY2lFGU7B; zcCl6~w*C&CO{r|vbI_dvEJVfCBLNXeCKnn)y9g^>$~blar;==Ouuzg`i2Ny z5qfx(j2z;rE^2OOR4$bg^Jes=Tn9$&%3A)LJ0e+Q&**k9$_5K=H??r4UlKtt2^}P5 zu&s``H+to=svHAzZky){X~t7>dW5f^B)+e2++<%b3^Fo%%+nkskb8eYfgihYjrw~9 ztBOVH&QbVO23=KSoYqu;f7>jNj=~5YHMhU~F_k)xN4MO_A|rJg`a63`p}$~5 z!GAO|rpkL!;>AQHM9*s6o%6v1VdF$z{+S`p7o9{v@3`l-thz~dz-VvOPMOoH;qxs~ zGDy~RG9ih}6#;iS*tA1jBxO<}Mt9x98E412rN_(Hz+w)gE{OgemL3pg9LW_R&!Fv=+ z!%PWu^e~G#)$m1iGu@!5M@lO!dv6Yu^DR+_f)Meaj(Uo^VVzXy*{{vEUC$H|Du$Z` zn1)ltO*zQ4Ih^hMpGdETD0iIP&jLhuAoRQoI?wfk=)~3RT6Ee~j8P~gs{6K@Z8KF< zA`8K0s*dbp!&g*lM!8c4)MZ}eE(GFChp3WqlVxjgQ?s=SO-!8Xd>k&cLRGffI7o20 z=trxaHTE?+0h>3Ad;ded%waL?%T8n$QPrgNTFl$>4WV?`AUYnL-*e-2X#fmoE;q?4 zcVJplYU>uS%p3>hx@~Sh=P$rthc37a;wy4r`qbgEkwLM>CabkN2%dwYWJ2*dhNi^| z`jE(oaiRKFUme8t7<3xpuDjUL`zCF1KdwYrPmk?7k7MAQZldyk*-LI0F|{`H>zu}~ zIL$3UIYz)PaO)6g9_knpDQnTr;>kXazJxNgpapX%1fpg9P%`H0>*=x|={vQOc8chh zu|vt%wllAz{0}3$`eVHbw;<8S8zTfI><%M5cxAQn$wwHR#Re;zyZ+ss=9?bFT&Ymw`6%W%K za^Brcg1hh233)uo*sR3%5$lvKyT}*3vGwNKs!$Qk{+H0jqMMM)rK2agC*OJw_uqYD z)Z}j)ccej8WAE7Y5o!OtfsZcvZW+ORl;m>U4ZJ|PH6>?R{DPR50kE~kM6EN zrskz(J&l+xQRtod;o{heAAQjWKBQyv9FWYd{`or?JQ_7TNHAFdeOi_U>%H&67B2f( zzC2qrR96d^oJ$He2jHv~<>lEkK6Nb4QTq$IH3#IH%+<0zcCi1*yK0~U!yi8OiBg+1 zL{j)+S0rW>r$R5yg_v5VqES|LDc|lkA-1kIS|T<33%&@2b|HhPuTN7_09%lNE{9wtkl1c~^{HNZx{YgAxqb%9cf{ z<@|qY7kYJss9>xHpmQKB!uAo%qJ{^IzlExG#o zt-2fuv#I{KBPCU-`-q_o^h$&Co$5UmTV+#a3xs8Z^NhB~C7*Wrri`C99%__X8@R43 zi#VgomlE}l1rj(5;{q}L`~JVM5&pL9u@q6NlFL-kXhWd>h)P9dmZi|q-(22JQq99% zP9Xcb5@(W{;)Ba|{?FB(Z4ZCiH4dC*5%+2KYxihr*uc$A;TTFRQ6hixe+I~Mh1;$E zgehSZ>*xj!T^b#$(Yy%51O^Z|Q<{oVSC=EOw!%tPSq;v&$lBroyqhId5twwzhDJ6n z9s-HZh7Hb+b@f}Mx@Pw~%_Vzbg9w#dWDrw3;yG@)rs8Cle!D<_U z@Y_o!C{*y{dnJL}Lg7*v6e=8sJ&hYs@_!NuTT-cntr7lB6|q3X|H{?WFQ`Q#1eH!PGuM zcyY%v&XLOD5AeU0A&PWQ*4;|CuGn+UEWO?@;R>&)5f>s=-jfa~jN+0S;>6EQEoBDx z&oI?KVg{GD8}`4yX(8rkaK~xW4fR`%EJsLSFVQ);`A>ZZB`lOl<2K@e?ojC)c zlpvLcX{xpB3UwfYf!thIsP>D%&X9vO`qjV71)v~Fl?)x>ZfgA1>k5Tn5&i_>Hrud>7gKC)alqnU$m!EQ0V=+U1i~gY;xxU6TsU`{Hh{w@#fFi_ zsXDc;aPRLhZuS!Cg<>_{xpf5v|2GQzB+@7V4q`nLT*kG0UXADL3JUL=SsXBb3gkFK z+-=~WGiO##5eabCR^<0LB_u1r&3||A?ipZ&;>>A4e$0KQgzdD@eKdLQUDUbLzk?Ug z2wpfnXJ-0m-m0G=0VFT#FMt=pWjS-^oBtK?d1pXNddEG{63)ZlMnOSQTVF3LE4zPi zFka<;P*%Ltnb2^JPE5K!d(<`Tq|7)i;WocV48Lnd??340fdEU5FD5lTJ=ZM}0~5z| z{$jcosC=yr@WFNS@imUS{U*MAUtGMiY}R{H(a!kpy|XA*(g@zMO8%1_ zAJ4$cyVjW?x3aSG@fxjRzTeNM3rLZ+hY@ z>vaax=_w73-pwtDcc*d`yr$D)5L}kgk&%`0YLzdcz~N&F;MhM_2%HURaW7FYyaYeo z+}vPsT3Bo(&uKS@G+<-1Xd0{ZDf=7|7{%`ta+l2fN7I{ZFQ>-6Hm$$j}Ikr43N;_ZpP`xEaZg%7{ z;9I#sZU1v2+B1baXLPCJ+so;m&($}~H8uN}IGAKUgdV!KwKfd4=8xu?F3xwSczSx; z+1Z^O?HK7Zf`&sf7@{gUIr-ty(RCW3NAC4kmAJBE#9h zyJGCT2bwwtN;TuXuPQ8|nybfX=oSEI@bw5g zpm+56+S*!54(+CNf&XR$Hfnmi0R;3>A*CINyO{R9A398{GhCXu?1XGN^XgQJOT`qCeI|7&qm|m~DZ_gEfJ39B zhCZ52S2ny7LMD+ycaQ+6u^zp>)u4vDm7~U*j}2L4DE(18g5HS{FM)&RGBHbb=Ef}B zg3K%9e9vPQF-5{&yf*n!Iw7}oydmJD>Q^$*`GM~FU4)7%v)QC#I`kg{@~p*3HB>ec zG!ZRCgVK9(&}))>Rjr$EWc7<&MRn`BXh8_ztH4%}9t|WBIUlfk#em}m@qw2MGy~`V zEbNokl+^v8la0!|LN5M3*~gFW7IV~vu1d#K=&~9n5hp%$iOY?`3byP&-lK&(iSuo6 zEuH;i;rEZBiVj}z%Rc%Q_N93I=JRfwk3q2kO6^&g9r|te$bZzyx#pjy^tiv!mD7$*TtjmhIuYk61J3=i1xZyuyy;lZX*thN8}@9;moH zixoFAw0RO+rFJ{HebDL0k&NwyLRR296cufQ-vWv+ zk%E&{0wIfRs(Yik-SGGGQnN|#-~`a2r)3j_`|GV1+uf^nVx()6z+;oFPM%b)avR*v zT-I=@nvfQ^*xFL(F}-rr*sXIcvL~!^5!&h=_n(AYt8UnVe-^4z`L`Gtn z_jK2du4E{1XjzZA{;s{?wCegVftL+a%2fEC!%J%;fkL;hQw^md>+nK)W~-dg&@nzx zGD-Y3O)~jXGX3oen|_J=n>x;FuhEiV2CvHI1;{dnUz9OB5cgTzyllHwVK*_A zv^Bgv%%vl2u|Lvj(!iF1-kj%PdcY?9V&aA8&3asNN3N58zE#U{{>pLjx%l|U<8QCC z6l#%sVc%bi&&oOixIRBNzczN~G@6BDyEwhpm&i>)+J?v4Te-cQnrgvux{ol)EseO# zX(#|#EId=UdkU6rip}XqQ6iWx;?gz3uG5 z0>7)SA2sb}xEN||f7zSdzQRRY3D#4uZd3z|JZiGCfnv<`j9nIH!kD5qh7`@H;w52^ z*6AUpRk|j5d)pPs?t&Z%1tbj zdo3E{A0HI2z{RH*FkM^CxsSuCpk?SIE$VV6|Choqo1Z^!$*I{~q%r}}-|brS41fDA z{{lTq{Iq2e5?Z!6F<+B1+Eg;(u#;|0Jes|%l91Qr-I7kAR7Q*6A74=esSo>4*XiN% zPoF-f(NIxY1it1O>xwiDQ+n!0zv})|W_uQ(6+S zr$v60fk-0uFLQd$IB2pJyt7UsvZ7cg$jr>lrlfQW(UfE4e;;0yPtjatS}Bj0%9kOK zp6T{|d^}cP{`2S0&K#t?;XXNZzY6H>7Uftxz8xMJi`UW6JTZ3|R#|-wMxxYEwK=~M z8qn4CZCl)W##Rt)!nSEB&mKgY{O?d6qpBCN%%`u)!FI8E-O;W#Ds{wx6wAyVt>l@o zBeJXdg;pCsm|A9rMIVXItsFHh-Qi}sj%8(Q;b3J?OAR5UiQ{Du0GS}*I1%@q-1q!d zIe%NYb9(B`b;T!S*Hu?oa_)2NbeF8*Dv#*wIK8Y!igwD3Zge8orw043P(fYRhDLME z!la+f4vMxH8r15OgBbpAo{jJu5Za!@o$FLW&kYIHRX-cwv^RA7joYz#fp2Xv>jjal z(U4Gepy;#c_)l}d($$R200eP3qhx9&$*yTd3Jix42r1nA{!q)C;r(Av5)axD+%_;p zLVO0{gzU`3X*w=sf#OI)9@GWan<&}&jw4&vLbrhASxva}Ep8?*gXdcyakF28J%BrF zsrDxltmp)VGaCiNrA~Pf)%~Kh!@UWM#PrB|K+Kg4i;dYS3Dp}bv0c8iE>yj6yCrOk z#X$Vc5>2WaD@JN(Wwg@m+=ecCuFdtknpzjzUZKC*98&yz$8b=73;YH|9CNIzrGs-C zbD>JkOo#V7+E^wLMI~*L@7x}IO2A_c*DyociX!a!SONirt*!dcvqG47mMZ!^!=S)W z0gbhQ$HE|o6DCYbMi)VSvfK509%!AUy+^O-zn?Q%&1v3bJ5>DdmJ?%NMuE&?WBVttaqD8T8xQE_T)L{IfUg zTw0fPIVm3Tt2{m`+IAm(H;*Tqx$sXIpmitEJjlH|NNBtGYu2N;`sm%Vq2F)5|LZXL zuftRFYtfIEEN^lK@GdRhdhNv2z4~!{MQKKKbFnc!=rz=VbJEnz@1|tHRX#MSe)}eg z(Btq*p26EM_APgA$cL4FX&r24ProZ>h1_s@{;Ui%Pg84&$FPFJY$O(|roKJ~AX1vf z=|6A!5(uxZ!EKwBTvO>>*?L-yrJlSMLm-xEwOH9Qk|=xlEmHfnA#H3YkE9j0K4jeG z&%gMAxrt-DgVW>008yKfe|%p@<648-+m_3N)d2wK{O|MOl1|7K=G^nCuZuCg@^|fXtL<>mu`+3^-5EUc%0Wz@HNCVMoHEs~-1^i-4A6^qS`jr9GTv+}?|80Az=YB#C7G(N8>&C% z#_*|jy{wVr);ppGQ@AFFUM%g*B#V0Yg0I8y9pNm(wnaP)&+88Nk!KknYFp%P$VMh~ zCI|iK8khy-6%kE6C9jtS)cq8C66Z?Le$h4T=^P-gpY@Ja{Cq-Pi(mnfIO0?#d0FYo zQH3JmmDOznh^YMMTjSAfUPrk^f;dIIP+4E+Xu736{G-dBqzU z#5f6gdol;ZZ*3|kzdK~=h;mN?5``{KJ-nUox;Ymdsi(~R+Elaf+XM>T&I|?f?LnT6 zrKS?<-S-1jAN;97jokfK80SNdh6tM`9DTKX z*>fx`@9E z(7!fSOs?n+r}#jFB|cz19EgEv)I$kaffC2M!+ZVU*-Vn!Llvgziam|$dW0c-W>o_9gPGc8r|jfw*pLx9_-o1a)zuDJzE z&uF`Tq^!XPi2-#%;1QGJr{Q&p+Q=QgUgM4BE6QX6pf2wf5IG|r&#Jbfc zuSEB~1yOH~$UDumMUa1Cu#^)Eau&G65&`s@qTH-Y6L5I4=QPz^$>f5R3LvoFMzGOm0o`;eEAAk z%;{z8_03XL2jiFA>w7&X4GAArN*3m+xecn!j%K3$FGuMh26xC!JBgZ$#n))W7;`Fu z!I^9;@nkL!=7*Q>h|g3?HMO#HJ&cM_Z;K~PJzOXE?UCH@=UUvM5lKj}e6jk~wyx}r z|DZi_Vil^GtO6!DWLKdfo9TfD2*A9(uzZU4+shnc7_{uoDq9wz=Jnm91rU!nVPJ$V zAYibZ2e91C*a`+4A~WPo%#<4)m=jv8b9#k*ewGQ_Bx`2@MLKfu`G%%b6z`o3qBK-0 zZQgCGtc~3F=zOf4>V}i9)!uTsN@|m6ejIk!I2P`1Sew}0OMGC&55OvaXSGaIgHZb{ zH%7htQ4(QKGqz|+Y#>3d17s`06#7SOe|gU4_UMbwMPWnOws(b9)~)ZEpub1xBy_hl zURfMXQF$V-b|(s3Z>B}-RLT7Cq0`7Td%0&ag}^l}HHF5k=@L_S9JW1DuFacpxlU<3 z$%!6xg!P9FL?j=uO;`QQaG;TQGF4$^)3kct?#^#cuj582yITMOgwcBM$1)!O5xt-u zK&&mkjOiS2cb6#*Gcq!2YV;Nqe7yD;CW2YHWik^Zwrp-7etza?!3Vp&z|=WQ{qJ|) zBrbluTiB)eiudtpd%MFsv5y0l;^8v2R*AVy-zT$|fCgq-VfR6^f6w!`@WJqr`k?s3 zx(k^s+>2hnx^DbANQnVDw3+3)GtZLq;x!u6JA1j|ZCBgDOD&waEw~2?owi;60ixG< zlM#NGyZIe9KePN`hCuvHTis;q&8|ZD3%}Sdgz$u`p?ZpSl=S)kAJxuffHPKuA#5+v zWegg5fs{WUTc9kHkoBp~2g2;UEX7k?Z6>(6*fE`24;OO|FM7UCZbw~*9h)hRvIb`N z)_5lumW^kn#-rQfa{4+mjS)+NWXk$ub_1HSS}T$$v}FUcjd110luRrowPQD%*YORg zqG0Ui^>u|tmdiHqv}{1>4?bGZgCwPT%Aa!3%m3_^Y0=_`=cBn)KJiSsy?5)@kJ8&d zh;HDyw_)k`xe&tAXeU}}N9c~6<;8F&5mWsaE~}gK=u~}Yc@&w&So3oI0KV)VH6`X2 zW4B5kU>w<7i$Ls!sqDW5fnyE0!WIGJJxt9JB`v8+K(F6er#OKZZ6}yWJWMSPTZ|x~{M$_2m>Mesaa)MmC6?d{iU|Gw71HnQ?jfe#9P6Q1@>< z0Ae-T^bkfM#Qt*9<>%icoR9T&XM?HRD}rLLpB*fr4i<2W@{P_oZ;j(woUZj%CAL(6 zCMHaGH&6!A)IeV;@6(j}S`N;(raAiaaZG;ZU8`{?Uz_qbBQ|p@-Tw?hhwh8*F@o-p zlncD*M%u7@g%#&hL^3~1LISxxNJ1M+T56inS{1^eE2?Pa-lG>cY9z@ZbRnNqTjL2( zT@h{T(bXZhkh0|MX-g+juGr0guYx3gaz~8Zs2oSXbw3e|l}keRbc6+STzf|G5!&TM z8;6s#p3hpddiWju{gvpwlGL2~;=VUOo*Ce^;tuEVs80?9IlqNp0GGIDupqs({+4TY zdbWGtXwZm_(sk|wU&}vQJQ|or0hnwUXp(c?K(c9M^5Z>BQs1pBT)87GlJ)$lLGR3+ z+aSSwCtORKd-ZFCz0xvaDS+_#LXKa$1TuV|3TFdKB=$m8;>W3)iNsEmHkERNgh471 z7yrSuEBz<|0Lur1-G?cF6#9n|!6{}%4zzr$>r;z4G1nAcv&qT4^t54gcRS)d8C%_K z6#-V@y0a2qalXVdHK4LPWSTnFl}oIY6JDI%%?}Hg*h`crl|;Yg5Y!lSYTQ%81zHX- zGd~YD*e^07Nska6t72PfucY1aPQT04`WUGNXg;Rl?OJZ;vc;@Q*bdG27fz8EM+$BI z(xkU*xonrY+$f`bJhyW{LVuRJcqRDLrzMFOH-dk}F9h-2whL&8Y-(&&KEPNhD~Okq zWIDFrC&ND1eHg56Whs6l?bbdwGpiCUESR4fIC6zfaOJunUs!NxzQLTlMQZ05S2W)Gx!}>th~N?Q;+%E;N|L z9y3>GM%@HFqr@ z%0?+ZZBdR85#`JBEj!y$axhOF2x_vTOv&^16}er zug%%;HE<;d<1_*TsJConUFplxGm}&z`t7DYHWTI4TP`rBEv;aA21Gh8Z=bjIvP8|N zrlz~pw#y=*q?j2T3! z2@uujcyYoT9sz82CA%j6N=T2y?nIk^W`gkivei)}fT|n&&8yqA4nEJg9u)gzSN*KG z5ZGy9YR)sdG@8;e8W+(A(%mA0cI|GL6`E)dltVzewzLY|%J`gUx| zmmarq5O>lqTN7d~-&S=_1{J!eOoISqS)Q%H3tN;zu`j0`CKYvx$d?3$N)ZUleMCoV z{hZAB1)I^6Z=Lz&puE#*ZOA*gUZ$7+)vH$OzamN{kNfGlE2tO4(L>J$|4P-BJ zFl})%2s6N4y9H)3SHj-*5Fe&+Ba5Gysd8aFkj`Cx1DRm)@n$LP)3BtHL5oe@e&#PH zf1}nCOV(0J>^&$iPjg1PrO06!cUD}W30z^E=&g&dzDt4dPYoU(tUY;BbvX#k(XCh5wQw?T0;9f#LZ_MM->QF ztmjl?_t_24uWb7>&e)`tFDA8^W}p%rj2M*xW_s}O5s%qlVJFA{PaHdVTbPt3#6O&8 zLhlf~K_ouyZ%R656-ovPip!sq^c_jE)OY5cQ+v7dOq_d(+M)DC%fcfm*5gaRobcGO zD*9)1Q(@KG$fXWdgo?p$<8~I)@XGPLMI@N^c3)W>DESRZRLTFtg=g%pllYOZ)f*M@Wc2j-yh!g zSK=Bb5XN?D%AHo%U?M^8llvtjWe$4RH&?m^`4VJ>HWY52*4{q(`T0JBC5L%sU|Lf8 z5m#GC5lQS+W1W55)m_2|JZ@x1Q9ONL0v zn`~2o-U=Md(8x$_#ESRLhXK*oqm?#k*brTmI7kJkI^ljyq-NdSgi)(x_D=gt{Y6I4MzP) z?OiNax#_D{Psb0xt0Sy;FR-(23W7ce(EkleTwQ9enwn1czzQ;%m1eUEuf$wGr)OBR zB4TosVCO1PGV)m=}Xx|WuxCAVPW(ADnEZK2!_e*JynA4H*Gk+t9bwG z5#ph33gfhf;1Ps{ERk*M0l@_&T}Y;0_8Hes_vrI`QaM_z9{2n=i)$Gs!p;L?2P#GNB2VKwztm2%8K&w^aRxp74LuW z2sjQq(w9(E>1rI6zW^CbQcehh_pl%>HWGIytMRrUvrn9v;WQ z$x*ShzJxx%#@xC>DA*bF1&a;q5|&6y@^qX!WaNfM%E=)tzQoqrM!Io8_sb_>uviIj zd2)CQj{eki#H6C4zTsiljWDhRIchMts2d#p!&nayHMf;k8ZIX29CynTU^lKz!Z`^8u4CYu+QkwSvJqXN_c%+E|d zUh!}ZGSJUAja~jliA(9=7brw(cF%!Yj^j$tkF58Ccyo(#M=MNQ{)jmMP|Z;dT1jrWJn7D0^*!gQ%-Qd_@HCr=DJH(Y!1dbz8cxovWaX%;o=%JY74f^=CBc9$@ah%gO`BYvw~k0ewJ51atMc~@ z&YRRevX#D`FswOFC+<@9todXiuJ`cuRa*Y~rKvqjJPB(`q^z#-ZNmcaTAp zK9U4UZ+}m>gh!fz2IrlBE2Dh0b!%~qPkGcy%kimRW$2^AtT%;gS%@ zzT4?`D~TX2o}flv?y$JME5m@Hfrcy9s_M!*F8G&PTqp#EwO~O2)R`aFT7d9cx7l$> z$Vu2O;@8~@uptF^J>7EQOC?~B1N&37)Y}cqUK(5R+XXI1DwTuS7RAAJ4fXzJRtv`1 z^>qjcpS%6^%5XAk%?;H?>Ms>wyNC@x!2&5DG;~pL*zD(rw0vcQk_R8aUasN)dby&o zzD_2nh7AQg7F;f!4x`KjQGeZFdUJlvY}IX_LCvY}m%{-LJT~1DZ8<+zYOuAljuDgA z*_!aV(niZk@38f8cu2H9IXt@DEyF1mq`B)uz(J!p(o#!ZcBPZ?j5Aezo%{o?wO(RY zrK~6aiqQ!@w-{H|uKpG-t$AK5#2XvPUz}HD=iQXLx$dg?%6_$T<42TA^X={bx+0!I z0Lpr|E~DN<)K$tSa356atJSfoH$AQo&3KjCzXYELou3zrmvUpOKj;Rt zKo@NHmM92xBsNBXP!KPvKLYJJ1N6` zx`-y`B>s(wi78e#@fM`gh2x2oRM*>DR`j8m6C8nvYTtO>2mVRCz<$#kTV}8o9u{m0 z!*c5sId&E3;IUaT0>Nf`-qf_MeRlf{;1UP zgtzMty_I+pU_Hh^G|FXKs?c-2cc3mmdi`V&`5;dzZEI6(HQES71#WVBgG-m$K4ipVnWr_jaM~k92XC$FRwIW<%{*px~?P|}s{8$CE%j#Z9eDB;eLgN$ zF&XV~q^y>*rhz4Rh^|7SMeMS%Vb}I{YttlB?)IrGU}DL5E<#l4{v=cj0itQ6kO-2?keMYBO7stI7JBM0T^lK;KxpDe<;w$pZ9d9Z_akcu5_vVAQ%^``~&-B&cI zleX8I0((;j|MMZn5X!20Vzo4C(q%t@g!b&;6v!rpf-`5Lvk5;URMPK)@NE?c-zwJf z#cJ^U*OGO^A4_fk2hwhldSpecCtbi+-ap2G{J(;1c9%Uyup9DE2LxDEY;Gt3mt_$b zDs3u%gH{V}u-$*Xu+k99VG{Za3YBU9Yf@J30gGWL5_|%Fp3DJ-HF~FC&zxSO1izDz zYtrXb=L`M^aW4JicvUIl8i}(Knp`un5 diff --git a/main.go b/main.go index 8fef988c87..cad1336ee3 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,7 @@ import ( ) // Version of Vuls -var version = "0.1.7" +var version = "0.2.0" // Revision of Git var revision string @@ -45,6 +45,7 @@ func main() { subcommands.Register(&commands.ScanCmd{}, "scan") subcommands.Register(&commands.PrepareCmd{}, "prepare") subcommands.Register(&commands.HistoryCmd{}, "history") + subcommands.Register(&commands.ReportCmd{}, "report") subcommands.Register(&commands.ConfigtestCmd{}, "configtest") var v = flag.Bool("v", false, "Show version") diff --git a/models/models.go b/models/models.go index d5e75bf0be..1ad9976863 100644 --- a/models/models.go +++ b/models/models.go @@ -23,15 +23,13 @@ import ( "time" "github.com/future-architect/vuls/config" - "github.com/jinzhu/gorm" + "github.com/future-architect/vuls/cveapi" cve "github.com/kotakanbe/go-cve-dictionary/models" ) // ScanHistory is the history of Scanning. type ScanHistory struct { - gorm.Model ScanResults ScanResults - ScannedAt time.Time } // ScanResults is slice of ScanResult. @@ -55,43 +53,111 @@ func (s ScanResults) Less(i, j int) bool { return s[i].ServerName < s[j].ServerName } -// FilterByCvssOver is filter function. -func (s ScanResults) FilterByCvssOver() (filtered ScanResults) { - for _, result := range s { - cveInfos := []CveInfo{} - for _, cveInfo := range result.KnownCves { - if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) { - cveInfos = append(cveInfos, cveInfo) - } - } - result.KnownCves = cveInfos - filtered = append(filtered, result) - } - return -} - // ScanResult has the result of scanned CVE information. type ScanResult struct { - gorm.Model `json:"-" xml:"-"` - ScanHistoryID uint `json:"-" xml:"-"` - ScannedAt time.Time + ScannedAt time.Time + Lang string ServerName string // TOML Section key - // Hostname string - Family string - Release string - - Container Container + Family string + Release string + Container Container + Platform Platform - Platform Platform + // Scanned Vulns via SSH + CPE Vulns + ScannedCves []VulnInfo - // Fqdn string - // NWLinks []NWLink KnownCves []CveInfo UnknownCves []CveInfo IgnoredCves []CveInfo - Optional [][]interface{} `gorm:"-"` + Packages PackageInfoList + + Optional [][]interface{} +} + +// FillCveDetail fetches CVE detailed information from +// CVE Database, and then set to fields. +func (r ScanResult) FillCveDetail() (ScanResult, error) { + set := map[string]VulnInfo{} + var cveIDs []string + for _, v := range r.ScannedCves { + set[v.CveID] = v + cveIDs = append(cveIDs, v.CveID) + } + + ds, err := cveapi.CveClient.FetchCveDetails(cveIDs) + if err != nil { + return r, err + } + + icves := config.Conf.Servers[r.ServerName].IgnoreCves + + var known, unknown, ignored CveInfos + for _, d := range ds { + cinfo := CveInfo{ + CveDetail: d, + VulnInfo: set[d.CveID], + } + + // ignored + found := false + for _, icve := range icves { + if icve == d.CveID { + ignored = append(ignored, cinfo) + found = true + break + } + } + if found { + continue + } + + // unknown + if d.CvssScore(config.Conf.Lang) <= 0 { + unknown = append(unknown, cinfo) + continue + } + + // known + known = append(known, cinfo) + } + sort.Sort(known) + sort.Sort(unknown) + sort.Sort(ignored) + r.KnownCves = known + r.UnknownCves = unknown + r.IgnoredCves = ignored + return r, nil +} + +// FilterByCvssOver is filter function. +func (r ScanResult) FilterByCvssOver() ScanResult { + cveInfos := []CveInfo{} + for _, cveInfo := range r.KnownCves { + if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) { + cveInfos = append(cveInfos, cveInfo) + } + } + r.KnownCves = cveInfos + return r +} + +// ReportFileName returns the filename on localhost without extention +func (r ScanResult) ReportFileName() (name string) { + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s", r.ServerName) + } + return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName) +} + +// ReportKeyName returns the name of key on S3, Azure-Blob without extention +func (r ScanResult) ReportKeyName() (name string) { + timestr := r.ScannedAt.Format(time.RFC3339) + if len(r.Container.ContainerID) == 0 { + return fmt.Sprintf("%s/%s", timestr, r.ServerName) + } + return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName) } // ServerInfo returns server name one line @@ -141,15 +207,15 @@ func (r ScanResult) ServerInfoTui() string { // CveSummary summarize the number of CVEs group by CVSSv2 Severity func (r ScanResult) CveSummary() string { - var high, middle, low, unknown int + var high, medium, low, unknown int cves := append(r.KnownCves, r.UnknownCves...) for _, cveInfo := range cves { score := cveInfo.CveDetail.CvssScore(config.Conf.Lang) switch { - case 7.0 < score: + case 7.0 <= score: high++ - case 4.0 < score: - middle++ + case 4.0 <= score: + medium++ case 0 < score: low++ default: @@ -158,11 +224,11 @@ func (r ScanResult) CveSummary() string { } if config.Conf.IgnoreUnscoredCves { - return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d)", - high+middle+low, high, middle, low) + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)", + high+medium+low, high, medium, low) } - return fmt.Sprintf("Total: %d (High:%d Middle:%d Low:%d ?:%d)", - high+middle+low+unknown, high, middle, low, unknown) + return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)", + high+medium+low+unknown, high, medium, low, unknown) } // AllCves returns Known and Unknown CVEs @@ -172,15 +238,59 @@ func (r ScanResult) AllCves() []CveInfo { // NWLink has network link information. type NWLink struct { - gorm.Model `json:"-" xml:"-"` - ScanResultID uint `json:"-" xml:"-"` - IPAddress string Netmask string DevName string LinkState string } +// VulnInfos is VulnInfo list, getter/setter, sortable methods. +type VulnInfos []VulnInfo + +// VulnInfo holds a vulnerability information and unsecure packages +type VulnInfo struct { + CveID string + Packages PackageInfoList + DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD + CpeNames []string +} + +// FindByCveID find by CVEID +func (s VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) { + for _, p := range s { + if cveID == p.CveID { + return p, true + } + } + return VulnInfo{CveID: cveID}, false +} + +// immutable +func (s VulnInfos) set(cveID string, v VulnInfo) VulnInfos { + for i, p := range s { + if cveID == p.CveID { + s[i] = v + return s + } + } + return append(s, v) +} + +// Len implement Sort Interface +func (s VulnInfos) Len() int { + return len(s) +} + +// Swap implement Sort Interface +func (s VulnInfos) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less implement Sort Interface +func (s VulnInfos) Less(i, j int) bool { + return s[i].CveID < s[j].CveID +} + // CveInfos is for sorting type CveInfos []CveInfo @@ -202,21 +312,8 @@ func (c CveInfos) Less(i, j int) bool { // CveInfo has Cve Information. type CveInfo struct { - gorm.Model `json:"-" xml:"-"` - ScanResultID uint `json:"-" xml:"-"` - - CveDetail cve.CveDetail - Packages []PackageInfo - DistroAdvisories []DistroAdvisory - CpeNames []CpeName -} - -// CpeName has CPE name -type CpeName struct { - gorm.Model `json:"-" xml:"-"` - CveInfoID uint `json:"-" xml:"-"` - - Name string + CveDetail cve.CveDetail + VulnInfo } // PackageInfoList is slice of PackageInfo @@ -260,6 +357,34 @@ func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found boo return PackageInfo{}, false } +// MergeNewVersion merges candidate version information to the receiver struct +func (ps PackageInfoList) MergeNewVersion(as PackageInfoList) { + for _, a := range as { + for i, p := range ps { + if p.Name == a.Name { + ps[i].NewVersion = a.NewVersion + ps[i].NewRelease = a.NewRelease + } + } + } +} + +func (ps PackageInfoList) countUpdatablePacks() int { + count := 0 + for _, p := range ps { + if len(p.NewVersion) != 0 { + count++ + } + } + return count +} + +// ToUpdatablePacksSummary returns a summary of updatable packages +func (ps PackageInfoList) ToUpdatablePacksSummary() string { + return fmt.Sprintf("%d updatable packages", + ps.countUpdatablePacks()) +} + // Find search PackageInfo by name-version-release // func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) { // for _, p := range ps { @@ -287,9 +412,6 @@ func (a PackageInfosByName) Less(i, j int) bool { return a[i].Name < a[j].Name } // PackageInfo has installed packages. type PackageInfo struct { - gorm.Model `json:"-" xml:"-"` - CveInfoID uint `json:"-" xml:"-"` - Name string Version string Release string @@ -324,9 +446,6 @@ func (p PackageInfo) ToStringNewVersion() string { // DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information. type DistroAdvisory struct { - gorm.Model `json:"-" xml:"-"` - CveInfoID uint `json:"-" xml:"-"` - AdvisoryID string Severity string Issued time.Time @@ -335,18 +454,12 @@ type DistroAdvisory struct { // Container has Container information type Container struct { - gorm.Model `json:"-" xml:"-"` - ScanResultID uint `json:"-" xml:"-"` - ContainerID string Name string } // Platform has platform information type Platform struct { - gorm.Model `json:"-" xml:"-"` - ScanResultID uint `json:"-" xml:"-"` - Name string // aws or azure or gcp or other... InstanceID string } diff --git a/models/models_test.go b/models/models_test.go index a6fa25e677..0ef1d40e44 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -17,9 +17,14 @@ along with this program. If not, see . package models -import "testing" +import ( + "reflect" + "testing" -func TestPackageInfosUniqByName(t *testing.T) { + "github.com/k0kubun/pp" +) + +func TestPackageInfoListUniqByName(t *testing.T) { var test = struct { in PackageInfoList out PackageInfoList @@ -52,3 +57,81 @@ func TestPackageInfosUniqByName(t *testing.T) { } } } + +func TestMergeNewVersion(t *testing.T) { + var test = struct { + a PackageInfoList + b PackageInfoList + expected PackageInfoList + }{ + PackageInfoList{ + { + Name: "hoge", + }, + }, + PackageInfoList{ + { + Name: "hoge", + NewVersion: "1.0.0", + NewRelease: "release1", + }, + }, + PackageInfoList{ + { + Name: "hoge", + NewVersion: "1.0.0", + NewRelease: "release1", + }, + }, + } + + test.a.MergeNewVersion(test.b) + if !reflect.DeepEqual(test.a, test.expected) { + e := pp.Sprintf("%v", test.a) + a := pp.Sprintf("%v", test.expected) + t.Errorf("expected %s, actual %s", e, a) + } +} +func TestVulnInfosSetGet(t *testing.T) { + var test = struct { + in []string + out []string + }{ + []string{ + "CVE1", + "CVE2", + "CVE3", + "CVE1", + "CVE1", + "CVE2", + "CVE3", + }, + []string{ + "CVE1", + "CVE2", + "CVE3", + }, + } + + // var ps packageCveInfos + var ps VulnInfos + for _, cid := range test.in { + ps = ps.set(cid, VulnInfo{CveID: cid}) + } + + if len(test.out) != len(ps) { + t.Errorf("length: expected %d, actual %d", len(test.out), len(ps)) + } + + for i, expectedCid := range test.out { + if expectedCid != ps[i].CveID { + t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID) + } + } + for _, cid := range test.in { + p, _ := ps.FindByCveID(cid) + if p.CveID != cid { + t.Errorf("expected %s, actual %s", cid, p.CveID) + } + } +} diff --git a/report/azureblob.go b/report/azureblob.go index bf2ef35bf7..2d84a8209a 100644 --- a/report/azureblob.go +++ b/report/azureblob.go @@ -20,6 +20,7 @@ package report import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "time" @@ -27,12 +28,76 @@ import ( c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/util" ) // AzureBlobWriter writes results to AzureBlob type AzureBlobWriter struct{} +// Write results to Azure Blob storage +func (w AzureBlobWriter) Write(rs ...models.ScanResult) (err error) { + if len(rs) == 0 { + return nil + } + + cli, err := getBlobClient() + if err != nil { + return err + } + + if c.Conf.FormatOneLineText { + timestr := rs[0].ScannedAt.Format(time.RFC3339) + k := fmt.Sprintf(timestr + "/summary.txt") + text := toOneLineSummary(rs...) + b := []byte(text) + if err := createBlockBlob(cli, k, b); err != nil { + return err + } + } + + for _, r := range rs { + key := r.ReportKeyName() + if c.Conf.FormatJSON { + k := key + ".json" + var b []byte + if b, err = json.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to JSON: %s", err) + } + if err := createBlockBlob(cli, k, b); err != nil { + return err + } + } + + if c.Conf.FormatShortText { + k := key + "_short.txt" + b := []byte(toShortPlainText(r)) + if err := createBlockBlob(cli, k, b); err != nil { + return err + } + } + + if c.Conf.FormatFullText { + k := key + "_full.txt" + b := []byte(toFullPlainText(r)) + if err := createBlockBlob(cli, k, b); err != nil { + return err + } + } + + if c.Conf.FormatXML { + k := key + ".xml" + var b []byte + if b, err = xml.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to XML: %s", err) + } + allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{}) + if err := createBlockBlob(cli, k, allBytes); err != nil { + return err + } + } + } + return +} + // CheckIfAzureContainerExists check the existence of Azure storage container func CheckIfAzureContainerExists() error { cli, err := getBlobClient() @@ -57,84 +122,24 @@ func getBlobClient() (storage.BlobStorageClient, error) { return api.GetBlobService(), nil } -// Write results to Azure Blob storage -func (w AzureBlobWriter) Write(scanResults []models.ScanResult) (err error) { - reqChan := make(chan models.ScanResult, len(scanResults)) - resChan := make(chan bool) - errChan := make(chan error, len(scanResults)) - defer close(resChan) - defer close(errChan) - defer close(reqChan) - - timeout := time.After(10 * 60 * time.Second) - concurrency := 10 - tasks := util.GenWorkers(concurrency) - - go func() { - for _, r := range scanResults { - reqChan <- r - } - }() - - for range scanResults { - tasks <- func() { - select { - case sresult := <-reqChan: - func(r models.ScanResult) { - err := w.upload(r) - if err != nil { - errChan <- err - } - resChan <- true - }(sresult) - } - } - } - - errs := []error{} - for i := 0; i < len(scanResults); i++ { - select { - case <-resChan: - case err := <-errChan: - errs = append(errs, err) - case <-timeout: - errs = append(errs, fmt.Errorf("Timeout while uploading to azure Blob")) +func createBlockBlob(cli storage.BlobStorageClient, k string, b []byte) error { + var err error + if c.Conf.GZIP { + if b, err = gz(b); err != nil { + return err } + k = k + ".gz" } - if 0 < len(errs) { - return fmt.Errorf("Failed to upload json to Azure Blob: %v", errs) - } - return nil -} - -func (w AzureBlobWriter) upload(res models.ScanResult) (err error) { - cli, err := getBlobClient() - if err != nil { - return err - } - timestr := time.Now().Format(time.RFC3339) - name := "" - if len(res.Container.ContainerID) == 0 { - name = fmt.Sprintf("%s/%s.json", timestr, res.ServerName) - } else { - name = fmt.Sprintf("%s/%s_%s.json", timestr, res.ServerName, res.Container.Name) - } - - jsonBytes, err := json.Marshal(res) - if err != nil { - return fmt.Errorf("Failed to Marshal to JSON: %s", err) - } - - if err = cli.CreateBlockBlobFromReader( + if err := cli.CreateBlockBlobFromReader( c.Conf.AzureContainer, - name, - uint64(len(jsonBytes)), - bytes.NewReader(jsonBytes), + k, + uint64(len(b)), + bytes.NewReader(b), map[string]string{}, ); err != nil { - return fmt.Errorf("%s/%s, %s", - c.Conf.AzureContainer, name, err) + return fmt.Errorf("Failed to upload data to %s/%s, %s", + c.Conf.AzureContainer, k, err) } - return + return nil } diff --git a/report/mail.go b/report/email.go similarity index 62% rename from report/mail.go rename to report/email.go index a8ba42718f..e2b7c71ad4 100644 --- a/report/mail.go +++ b/report/email.go @@ -29,27 +29,27 @@ import ( "github.com/future-architect/vuls/models" ) -// MailWriter send mail -type MailWriter struct{} +// EMailWriter send mail +type EMailWriter struct{} -func (w MailWriter) Write(scanResults []models.ScanResult) (err error) { +func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { conf := config.Conf - for _, s := range scanResults { - to := strings.Join(conf.Mail.To[:], ", ") - cc := strings.Join(conf.Mail.Cc[:], ", ") - mailAddresses := append(conf.Mail.To, conf.Mail.Cc...) - if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil { - return fmt.Errorf("Failed to parse email addresses: %s", err) - } + to := strings.Join(conf.EMail.To[:], ", ") + cc := strings.Join(conf.EMail.Cc[:], ", ") + mailAddresses := append(conf.EMail.To, conf.EMail.Cc...) + if _, err := mail.ParseAddressList(strings.Join(mailAddresses[:], ", ")); err != nil { + return fmt.Errorf("Failed to parse email addresses: %s", err) + } + for _, r := range rs { subject := fmt.Sprintf("%s%s %s", - conf.Mail.SubjectPrefix, - s.ServerInfo(), - s.CveSummary(), + conf.EMail.SubjectPrefix, + r.ServerInfo(), + r.CveSummary(), ) headers := make(map[string]string) - headers["From"] = conf.Mail.From + headers["From"] = conf.EMail.From headers["To"] = to headers["Cc"] = cc headers["Subject"] = subject @@ -60,25 +60,19 @@ func (w MailWriter) Write(scanResults []models.ScanResult) (err error) { for k, v := range headers { message += fmt.Sprintf("%s: %s\r\n", k, v) } + message += "\r\n" + toFullPlainText(r) - var body string - if body, err = toPlainText(s); err != nil { - return err - } - message += "\r\n" + body - - smtpServer := net.JoinHostPort(conf.Mail.SMTPAddr, conf.Mail.SMTPPort) - - err := smtp.SendMail( + smtpServer := net.JoinHostPort(conf.EMail.SMTPAddr, conf.EMail.SMTPPort) + err = smtp.SendMail( smtpServer, smtp.PlainAuth( "", - conf.Mail.User, - conf.Mail.Password, - conf.Mail.SMTPAddr, + conf.EMail.User, + conf.EMail.Password, + conf.EMail.SMTPAddr, ), - conf.Mail.From, - conf.Mail.To, + conf.EMail.From, + conf.EMail.To, []byte(message), ) diff --git a/report/json.go b/report/json.go deleted file mode 100644 index 728239efab..0000000000 --- a/report/json.go +++ /dev/null @@ -1,150 +0,0 @@ -/* Vuls - Vulnerability Scanner -Copyright (C) 2016 Future Architect, Inc. Japan. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package report - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "time" - - c "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/models" -) - -// JSONDirs array of json files path. -type JSONDirs []string - -func (d JSONDirs) Len() int { - return len(d) -} -func (d JSONDirs) Swap(i, j int) { - d[i], d[j] = d[j], d[i] -} -func (d JSONDirs) Less(i, j int) bool { - return d[j] < d[i] -} - -// JSONWriter writes results to file. -type JSONWriter struct { - ScannedAt time.Time -} - -func (w JSONWriter) Write(scanResults []models.ScanResult) (err error) { - var path string - if path, err = ensureResultDir(w.ScannedAt); err != nil { - return fmt.Errorf("Failed to make direcotory/symlink : %s", err) - } - - for _, scanResult := range scanResults { - scanResult.ScannedAt = w.ScannedAt - } - - var jsonBytes []byte - for _, r := range scanResults { - jsonPath := "" - if len(r.Container.ContainerID) == 0 { - jsonPath = filepath.Join(path, fmt.Sprintf("%s.json", r.ServerName)) - } else { - jsonPath = filepath.Join(path, - fmt.Sprintf("%s_%s.json", r.ServerName, r.Container.Name)) - } - - if jsonBytes, err = json.Marshal(r); err != nil { - return fmt.Errorf("Failed to Marshal to JSON: %s", err) - } - if err := ioutil.WriteFile(jsonPath, jsonBytes, 0600); err != nil { - return fmt.Errorf("Failed to write JSON. path: %s, err: %s", jsonPath, err) - } - } - return nil -} - -// JSONDirPattern is file name pattern of JSON directory -var JSONDirPattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$`) - -// GetValidJSONDirs return valid json directory as array -func GetValidJSONDirs() (jsonDirs JSONDirs, err error) { - var dirInfo []os.FileInfo - if dirInfo, err = ioutil.ReadDir(c.Conf.ResultsDir); err != nil { - err = fmt.Errorf("Failed to read %s: %s", c.Conf.ResultsDir, err) - return - } - for _, d := range dirInfo { - if d.IsDir() && JSONDirPattern.MatchString(d.Name()) { - jsonDir := filepath.Join(c.Conf.ResultsDir, d.Name()) - jsonDirs = append(jsonDirs, jsonDir) - } - } - sort.Sort(jsonDirs) - return -} - -// LoadOneScanHistory read JSON data -func LoadOneScanHistory(jsonDir string) (scanHistory models.ScanHistory, err error) { - var scanResults []models.ScanResult - var files []os.FileInfo - if files, err = ioutil.ReadDir(jsonDir); err != nil { - err = fmt.Errorf("Failed to read %s: %s", jsonDir, err) - return - } - for _, file := range files { - if filepath.Ext(file.Name()) != ".json" { - continue - } - var scanResult models.ScanResult - var data []byte - jsonPath := filepath.Join(jsonDir, file.Name()) - if data, err = ioutil.ReadFile(jsonPath); err != nil { - err = fmt.Errorf("Failed to read %s: %s", jsonPath, err) - return - } - if json.Unmarshal(data, &scanResult) != nil { - err = fmt.Errorf("Failed to parse %s: %s", jsonPath, err) - return - } - scanResults = append(scanResults, scanResult) - } - if len(scanResults) == 0 { - err = fmt.Errorf("There is no json file under %s", jsonDir) - return - } - - var scannedAt time.Time - if scanResults[0].ScannedAt.IsZero() { - splitPath := strings.Split(jsonDir, string(os.PathSeparator)) - timeStr := splitPath[len(splitPath)-1] - if scannedAt, err = time.Parse(time.RFC3339, timeStr); err != nil { - err = fmt.Errorf("Failed to parse %s: %s", timeStr, err) - return - } - } else { - scannedAt = scanResults[0].ScannedAt - } - - scanHistory = models.ScanHistory{ - ScanResults: scanResults, - ScannedAt: scannedAt, - } - return -} diff --git a/report/localfile.go b/report/localfile.go new file mode 100644 index 0000000000..f8d6e20226 --- /dev/null +++ b/report/localfile.go @@ -0,0 +1,111 @@ +/* Vuls - Vulnerability Scanner +Copyright (C) 2016 Future Architect, Inc. Japan. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package report + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + c "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/models" +) + +// LocalFileWriter writes results to a local file. +type LocalFileWriter struct { + CurrentDir string +} + +func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) { + if c.Conf.FormatOneLineText { + path := filepath.Join(w.CurrentDir, "summary.txt") + text := toOneLineSummary(rs...) + if err := writeFile(path, []byte(text), 0600); err != nil { + return fmt.Errorf( + "Failed to write to file. path: %s, err: %s", + path, err) + } + } + + for _, r := range rs { + path := filepath.Join(w.CurrentDir, r.ReportFileName()) + + if c.Conf.FormatJSON { + p := path + ".json" + var b []byte + if b, err = json.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to JSON: %s", err) + } + if err := writeFile(p, b, 0600); err != nil { + return fmt.Errorf("Failed to write JSON. path: %s, err: %s", p, err) + } + } + + if c.Conf.FormatShortText { + p := path + "_short.txt" + if err := writeFile( + p, []byte(toShortPlainText(r)), 0600); err != nil { + return fmt.Errorf( + "Failed to write text files. path: %s, err: %s", p, err) + } + } + + if c.Conf.FormatFullText { + p := path + "_full.txt" + if err := writeFile( + p, []byte(toFullPlainText(r)), 0600); err != nil { + return fmt.Errorf( + "Failed to write text files. path: %s, err: %s", p, err) + } + } + + if c.Conf.FormatXML { + p := path + ".xml" + var b []byte + if b, err = xml.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to XML: %s", err) + } + allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{}) + if err := writeFile(p, allBytes, 0600); err != nil { + return fmt.Errorf("Failed to write XML. path: %s, err: %s", p, err) + } + } + } + return nil +} + +func writeFile(path string, data []byte, perm os.FileMode) error { + var err error + if c.Conf.GZIP { + if data, err = gz(data); err != nil { + return err + } + path = path + ".gz" + } + + if err := ioutil.WriteFile( + path, []byte(data), perm); err != nil { + return err + } + + return nil +} diff --git a/report/logrus.go b/report/logrus.go deleted file mode 100644 index c48b2a5fae..0000000000 --- a/report/logrus.go +++ /dev/null @@ -1,56 +0,0 @@ -/* Vuls - Vulnerability Scanner -Copyright (C) 2016 Future Architect, Inc. Japan. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package report - -import ( - "os" - "path/filepath" - "runtime" - - "github.com/Sirupsen/logrus" - "github.com/future-architect/vuls/models" - formatter "github.com/kotakanbe/logrus-prefixed-formatter" -) - -// LogrusWriter write to logfile -type LogrusWriter struct { -} - -func (w LogrusWriter) Write(scanResults []models.ScanResult) error { - path := "/var/log/vuls/report.log" - if runtime.GOOS == "windows" { - path = filepath.Join(os.Getenv("APPDATA"), "vuls", "report.log") - } - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) - if err != nil { - return err - } - log := logrus.New() - log.Formatter = &formatter.TextFormatter{} - log.Out = f - log.Level = logrus.InfoLevel - - for _, s := range scanResults { - text, err := toPlainText(s) - if err != nil { - return err - } - log.Infof(text) - } - return nil -} diff --git a/report/s3.go b/report/s3.go index 062623038c..754d757787 100644 --- a/report/s3.go +++ b/report/s3.go @@ -20,6 +20,7 @@ package report import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "time" @@ -32,6 +33,78 @@ import ( "github.com/future-architect/vuls/models" ) +// S3Writer writes results to S3 +type S3Writer struct{} + +func getS3() *s3.S3 { + return s3.New(session.New(&aws.Config{ + Region: aws.String(c.Conf.AwsRegion), + Credentials: credentials.NewSharedCredentials("", c.Conf.AwsProfile), + })) +} + +// Write results to S3 +// http://docs.aws.amazon.com/sdk-for-go/latest/v1/developerguide/common-examples.title.html +func (w S3Writer) Write(rs ...models.ScanResult) (err error) { + if len(rs) == 0 { + return nil + } + + svc := getS3() + + if c.Conf.FormatOneLineText { + timestr := rs[0].ScannedAt.Format(time.RFC3339) + k := fmt.Sprintf(timestr + "/summary.txt") + text := toOneLineSummary(rs...) + if err := putObject(svc, k, []byte(text)); err != nil { + return err + } + } + + for _, r := range rs { + key := r.ReportKeyName() + if c.Conf.FormatJSON { + k := key + ".json" + var b []byte + if b, err = json.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to JSON: %s", err) + } + if err := putObject(svc, k, b); err != nil { + return err + } + } + + if c.Conf.FormatShortText { + k := key + "_short.txt" + text := toShortPlainText(r) + if err := putObject(svc, k, []byte(text)); err != nil { + return err + } + } + + if c.Conf.FormatFullText { + k := key + "_full.txt" + text := toFullPlainText(r) + if err := putObject(svc, k, []byte(text)); err != nil { + return err + } + } + + if c.Conf.FormatXML { + k := key + ".xml" + var b []byte + if b, err = xml.Marshal(r); err != nil { + return fmt.Errorf("Failed to Marshal to XML: %s", err) + } + allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), b, []byte(vulsCloseTag)}, []byte{}) + if err := putObject(svc, k, allBytes); err != nil { + return err + } + } + } + return nil +} + // CheckIfBucketExists check the existence of S3 bucket func CheckIfBucketExists() error { svc := getS3() @@ -57,46 +130,22 @@ func CheckIfBucketExists() error { return nil } -// S3Writer writes results to S3 -type S3Writer struct{} - -func getS3() *s3.S3 { - return s3.New(session.New(&aws.Config{ - Region: aws.String(c.Conf.AwsRegion), - Credentials: credentials.NewSharedCredentials("", c.Conf.AwsProfile), - })) -} - -// Write results to S3 -func (w S3Writer) Write(scanResults []models.ScanResult) (err error) { - - var jsonBytes []byte - if jsonBytes, err = json.Marshal(scanResults); err != nil { - return fmt.Errorf("Failed to Marshal to JSON: %s", err) - } - - // http://docs.aws.amazon.com/sdk-for-go/latest/v1/developerguide/common-examples.title.html - svc := getS3() - timestr := time.Now().Format(time.RFC3339) - for _, r := range scanResults { - key := "" - if len(r.Container.ContainerID) == 0 { - key = fmt.Sprintf("%s/%s.json", timestr, r.ServerName) - } else { - key = fmt.Sprintf("%s/%s_%s.json", timestr, r.ServerName, r.Container.Name) +func putObject(svc *s3.S3, k string, b []byte) error { + var err error + if c.Conf.GZIP { + if b, err = gz(b); err != nil { + return err } + k = k + ".gz" + } - if jsonBytes, err = json.Marshal(r); err != nil { - return fmt.Errorf("Failed to Marshal to JSON: %s", err) - } - _, err = svc.PutObject(&s3.PutObjectInput{ - Bucket: &c.Conf.S3Bucket, - Key: &key, - Body: bytes.NewReader(jsonBytes), - }) - if err != nil { - return fmt.Errorf("Failed to upload data to %s/%s, %s", c.Conf.S3Bucket, key, err) - } + if _, err := svc.PutObject(&s3.PutObjectInput{ + Bucket: &c.Conf.S3Bucket, + Key: &k, + Body: bytes.NewReader(b), + }); err != nil { + return fmt.Errorf("Failed to upload data to %s/%s, %s", + c.Conf.S3Bucket, k, err) } return nil } diff --git a/report/slack.go b/report/slack.go index 655445e67f..d38e49ca22 100644 --- a/report/slack.go +++ b/report/slack.go @@ -56,36 +56,36 @@ type message struct { // SlackWriter send report to slack type SlackWriter struct{} -func (w SlackWriter) Write(scanResults []models.ScanResult) error { +func (w SlackWriter) Write(rs ...models.ScanResult) error { conf := config.Conf.Slack - for _, s := range scanResults { - channel := conf.Channel + channel := conf.Channel + + for _, r := range rs { if channel == "${servername}" { - channel = fmt.Sprintf("#%s", s.ServerName) + channel = fmt.Sprintf("#%s", r.ServerName) } msg := message{ - Text: msgText(s), + Text: msgText(r), Username: conf.AuthUser, IconEmoji: conf.IconEmoji, Channel: channel, - Attachments: toSlackAttachments(s), + Attachments: toSlackAttachments(r), } bytes, _ := json.Marshal(msg) jsonBody := string(bytes) f := func() (err error) { - resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL). - Send(string(jsonBody)).End() - if resp.StatusCode != 200 { - log.Errorf("Resonse body: %s", body) - if 0 < len(errs) { - return errs[0] - } + resp, body, errs := gorequest.New().Proxy(config.Conf.HTTPProxy).Post(conf.HookURL).Send(string(jsonBody)).End() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return fmt.Errorf( + "HTTP POST error: %v, url: %s, resp: %v, body: %s", + errs, conf.HookURL, resp, body) } return nil } notify := func(err error, t time.Duration) { + log.Warn("Error %s", err) log.Warn("Retrying in ", t) } if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil { @@ -118,8 +118,8 @@ func toSlackAttachments(scanResult models.ScanResult) (attaches []*attachment) { for _, p := range cveInfo.Packages { curentPackages = append(curentPackages, p.ToStringCurrentVersion()) } - for _, cpename := range cveInfo.CpeNames { - curentPackages = append(curentPackages, cpename.Name) + for _, n := range cveInfo.CpeNames { + curentPackages = append(curentPackages, n) } newPackages := []string{} diff --git a/report/stdout.go b/report/stdout.go index eb7dbaa5cc..ef963c58c6 100644 --- a/report/stdout.go +++ b/report/stdout.go @@ -20,19 +20,40 @@ package report import ( "fmt" + c "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" ) // StdoutWriter write to stdout type StdoutWriter struct{} -func (w StdoutWriter) Write(scanResults []models.ScanResult) error { - for _, s := range scanResults { - text, err := toPlainText(s) - if err != nil { - return err +// WriteScanSummary prints Scan summary at the end of scan +func (w StdoutWriter) WriteScanSummary(rs ...models.ScanResult) { + fmt.Printf("\n\n") + fmt.Printf("Scan Summary\n") + fmt.Printf("============\n") + fmt.Printf("%s\n", toScanSummary(rs...)) +} + +func (w StdoutWriter) Write(rs ...models.ScanResult) error { + if c.Conf.FormatOneLineText { + fmt.Print("\n\n") + fmt.Println("One Line Summary") + fmt.Println("================") + fmt.Println(toOneLineSummary(rs...)) + fmt.Print("\n") + } + + if c.Conf.FormatShortText { + for _, r := range rs { + fmt.Println(toShortPlainText(r)) + } + } + + if c.Conf.FormatFullText { + for _, r := range rs { + fmt.Println(toFullPlainText(r)) } - fmt.Println(text) } return nil } diff --git a/report/textfile.go b/report/textfile.go deleted file mode 100644 index d93a7e66ea..0000000000 --- a/report/textfile.go +++ /dev/null @@ -1,64 +0,0 @@ -/* Vuls - Vulnerability Scanner -Copyright (C) 2016 Future Architect, Inc. Japan. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package report - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" - "time" - - "github.com/future-architect/vuls/models" -) - -// TextFileWriter writes results to file. -type TextFileWriter struct { - ScannedAt time.Time -} - -func (w TextFileWriter) Write(scanResults []models.ScanResult) (err error) { - path, err := ensureResultDir(w.ScannedAt) - all := []string{} - for _, r := range scanResults { - textFilePath := "" - if len(r.Container.ContainerID) == 0 { - textFilePath = filepath.Join(path, fmt.Sprintf("%s.txt", r.ServerName)) - } else { - textFilePath = filepath.Join(path, - fmt.Sprintf("%s_%s.txt", r.ServerName, r.Container.Name)) - } - text, err := toPlainText(r) - if err != nil { - return err - } - all = append(all, text) - b := []byte(text) - if err := ioutil.WriteFile(textFilePath, b, 0600); err != nil { - return fmt.Errorf("Failed to write text files. path: %s, err: %s", textFilePath, err) - } - } - - text := strings.Join(all, "\n\n") - b := []byte(text) - allPath := filepath.Join(path, "all.txt") - if err := ioutil.WriteFile(allPath, b, 0600); err != nil { - return fmt.Errorf("Failed to write text files. path: %s, err: %s", allPath, err) - } - return nil -} diff --git a/report/tui.go b/report/tui.go index bf7ad6d508..ce5c9965ef 100644 --- a/report/tui.go +++ b/report/tui.go @@ -20,7 +20,7 @@ package report import ( "bytes" "fmt" - "path/filepath" + "os" "strings" "text/template" "time" @@ -40,13 +40,8 @@ var currentCveInfo int var currentDetailLimitY int // RunTui execute main logic -func RunTui(jsonDirName string) subcommands.ExitStatus { - var err error - scanHistory, err = selectScanHistory(jsonDirName) - if err != nil { - log.Errorf("%s", err) - return subcommands.ExitFailure - } +func RunTui(history models.ScanHistory) subcommands.ExitStatus { + scanHistory = history g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -64,34 +59,14 @@ func RunTui(jsonDirName string) subcommands.ExitStatus { g.SelFgColor = gocui.ColorBlack g.Cursor = true - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil { + g.Close() log.Errorf("%s", err) - return subcommands.ExitFailure + os.Exit(1) } - return subcommands.ExitSuccess } -func selectScanHistory(jsonDirName string) (latest models.ScanHistory, err error) { - var jsonDir string - if 0 < len(jsonDirName) { - jsonDir = filepath.Join(config.Conf.ResultsDir, jsonDirName) - } else { - var jsonDirs JSONDirs - if jsonDirs, err = GetValidJSONDirs(); err != nil { - return - } - if len(jsonDirs) == 0 { - return latest, fmt.Errorf("No scan results are found in %s", config.Conf.ResultsDir) - } - jsonDir = jsonDirs[0] - } - if latest, err = LoadOneScanHistory(jsonDir); err != nil { - return - } - return -} - func keybindings(g *gocui.Gui) (err error) { errs := []error{} @@ -537,6 +512,9 @@ func setSideLayout(g *gocui.Gui) error { for _, result := range scanHistory.ScanResults { fmt.Fprintln(v, result.ServerInfoTui()) } + if len(scanHistory.ScanResults) == 0 { + return fmt.Errorf("No scan results") + } currentScanResult = scanHistory.ScanResults[0] if _, err := g.SetCurrentView("side"); err != nil { return err @@ -666,7 +644,7 @@ type dataForTmpl struct { VulnSiteLinks []string References []cve.Reference Packages []string - CpeNames []models.CpeName + CpeNames []string PublishedDate time.Time LastModifiedDate time.Time } @@ -780,8 +758,8 @@ Package/CPE {{range $pack := .Packages -}} * {{$pack}} {{end -}} -{{range .CpeNames -}} -* {{.Name}} +{{range $name := .CpeNames -}} +* {{$name}} {{end}} Links -------------- diff --git a/report/util.go b/report/util.go index e236c7502c..667abe210f 100644 --- a/report/util.go +++ b/report/util.go @@ -20,89 +20,49 @@ package report import ( "bytes" "fmt" - "os" - "path/filepath" "strings" - "time" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/gosuri/uitable" ) -func ensureResultDir(scannedAt time.Time) (path string, err error) { - jsonDirName := scannedAt.Format(time.RFC3339) - - resultsDir := config.Conf.ResultsDir - if len(resultsDir) == 0 { - wd, _ := os.Getwd() - resultsDir = filepath.Join(wd, "results") - } - jsonDir := filepath.Join(resultsDir, jsonDirName) - - if err := os.MkdirAll(jsonDir, 0700); err != nil { - return "", fmt.Errorf("Failed to create dir: %s", err) - } - - symlinkPath := filepath.Join(resultsDir, "current") - if _, err := os.Lstat(symlinkPath); err == nil { - if err := os.Remove(symlinkPath); err != nil { - return "", fmt.Errorf( - "Failed to remove symlink. path: %s, err: %s", symlinkPath, err) +const maxColWidth = 80 + +func toScanSummary(rs ...models.ScanResult) string { + table := uitable.New() + table.MaxColWidth = maxColWidth + table.Wrap = true + for _, r := range rs { + cols := []interface{}{ + r.ServerName, + fmt.Sprintf("%s%s", r.Family, r.Release), + fmt.Sprintf("%d CVEs", len(r.ScannedCves)), + r.Packages.ToUpdatablePacksSummary(), } + table.AddRow(cols...) } - - if err := os.Symlink(jsonDir, symlinkPath); err != nil { - return "", fmt.Errorf( - "Failed to create symlink: path: %s, err: %s", symlinkPath, err) - } - return jsonDir, nil + return fmt.Sprintf("%s\n", table) } -func toPlainText(scanResult models.ScanResult) (string, error) { - serverInfo := scanResult.ServerInfo() - - var buffer bytes.Buffer - for i := 0; i < len(serverInfo); i++ { - buffer.WriteString("=") - } - header := fmt.Sprintf("%s\n%s", serverInfo, buffer.String()) - - if len(scanResult.KnownCves) == 0 && len(scanResult.UnknownCves) == 0 { - return fmt.Sprintf(` -%s -No unsecure packages. -`, header), nil - } - - summary := ToPlainTextSummary(scanResult) - scoredReport, unscoredReport := []string{}, []string{} - scoredReport, unscoredReport = toPlainTextDetails(scanResult, scanResult.Family) - - scored := strings.Join(scoredReport, "\n\n") - - unscored := "" - if !config.Conf.IgnoreUnscoredCves { - unscored = strings.Join(unscoredReport, "\n\n") +func toOneLineSummary(rs ...models.ScanResult) string { + table := uitable.New() + table.MaxColWidth = maxColWidth + table.Wrap = true + for _, r := range rs { + cols := []interface{}{ + r.ServerName, + r.CveSummary(), + r.Packages.ToUpdatablePacksSummary(), + } + table.AddRow(cols...) } - - detail := fmt.Sprintf(` -%s - -%s -`, - scored, - unscored, - ) - text := fmt.Sprintf("%s\n%s\n%s\n", header, summary, detail) - - return text, nil + return fmt.Sprintf("%s\n", table) } -// ToPlainTextSummary format summary for plain text. -func ToPlainTextSummary(r models.ScanResult) string { +func toShortPlainText(r models.ScanResult) string { stable := uitable.New() - stable.MaxColWidth = 84 + stable.MaxColWidth = maxColWidth stable.Wrap = true cves := r.KnownCves @@ -110,14 +70,45 @@ func ToPlainTextSummary(r models.ScanResult) string { cves = append(cves, r.UnknownCves...) } + var buf bytes.Buffer + for i := 0; i < len(r.ServerInfo()); i++ { + buf.WriteString("=") + } + header := fmt.Sprintf("%s\n%s\n%s\t%s\n\n", + r.ServerInfo(), + buf.String(), + r.CveSummary(), + r.Packages.ToUpdatablePacksSummary(), + ) + + if len(cves) == 0 { + return fmt.Sprintf(` +%s +No CVE-IDs are found in updatable packages. +%s +`, header, r.Packages.ToUpdatablePacksSummary()) + } + for _, d := range cves { - var scols []string + var packsVer string + for _, p := range d.Packages { + packsVer += fmt.Sprintf( + "%s -> %s\n", p.ToStringCurrentVersion(), p.ToStringNewVersion()) + } + for _, n := range d.CpeNames { + packsVer += n + } + var scols []string switch { case config.Conf.Lang == "ja" && 0 < d.CveDetail.Jvn.CvssScore(): - - summary := d.CveDetail.Jvn.CveTitle() + summary := fmt.Sprintf("%s\n%s\n%s\n%s", + d.CveDetail.Jvn.CveTitle(), + d.CveDetail.Jvn.Link(), + distroLinks(d, r.Family)[0].url, + packsVer, + ) scols = []string{ d.CveDetail.CveID, fmt.Sprintf("%-4.1f (%s)", @@ -126,8 +117,15 @@ func ToPlainTextSummary(r models.ScanResult) string { ), summary, } + case 0 < d.CveDetail.CvssScore("en"): - summary := d.CveDetail.Nvd.CveSummary() + summary := fmt.Sprintf("%s\n%s/%s\n%s\n%s", + d.CveDetail.Nvd.CveSummary(), + cveDetailsBaseURL, + d.CveDetail.CveID, + distroLinks(d, r.Family)[0].url, + packsVer, + ) scols = []string{ d.CveDetail.CveID, fmt.Sprintf("%-4.1f (%s)", @@ -137,10 +135,12 @@ func ToPlainTextSummary(r models.ScanResult) string { summary, } default: + summary := fmt.Sprintf("%s\n%s", + distroLinks(d, r.Family)[0].url, packsVer) scols = []string{ d.CveDetail.CveID, "?", - d.CveDetail.Nvd.CveSummary(), + summary, } } @@ -149,12 +149,55 @@ func ToPlainTextSummary(r models.ScanResult) string { cols[i] = scols[i] } stable.AddRow(cols...) + stable.AddRow("") + } + return fmt.Sprintf("%s\n%s\n", header, stable) +} + +func toFullPlainText(r models.ScanResult) string { + serverInfo := r.ServerInfo() + + var buf bytes.Buffer + for i := 0; i < len(serverInfo); i++ { + buf.WriteString("=") + } + header := fmt.Sprintf("%s\n%s\n%s\t%s\n", + r.ServerInfo(), + buf.String(), + r.CveSummary(), + r.Packages.ToUpdatablePacksSummary(), + ) + + if len(r.KnownCves) == 0 && len(r.UnknownCves) == 0 { + return fmt.Sprintf(` +%s +No CVE-IDs are found in updatable packages. +%s +`, header, r.Packages.ToUpdatablePacksSummary()) + } + + scoredReport, unscoredReport := []string{}, []string{} + scoredReport, unscoredReport = toPlainTextDetails(r, r.Family) + + unscored := "" + if !config.Conf.IgnoreUnscoredCves { + unscored = strings.Join(unscoredReport, "\n\n") } - return fmt.Sprintf("%s", stable) + + scored := strings.Join(scoredReport, "\n\n") + detail := fmt.Sprintf(` +%s + +%s +`, + scored, + unscored, + ) + return fmt.Sprintf("%s\n%s\n", header, detail) } -func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) { - for _, cve := range data.KnownCves { +func toPlainTextDetails(r models.ScanResult, osFamily string) (scoredReport, unscoredReport []string) { + for _, cve := range r.KnownCves { switch config.Conf.Lang { case "en": if 0 < cve.CveDetail.Nvd.CvssScore() { @@ -177,7 +220,7 @@ func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, } } } - for _, cve := range data.UnknownCves { + for _, cve := range r.UnknownCves { unscoredReport = append( unscoredReport, toPlainTextUnknownCve(cve, osFamily)) } @@ -187,7 +230,7 @@ func toPlainTextDetails(data models.ScanResult, osFamily string) (scoredReport, func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { cveID := cveInfo.CveDetail.CveID dtable := uitable.New() - dtable.MaxColWidth = 100 + dtable.MaxColWidth = maxColWidth dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") @@ -201,6 +244,8 @@ func toPlainTextUnknownCve(cveInfo models.CveInfo, osFamily string) string { for _, link := range dlinks { dtable.AddRow(link.title, link.url) } + dtable = addPackageInfos(dtable, cveInfo.Packages) + dtable = addCpeNames(dtable, cveInfo.CpeNames) return fmt.Sprintf("%s", dtable) } @@ -211,7 +256,7 @@ func toPlainTextDetailsLangJa(cveInfo models.CveInfo, osFamily string) string { jvn := cveDetail.Jvn dtable := uitable.New() - dtable.MaxColWidth = 100 + dtable.MaxColWidth = maxColWidth dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") @@ -253,7 +298,7 @@ func toPlainTextDetailsLangEn(d models.CveInfo, osFamily string) string { nvd := cveDetail.Nvd dtable := uitable.New() - dtable.MaxColWidth = 100 + dtable.MaxColWidth = maxColWidth dtable.Wrap = true dtable.AddRow(cveID) dtable.AddRow("-------------") @@ -357,13 +402,12 @@ func distroLinks(cveInfo models.CveInfo, osFamily string) []distroLink { } } -//TODO // addPackageInfos add package information related the CVE to table func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable.Table { for i, p := range packs { var title string if i == 0 { - title = "Package/CPE" + title = "Package" } ver := fmt.Sprintf( "%s -> %s", p.ToStringCurrentVersion(), p.ToStringNewVersion()) @@ -372,9 +416,9 @@ func addPackageInfos(table *uitable.Table, packs []models.PackageInfo) *uitable. return table } -func addCpeNames(table *uitable.Table, names []models.CpeName) *uitable.Table { - for _, p := range names { - table.AddRow("CPE", fmt.Sprintf("%s", p.Name)) +func addCpeNames(table *uitable.Table, names []string) *uitable.Table { + for _, n := range names { + table.AddRow("CPE", fmt.Sprintf("%s", n)) } return table } diff --git a/report/writer.go b/report/writer.go index 13fd22f2f3..9f7e1dce13 100644 --- a/report/writer.go +++ b/report/writer.go @@ -17,7 +17,12 @@ along with this program. If not, see . package report -import "github.com/future-architect/vuls/models" +import ( + "bytes" + "compress/gzip" + + "github.com/future-architect/vuls/models" +) const ( nvdBaseURL = "https://web.nvd.nist.gov/view/vuln/detail" @@ -33,9 +38,27 @@ const ( debianTrackerBaseURL = "https://security-tracker.debian.org/tracker" freeBSDVuXMLBaseURL = "https://vuxml.freebsd.org/freebsd/%s.html" + + vulsOpenTag = "" + vulsCloseTag = "" ) // ResultWriter Interface type ResultWriter interface { - Write([]models.ScanResult) error + Write(...models.ScanResult) error +} + +func gz(data []byte) ([]byte, error) { + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write(data); err != nil { + return nil, err + } + if err := gz.Flush(); err != nil { + return nil, err + } + if err := gz.Close(); err != nil { + return nil, err + } + return b.Bytes(), nil } diff --git a/report/xml.go b/report/xml.go deleted file mode 100644 index 5699e1714a..0000000000 --- a/report/xml.go +++ /dev/null @@ -1,54 +0,0 @@ -package report - -import ( - "bytes" - "encoding/xml" - "fmt" - "io/ioutil" - "path/filepath" - "time" - - "github.com/future-architect/vuls/models" -) - -const ( - vulsOpenTag = "" - vulsCloseTag = "" -) - -// XMLWriter writes results to file. -type XMLWriter struct { - ScannedAt time.Time -} - -func (w XMLWriter) Write(scanResults []models.ScanResult) (err error) { - var path string - if path, err = ensureResultDir(w.ScannedAt); err != nil { - return fmt.Errorf("Failed to make direcotory/symlink : %s", err) - } - - for _, scanResult := range scanResults { - scanResult.ScannedAt = w.ScannedAt - } - - var xmlBytes []byte - for _, r := range scanResults { - xmlPath := "" - if len(r.Container.ContainerID) == 0 { - xmlPath = filepath.Join(path, fmt.Sprintf("%s.xml", r.ServerName)) - } else { - xmlPath = filepath.Join(path, - fmt.Sprintf("%s_%s.xml", r.ServerName, r.Container.Name)) - } - - if xmlBytes, err = xml.Marshal(r); err != nil { - return fmt.Errorf("Failed to Marshal to XML: %s", err) - } - - allBytes := bytes.Join([][]byte{[]byte(xml.Header + vulsOpenTag), xmlBytes, []byte(vulsCloseTag)}, []byte{}) - if err := ioutil.WriteFile(xmlPath, allBytes, 0600); err != nil { - return fmt.Errorf("Failed to write XML. path: %s, err: %s", xmlPath, err) - } - } - return nil -} diff --git a/scan/base.go b/scan/base.go index 04485ea44e..70c0570983 100644 --- a/scan/base.go +++ b/scan/base.go @@ -26,7 +26,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" ) @@ -224,62 +223,16 @@ func (l base) isAwsInstanceID(str string) bool { } func (l *base) convertToModel() (models.ScanResult, error) { - var scoredCves, unscoredCves, ignoredCves models.CveInfos - for _, p := range l.UnsecurePackages { - sort.Sort(models.PackageInfosByName(p.Packs)) - - // ignoreCves - found := false - for _, icve := range l.getServerInfo().IgnoreCves { - if icve == p.CveDetail.CveID { - ignoredCves = append(ignoredCves, models.CveInfo{ - CveDetail: p.CveDetail, - Packages: p.Packs, - DistroAdvisories: p.DistroAdvisories, - }) - found = true - break - } - } - if found { - continue - } - - // unscoredCves - if p.CveDetail.CvssScore(config.Conf.Lang) <= 0 { - unscoredCves = append(unscoredCves, models.CveInfo{ - CveDetail: p.CveDetail, - Packages: p.Packs, - DistroAdvisories: p.DistroAdvisories, - }) - continue - } - - cpenames := []models.CpeName{} - for _, cpename := range p.CpeNames { - cpenames = append(cpenames, - models.CpeName{Name: cpename}) - } - - // scoredCves - cve := models.CveInfo{ - CveDetail: p.CveDetail, - Packages: p.Packs, - DistroAdvisories: p.DistroAdvisories, - CpeNames: cpenames, - } - scoredCves = append(scoredCves, cve) + for _, p := range l.VulnInfos { + sort.Sort(models.PackageInfosByName(p.Packages)) } + sort.Sort(l.VulnInfos) container := models.Container{ ContainerID: l.ServerInfo.Container.ContainerID, Name: l.ServerInfo.Container.Name, } - sort.Sort(scoredCves) - sort.Sort(unscoredCves) - sort.Sort(ignoredCves) - return models.ScanResult{ ServerName: l.ServerInfo.ServerName, ScannedAt: time.Now(), @@ -287,52 +240,12 @@ func (l *base) convertToModel() (models.ScanResult, error) { Release: l.Distro.Release, Container: container, Platform: l.Platform, - KnownCves: scoredCves, - UnknownCves: unscoredCves, - IgnoredCves: ignoredCves, + ScannedCves: l.VulnInfos, + Packages: l.Packages, Optional: l.ServerInfo.Optional, }, nil } -// scanVulnByCpeName search vulnerabilities that specified in config file. -func (l *base) scanVulnByCpeName() error { - unsecurePacks := CvePacksList{} - - serverInfo := l.getServerInfo() - cpeNames := serverInfo.CpeNames - - // remove duplicate - set := map[string]CvePacksInfo{} - - for _, name := range cpeNames { - details, err := cveapi.CveClient.FetchCveDetailsByCpeName(name) - if err != nil { - return err - } - for _, detail := range details { - if val, ok := set[detail.CveID]; ok { - names := val.CpeNames - names = append(names, name) - val.CpeNames = names - set[detail.CveID] = val - } else { - set[detail.CveID] = CvePacksInfo{ - CveID: detail.CveID, - CveDetail: detail, - CpeNames: []string{name}, - } - } - } - } - - for key := range set { - unsecurePacks = append(unsecurePacks, set[key]) - } - unsecurePacks = append(unsecurePacks, l.UnsecurePackages...) - l.setUnsecurePackages(unsecurePacks) - return nil -} - func (l *base) setErrs(errs []error) { l.errs = errs } diff --git a/scan/debian.go b/scan/debian.go index 32b7b5ad85..22f4248d1a 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -26,7 +26,6 @@ import ( "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ) @@ -179,12 +178,12 @@ func (o *debian) scanPackages() error { } o.setPackages(packs) - var unsecurePacks []CvePacksInfo + var unsecurePacks []models.VulnInfo if unsecurePacks, err = o.scanUnsecurePackages(packs); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } - o.setUnsecurePackages(unsecurePacks) + o.setVulnInfos(unsecurePacks) return nil } @@ -242,37 +241,41 @@ func (o *debian) checkRequiredPackagesInstalled() error { return nil } -func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInfo, error) { +func (o *debian) scanUnsecurePackages(installed []models.PackageInfo) ([]models.VulnInfo, error) { o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") if r := o.ssh(cmd, sudo); !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } - upgradablePackNames, err := o.GetUpgradablePackNames() + // Convert the name of upgradable packages to PackageInfo struct + upgradableNames, err := o.GetUpgradablePackNames() if err != nil { - return []CvePacksInfo{}, err + return nil, err } - - // Convert package name to PackageInfo struct - var unsecurePacks []models.PackageInfo - for _, name := range upgradablePackNames { - for _, pack := range packs { + var upgradablePacks []models.PackageInfo + for _, name := range upgradableNames { + for _, pack := range installed { if pack.Name == name { - unsecurePacks = append(unsecurePacks, pack) + upgradablePacks = append(upgradablePacks, pack) break } } } - unsecurePacks, err = o.fillCandidateVersion(unsecurePacks) + + // Fill the candidate versions of upgradable packages + upgradablePacks, err = o.fillCandidateVersion(upgradablePacks) if err != nil { return nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } + o.Packages.MergeNewVersion(upgradablePacks) + + // Setup changelog cache current := cache.Meta{ Name: o.getServerInfo().GetServerName(), Distro: o.getServerInfo().Distro, - Packs: unsecurePacks, + Packs: upgradablePacks, } o.log.Debugf("Ensure changelog cache: %s", current.Name) if err := o.ensureChangelogCache(current); err != nil { @@ -280,12 +283,12 @@ func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInf } // Collect CVE information of upgradable packages - cvePacksInfos, err := o.scanPackageCveInfos(unsecurePacks) + vulnInfos, err := o.scanVulnInfos(upgradablePacks) if err != nil { return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err) } - return cvePacksInfos, nil + return vulnInfos, nil } func (o *debian) ensureChangelogCache(current cache.Meta) error { @@ -327,7 +330,7 @@ func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []m cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) r := o.ssh(cmd, sudo) if !r.isSuccess() { - return nil, fmt.Errorf("Failed to SSH: %s.", r) + return nil, fmt.Errorf("Failed to SSH: %s", r) } packChangelog := o.splitAptCachePolicy(r.Stdout) for k, v := range packChangelog { @@ -398,26 +401,26 @@ func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, er return } -func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePacksList CvePacksList, err error) { +func (o *debian) scanVulnInfos(upgradablePacks []models.PackageInfo) (models.VulnInfos, error) { meta := cache.Meta{ Name: o.getServerInfo().GetServerName(), Distro: o.getServerInfo().Distro, - Packs: unsecurePacks, + Packs: upgradablePacks, } type strarray []string resChan := make(chan struct { models.PackageInfo strarray - }, len(unsecurePacks)) - errChan := make(chan error, len(unsecurePacks)) - reqChan := make(chan models.PackageInfo, len(unsecurePacks)) + }, len(upgradablePacks)) + errChan := make(chan error, len(upgradablePacks)) + reqChan := make(chan models.PackageInfo, len(upgradablePacks)) defer close(resChan) defer close(errChan) defer close(reqChan) go func() { - for _, pack := range unsecurePacks { + for _, pack := range upgradablePacks { reqChan <- pack } }() @@ -425,7 +428,7 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac timeout := time.After(30 * 60 * time.Second) concurrency := 10 tasks := util.GenWorkers(concurrency) - for range unsecurePacks { + for range upgradablePacks { tasks <- func() { select { case pack := <-reqChan: @@ -458,7 +461,7 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac // { CVE ID: [packageInfo] } cvePackages := make(map[string][]models.PackageInfo) errs := []error{} - for i := 0; i < len(unsecurePacks); i++ { + for i := 0; i < len(upgradablePacks); i++ { select { case pair := <-resChan: pack := pair.PackageInfo @@ -467,12 +470,11 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack) } o.log.Infof("(%d/%d) Scanned %s-%s : %s", - i+1, len(unsecurePacks), pair.Name, pair.PackageInfo.Version, cveIDs) + i+1, len(upgradablePacks), pair.Name, pair.PackageInfo.Version, cveIDs) case err := <-errChan: errs = append(errs, err) case <-timeout: - //TODO append to errs - return nil, fmt.Errorf("Timeout scanPackageCveIDs") + errs = append(errs, fmt.Errorf("Timeout scanPackageCveIDs")) } } if 0 < len(errs) { @@ -484,23 +486,14 @@ func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePac cveIDs = append(cveIDs, k) } o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) - - o.log.Info("Fetching CVE details...") - cveDetails, err := cveapi.CveClient.FetchCveDetails(cveIDs) - if err != nil { - return nil, err - } - o.log.Info("Done") - - for _, detail := range cveDetails { - cvePacksList = append(cvePacksList, CvePacksInfo{ - CveID: detail.CveID, - CveDetail: detail, - Packs: cvePackages[detail.CveID], - // CvssScore: cinfo.CvssScore(conf.Lang), + var vinfos models.VulnInfos + for k, v := range cvePackages { + vinfos = append(vinfos, models.VulnInfo{ + CveID: k, + Packages: v, }) } - return + return vinfos, nil } func (o *debian) getChangelogCache(meta cache.Meta, pack models.PackageInfo) string { diff --git a/scan/freebsd.go b/scan/freebsd.go index 2d904f17ed..0abee79e6c 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ) @@ -87,12 +86,12 @@ func (o *bsd) scanPackages() error { } o.setPackages(packs) - var unsecurePacks []CvePacksInfo - if unsecurePacks, err = o.scanUnsecurePackages(); err != nil { + var vinfos []models.VulnInfo + if vinfos, err = o.scanUnsecurePackages(); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } - o.setUnsecurePackages(unsecurePacks) + o.setVulnInfos(vinfos) return nil } @@ -105,7 +104,7 @@ func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { return o.parsePkgVersion(r.Stdout), nil } -func (o *bsd) scanUnsecurePackages() (cvePacksList []CvePacksInfo, err error) { +func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { const vulndbPath = "/tmp/vuln.db" cmd := "rm -f " + vulndbPath r := o.ssh(cmd, noSudo) @@ -120,7 +119,7 @@ func (o *bsd) scanUnsecurePackages() (cvePacksList []CvePacksInfo, err error) { } if r.ExitStatus == 0 { // no vulnerabilities - return []CvePacksInfo{}, nil + return []models.VulnInfo{}, nil } var packAdtRslt []pkgAuditResult @@ -151,34 +150,22 @@ func (o *bsd) scanUnsecurePackages() (cvePacksList []CvePacksInfo, err error) { } } - cveIDs := []string{} for k := range cveIDAdtMap { - cveIDs = append(cveIDs, k) - } - - cveDetails, err := cveapi.CveClient.FetchCveDetails(cveIDs) - if err != nil { - return nil, err - } - o.log.Info("Done") - - for _, d := range cveDetails { packs := []models.PackageInfo{} - for _, r := range cveIDAdtMap[d.CveID] { + for _, r := range cveIDAdtMap[k] { packs = append(packs, r.pack) } disAdvs := []models.DistroAdvisory{} - for _, r := range cveIDAdtMap[d.CveID] { + for _, r := range cveIDAdtMap[k] { disAdvs = append(disAdvs, models.DistroAdvisory{ AdvisoryID: r.vulnIDCveIDs.vulnID, }) } - cvePacksList = append(cvePacksList, CvePacksInfo{ - CveID: d.CveID, - CveDetail: d, - Packs: packs, + vulnInfos = append(vulnInfos, models.VulnInfo{ + CveID: k, + Packages: packs, DistroAdvisories: disAdvs, }) } diff --git a/scan/redhat.go b/scan/redhat.go index 3dbfc585a3..7091b24d26 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -26,7 +26,6 @@ import ( "time" "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" @@ -189,12 +188,12 @@ func (o *redhat) scanPackages() error { } o.setPackages(packs) - var unsecurePacks []CvePacksInfo - if unsecurePacks, err = o.scanUnsecurePackages(); err != nil { + var vinfos []models.VulnInfo + if vinfos, err = o.scanVulnInfos(); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } - o.setUnsecurePackages(unsecurePacks) + o.setVulnInfos(vinfos) return nil } @@ -235,7 +234,7 @@ func (o *redhat) parseScannedPackagesLine(line string) (models.PackageInfo, erro }, nil } -func (o *redhat) scanUnsecurePackages() ([]CvePacksInfo, error) { +func (o *redhat) scanVulnInfos() ([]models.VulnInfo, error) { if o.Distro.Family != "centos" { // Amazon, RHEL has yum updateinfo as default // yum updateinfo can collenct vendor advisory information. @@ -247,7 +246,7 @@ func (o *redhat) scanUnsecurePackages() ([]CvePacksInfo, error) { } // For CentOS -func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (CvePacksList, error) { +func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, error) { cmd := "LANGUAGE=en_US.UTF-8 yum --color=never %s check-update" if o.getServerInfo().Enablerepo != "" { cmd = fmt.Sprintf(cmd, "--enablerepo="+o.getServerInfo().Enablerepo) @@ -268,19 +267,23 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (CvePacksList, error) } o.log.Debugf("%s", pp.Sprintf("%v", packInfoList)) + // set candidate version info + o.Packages.MergeNewVersion(packInfoList) + // Collect CVE-IDs in changelog type PackInfoCveIDs struct { PackInfo models.PackageInfo CveIDs []string } - // { packageName: changelog-lines } - var rpm2changelog map[string]*string allChangelog, err := o.getAllChangelog(packInfoList) if err != nil { o.log.Errorf("Failed to getAllchangelog. err: %s", err) return nil, err } + + // { packageName: changelog-lines } + var rpm2changelog map[string]*string rpm2changelog, err = o.parseAllChangelog(allChangelog) if err != nil { return nil, fmt.Errorf("Failed to parseAllChangelog. err: %s", err) @@ -337,39 +340,20 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (CvePacksList, error) cveIDPackInfoMap := make(map[string][]models.PackageInfo) for _, res := range results { for _, cveID := range res.CveIDs { - // packInfo, found := o.Packages.FindByName(res.Packname) - // if !found { - // return CvePacksList{}, fmt.Errorf( - // "Faild to transform data structure: %v", res.Packname) - // } - cveIDPackInfoMap[cveID] = append(cveIDPackInfoMap[cveID], res.PackInfo) + cveIDPackInfoMap[cveID] = append( + cveIDPackInfoMap[cveID], res.PackInfo) } } - var uniqueCveIDs []string - for cveID := range cveIDPackInfoMap { - uniqueCveIDs = append(uniqueCveIDs, cveID) - } - - // cveIDs => []cve.CveInfo - o.log.Info("Fetching CVE details...") - cveDetails, err := cveapi.CveClient.FetchCveDetails(uniqueCveIDs) - if err != nil { - return nil, err - } - o.log.Info("Done") - - cvePacksList := []CvePacksInfo{} - for _, detail := range cveDetails { + vinfos := []models.VulnInfo{} + for k, v := range cveIDPackInfoMap { // Amazon, RHEL do not use this method, so VendorAdvisory do not set. - cvePacksList = append(cvePacksList, CvePacksInfo{ - CveID: detail.CveID, - CveDetail: detail, - Packs: cveIDPackInfoMap[detail.CveID], - // CvssScore: cinfo.CvssScore(conf.Lang), + vinfos = append(vinfos, models.VulnInfo{ + CveID: k, + Packages: v, }) } - return cvePacksList, nil + return vinfos, nil } // parseYumCheckUpdateLines parse yum check-update to get package name, candidate version @@ -579,11 +563,11 @@ type distroAdvisoryCveIDs struct { // Scaning unsecure packages using yum-plugin-security. // Amazon, RHEL -func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, error) { +func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, error) { if o.Distro.Family == "centos" { // CentOS has no security channel. // So use yum check-update && parse changelog - return CvePacksList{}, fmt.Errorf( + return nil, fmt.Errorf( "yum updateinfo is not suppported on CentOS") } @@ -615,6 +599,9 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, err } o.log.Debugf("%s", pp.Sprintf("%v", updatable)) + // set candidate version info + o.Packages.MergeNewVersion(updatable) + dict := map[string][]models.PackageInfo{} for _, advIDPackNames := range advIDPackNamesList { packInfoList := models.PackageInfoList{} @@ -638,48 +625,41 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (CvePacksList, err } advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout) if err != nil { - return CvePacksList{}, err + return nil, err } // pp.Println(advisoryCveIDsList) // All information collected. - // Convert to CvePacksList. + // Convert to VulnInfos. o.log.Info("Fetching CVE details...") - result := CvePacksList{} + vinfos := models.VulnInfos{} for _, advIDCveIDs := range advisoryCveIDsList { - cveDetails, err := - cveapi.CveClient.FetchCveDetails(advIDCveIDs.CveIDs) - if err != nil { - return nil, err - } - - for _, cveDetail := range cveDetails { + for _, cveID := range advIDCveIDs.CveIDs { found := false - for i, p := range result { - if cveDetail.CveID == p.CveID { + for i, p := range vinfos { + if cveID == p.CveID { advAppended := append(p.DistroAdvisories, advIDCveIDs.DistroAdvisory) - result[i].DistroAdvisories = advAppended + vinfos[i].DistroAdvisories = advAppended packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID] - result[i].Packs = append(result[i].Packs, packs...) + vinfos[i].Packages = append(vinfos[i].Packages, packs...) found = true break } } if !found { - cpinfo := CvePacksInfo{ - CveID: cveDetail.CveID, - CveDetail: cveDetail, + cpinfo := models.VulnInfo{ + CveID: cveID, DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory}, - Packs: dict[advIDCveIDs.DistroAdvisory.AdvisoryID], + Packages: dict[advIDCveIDs.DistroAdvisory.AdvisoryID], } - result = append(result, cpinfo) + vinfos = append(vinfos, cpinfo) } + } } - o.log.Info("Done") - return result, nil + return vinfos, nil } var horizontalRulePattern = regexp.MustCompile(`^=+$`) @@ -929,7 +909,6 @@ func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string // parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS), packages func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) { - result := []advisoryIDPacks{} lines := strings.Split(stdout, "\n") for _, line := range lines { diff --git a/scan/serverapi.go b/scan/serverapi.go index eb8dbf3259..d011169275 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -21,6 +21,7 @@ import ( "bufio" "fmt" "os" + "path/filepath" "strings" "time" @@ -28,7 +29,7 @@ import ( "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" - cve "github.com/kotakanbe/go-cve-dictionary/models" + "github.com/future-architect/vuls/report" ) // Log for localhsot @@ -54,7 +55,6 @@ type osTypeInterface interface { checkRequiredPackagesInstalled() error scanPackages() error - scanVulnByCpeName() error install() error convertToModel() (models.ScanResult, error) @@ -72,64 +72,15 @@ type osPackages struct { Packages models.PackageInfoList // unsecure packages - UnsecurePackages CvePacksList + VulnInfos models.VulnInfos } func (p *osPackages) setPackages(pi models.PackageInfoList) { p.Packages = pi } -func (p *osPackages) setUnsecurePackages(pi []CvePacksInfo) { - p.UnsecurePackages = pi -} - -// CvePacksList have CvePacksInfo list, getter/setter, sortable methods. -type CvePacksList []CvePacksInfo - -// CvePacksInfo hold the CVE information. -type CvePacksInfo struct { - CveID string - CveDetail cve.CveDetail - Packs models.PackageInfoList - DistroAdvisories []models.DistroAdvisory // for Aamazon, RHEL, FreeBSD - CpeNames []string -} - -// FindByCveID find by CVEID -func (s CvePacksList) FindByCveID(cveID string) (pi CvePacksInfo, found bool) { - for _, p := range s { - if cveID == p.CveID { - return p, true - } - } - return CvePacksInfo{CveID: cveID}, false -} - -// immutable -func (s CvePacksList) set(cveID string, cvePacksInfo CvePacksInfo) CvePacksList { - for i, p := range s { - if cveID == p.CveID { - s[i] = cvePacksInfo - return s - } - } - return append(s, cvePacksInfo) -} - -// Len implement Sort Interface -func (s CvePacksList) Len() int { - return len(s) -} - -// Swap implement Sort Interface -func (s CvePacksList) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less implement Sort Interface -func (s CvePacksList) Less(i, j int) bool { - return s[i].CveDetail.CvssScore(config.Conf.Lang) > - s[j].CveDetail.CvssScore(config.Conf.Lang) +func (p *osPackages) setVulnInfos(vi []models.VulnInfo) { + p.VulnInfos = vi } func detectOS(c config.ServerInfo) (osType osTypeInterface) { @@ -518,14 +469,15 @@ func Scan() []error { }() Log.Info("Scanning vulnerable OS packages...") - if errs := scanPackages(); errs != nil { - return errs + scannedAt := time.Now() + dir, err := ensureResultDir(scannedAt) + if err != nil { + return []error{err} } - - Log.Info("Scanning vulnerable software specified in the CPE...") - if errs := scanVulnByCpeName(); errs != nil { + if errs := scanVulns(dir, scannedAt); errs != nil { return errs } + return nil } @@ -553,31 +505,67 @@ func checkRequiredPackagesInstalled() []error { }, timeoutSec) } -func scanPackages() []error { +func scanVulns(jsonDir string, scannedAt time.Time) []error { + var results models.ScanResults timeoutSec := 120 * 60 - return parallelSSHExec(func(o osTypeInterface) error { - return o.scanPackages() - }, timeoutSec) + errs := parallelSSHExec(func(o osTypeInterface) error { + if err := o.scanPackages(); err != nil { + return err + } -} + r, err := o.convertToModel() + if err != nil { + return err + } + r.ScannedAt = scannedAt + results = append(results, r) -// scanVulnByCpeName search vulnerabilities that specified in config file. -func scanVulnByCpeName() []error { - timeoutSec := 30 * 60 - return parallelSSHExec(func(o osTypeInterface) error { - return o.scanVulnByCpeName() + return nil }, timeoutSec) + config.Conf.FormatJSON = true + ws := []report.ResultWriter{ + report.LocalFileWriter{CurrentDir: jsonDir}, + } + for _, w := range ws { + if err := w.Write(results...); err != nil { + return []error{ + fmt.Errorf("Failed to write summary report: %s", err), + } + } + } + if errs != nil { + return errs + } + + report.StdoutWriter{}.WriteScanSummary(results...) + return nil } -// GetScanResults returns Scan Resutls -func GetScanResults() (results models.ScanResults, err error) { - for _, s := range servers { - r, err := s.convertToModel() - if err != nil { - return results, fmt.Errorf("Failed converting to model: %s", err) +func ensureResultDir(scannedAt time.Time) (currentDir string, err error) { + jsonDirName := scannedAt.Format(time.RFC3339) + + resultsDir := config.Conf.ResultsDir + if len(resultsDir) == 0 { + wd, _ := os.Getwd() + resultsDir = filepath.Join(wd, "results") + } + jsonDir := filepath.Join(resultsDir, jsonDirName) + if err := os.MkdirAll(jsonDir, 0700); err != nil { + return "", fmt.Errorf("Failed to create dir: %s", err) + } + + symlinkPath := filepath.Join(resultsDir, "current") + if _, err := os.Lstat(symlinkPath); err == nil { + if err := os.Remove(symlinkPath); err != nil { + return "", fmt.Errorf( + "Failed to remove symlink. path: %s, err: %s", symlinkPath, err) } - results = append(results, r) } - return + + if err := os.Symlink(jsonDir, symlinkPath); err != nil { + return "", fmt.Errorf( + "Failed to create symlink: path: %s, err: %s", symlinkPath, err) + } + return jsonDir, nil } diff --git a/scan/serverapi_test.go b/scan/serverapi_test.go index a9b5b64e41..040485ba11 100644 --- a/scan/serverapi_test.go +++ b/scan/serverapi_test.go @@ -1,158 +1 @@ package scan - -import ( - "github.com/future-architect/vuls/config" - "github.com/future-architect/vuls/models" - cve "github.com/kotakanbe/go-cve-dictionary/models" - "reflect" - "testing" -) - -func TestPackageCveInfosSetGet(t *testing.T) { - var test = struct { - in []string - out []string - }{ - []string{ - "CVE1", - "CVE2", - "CVE3", - "CVE1", - "CVE1", - "CVE2", - "CVE3", - }, - []string{ - "CVE1", - "CVE2", - "CVE3", - }, - } - - // var ps packageCveInfos - var ps CvePacksList - for _, cid := range test.in { - ps = ps.set(cid, CvePacksInfo{CveID: cid}) - } - - if len(test.out) != len(ps) { - t.Errorf("length: expected %d, actual %d", len(test.out), len(ps)) - } - - for i, expectedCid := range test.out { - if expectedCid != ps[i].CveID { - t.Errorf("expected %s, actual %s", expectedCid, ps[i].CveID) - } - } - for _, cid := range test.in { - p, _ := ps.FindByCveID(cid) - if p.CveID != cid { - t.Errorf("expected %s, actual %s", cid, p.CveID) - } - } -} - -func TestGetScanResults(t *testing.T) { - // setup servers - c := config.ServerInfo{ - ServerName: "ubuntu", - } - deb1 := newDebian(c) - deb2 := newDebian(c) - - cpis1 := []CvePacksInfo{ - { - CveID: "CVE1", - CveDetail: cve.CveDetail{CveID: "CVE1"}, - Packs: []models.PackageInfo{ - {Name: "mysql-client-5.5"}, - {Name: "mysql-server-5.5"}, - {Name: "mysql-common-5.5"}, - }, - }, - { - CveID: "CVE2", - CveDetail: cve.CveDetail{CveID: "CVE2"}, - Packs: []models.PackageInfo{ - {Name: "mysql-common-5.5"}, - {Name: "mysql-server-5.5"}, - {Name: "mysql-client-5.5"}, - }, - }, - } - cpis2 := []CvePacksInfo{ - { - CveID: "CVE3", - CveDetail: cve.CveDetail{CveID: "CVE3"}, - Packs: []models.PackageInfo{ - {Name: "libcurl3"}, - {Name: "curl"}, - }, - }, - { - CveID: "CVE4", - CveDetail: cve.CveDetail{CveID: "CVE4"}, - Packs: []models.PackageInfo{ - {Name: "bind9"}, - {Name: "libdns100"}, - }, - }, - } - deb1.setUnsecurePackages(cpis1) - servers = append(servers, deb1) - - deb2.setUnsecurePackages(cpis2) - servers = append(servers, deb2) - - // prepare expected data - expectedUnKnownPackages := []map[string][]models.PackageInfo{ - { - "CVE1": { - {Name: "mysql-client-5.5"}, - {Name: "mysql-common-5.5"}, - {Name: "mysql-server-5.5"}, - }, - }, - { - "CVE2": { - {Name: "mysql-client-5.5"}, - {Name: "mysql-common-5.5"}, - {Name: "mysql-server-5.5"}, - }, - }, - { - "CVE3": { - {Name: "curl"}, - {Name: "libcurl3"}, - }, - }, - { - "CVE4": { - {Name: "bind9"}, - {Name: "libdns100"}, - }, - }, - } - - // check scanResults - scanResults, _ := GetScanResults() - if len(scanResults) != 2 { - t.Errorf("length of scanResults should be 2") - } - for i, result := range scanResults { - if result.ServerName != "ubuntu" { - t.Errorf("expected ubuntu, actual %s", result.ServerName) - } - - unKnownCves := result.UnknownCves - if len(unKnownCves) != 2 { - t.Errorf("length of unKnownCves should be 2") - } - for j, unKnownCve := range unKnownCves { - expected := expectedUnKnownPackages[i*2+j][unKnownCve.CveDetail.CveID] - if !reflect.DeepEqual(expected, unKnownCve.Packages) { - t.Errorf("expected %v, actual %v", expected, unKnownCve.Packages) - } - } - } -} diff --git a/setup/docker/README.md b/setup/docker/README.md index d72aae70c2..1914f188ea 100644 --- a/setup/docker/README.md +++ b/setup/docker/README.md @@ -149,12 +149,24 @@ $ docker run --rm -it \ -v /etc/localtime:/etc/localtime:ro \ -e "TZ=Asia/Tokyo" \ vuls/vuls scan \ - -cve-dictionary-dbpath=/vuls/cve.sqlite3 \ - -report-json \ -config=./config.toml # path to config.toml in docker ``` -## Step5. vulsrepo +## Step5. Report + +```console +$ docker run --rm -it \ + -v ~/.ssh:/root/.ssh:ro \ + -v $PWD:/vuls \ + -v $PWD/vuls-log:/var/log/vuls \ + -v /etc/localtime:/etc/localtime:ro \ + vuls/vuls report \ + -cvedb-path=/vuls/cve.sqlite3 \ + -config=./config.toml \ # path to config.toml in docker + -format-short-text +``` + +## Step6. vulsrepo ```console $docker run -dt \ diff --git a/setup/docker/vuls/latest/README.md b/setup/docker/vuls/latest/README.md index 0fe862850e..897f8d3941 100644 --- a/setup/docker/vuls/latest/README.md +++ b/setup/docker/vuls/latest/README.md @@ -83,9 +83,21 @@ $ docker run --rm -it \ -v $PWD/vuls-log:/var/log/vuls \ -v /etc/localtime:/etc/localtime:ro \ vuls/vuls scan \ - -cve-dictionary-dbpath=/vuls/cve.sqlite3 \ + -config=./config.toml # path to config.toml in docker +``` + +## Report + +```console +$ docker run --rm -it \ + -v ~/.ssh:/root/.ssh:ro \ + -v $PWD:/vuls \ + -v $PWD/vuls-log:/var/log/vuls \ + -v /etc/localtime:/etc/localtime:ro \ + vuls/vuls report \ + -cvedb-path=/vuls/cve.sqlite3 \ -config=./config.toml \ # path to config.toml in docker - -report-json + -format-short-text ``` ## tui @@ -94,7 +106,8 @@ $ docker run --rm -it \ $ docker run --rm -it \ -v $PWD:/vuls \ -v $PWD/vuls-log:/var/log/vuls \ - vuls/vuls tui + vuls/vuls tui \ + -cvedb-path=/vuls/cve.sqlite3 ``` ## vulsrepo