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 eeeb7d2e01..08a81457c7 100644 Binary files a/img/vuls-architecture.png and b/img/vuls-architecture.png differ diff --git a/img/vuls-scan-flow.png b/img/vuls-scan-flow.png index a82b9403b8..f4959b35a3 100644 Binary files a/img/vuls-scan-flow.png and b/img/vuls-scan-flow.png differ 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