diff --git a/README.ja.md b/README.ja.md index 85190fd2b3..599b25ac2e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -169,6 +169,7 @@ NVDから脆弱性データベースを取得する。 環境によって異なるが、AWS上では10分程度かかる。 ```bash +$ cd $HOME $ for i in {2002..2016}; do go-cve-dictionary fetchnvd -years $i; done ... snip ... $ ls -alh cve.sqlite3 @@ -320,8 +321,17 @@ $ vuls tui # Architecture +## A. Scan via SSH Mode + ![Vuls-Architecture](img/vuls-architecture.png) +## B. Scan without SSH (Local Scan Mode) + +Vulsをスキャン対象サーバにデプロイする。Vulsはローカルホストにコマンドを発行する(SSH経由ではない)。スキャン結果のJSONを別サーバに集約する。スキャン結果の詳細化のためにはCVEデータベースへのアクセスが必要なので、事前にgo-cve-dictionaryをserver modeで起動しておく。 +その集約サーバ上で、あなたはWebUIやTUIを用いて各スキャン対象サーバのスキャン結果を参照することが可能。 + +![Vuls-Architecture Local Scan Mode](img/vuls-architecture-localscan.png) + ## [go-cve-dictionary](https://github.com/kotakanbe/go-cve-dictionary) - NVDとJVN(日本語)から脆弱性データベースを取得し、SQLite3に格納する。 @@ -714,6 +724,21 @@ $ vuls scan server1 server2 - ノーパスワードでsudoが実行可能 - configで定義されているサーバの中の、server1, server2のみスキャン +## Example: Scan via shell instead of SSH. + +ローカルホストのスキャンする場合、SSHではなく直接コマンドの発行が可能。 +config.tomlのhostに`localhost または 127.0.0.1`かつ、portに`local`を設定する必要がある。 +For more details, see [Architecture section](https://github.com/future-architect/vuls#architecture) + +- config.toml + ``` + [servers] + + [servers.localhost] + host = "localhost" # or "127.0.0.1" + port = "local" + ``` + ## Example: Scan Docker containers DockerコンテナはSSHデーモンを起動しないで運用するケースが一般的。 @@ -747,9 +772,22 @@ Vulsは、DockerホストにSSHで接続し、`docker exec`でDockerコンテナ keyPath = "/home/username/.ssh/id_rsa" containers = ["container_name_a", "4aa37a8b63b9"] ``` + - コンテナのみをスキャンする場合(ホストはスキャンしない) --containers-onlyオプションを指定する +- LXDコンテナをスキャンする場合 + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["${running}"] + [servers.172-31-4-82.container] + type = "lxd" + ``` # Usage: Report diff --git a/README.md b/README.md index e57689d438..a391fa7fc4 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ Fetch vulnerability data from NVD. It takes about 10 minutes (on AWS). ```bash +$ cd $HOME $ for i in {2002..2016}; do go-cve-dictionary fetchnvd -years $i; done ... snip ... $ ls -alh cve.sqlite3 @@ -319,8 +320,17 @@ see https://github.com/future-architect/vuls/tree/master/setup/docker # Architecture +## A. Scan via SSH Mode + ![Vuls-Architecture](img/vuls-architecture.png) +## B. Scan without SSH (Local Scan Mode) + +Deploy Vuls to the scan target server. Vuls issues a command to the local host (not via SSH). Aggregate the JSON of the scan result into another server. Since it is necessary to access the CVE database in order to refine the scan result, start go-cve-dictionary in server mode beforehand. +On the aggregation server, you can refer to the scanning result of each scan target server using WebUI or TUI. + +![Vuls-Architecture Local Scan Mode](img/vuls-architecture-localscan.png) + ## [go-cve-dictinary](https://github.com/kotakanbe/go-cve-dictionary) - Fetch vulnerability information from NVD and JVN(Japanese), then insert into SQLite3 or MySQL. @@ -721,9 +731,23 @@ 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) -## Example: Scan Docker containers +## Example: Scan via shell instead of SSH. + +Vuls scans localhost instead of SSH if the host address is `localhst or 127.0.0.1` and the port is `local` in config. +For more details, see [Architecture section](https://github.com/future-architect/vuls#architecture) + +- config.toml + ``` + [servers] + + [servers.localhost] + host = "localhost" # or "127.0.0.1" + port = "local" + ``` + +## Example: Scan containers (Docker/LXD) -It is common that keep Docker containers running without SSHd daemon. +It is common that keep 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. @@ -758,6 +782,19 @@ For more details, see [Architecture section](https://github.com/future-architect - To scan containers only - --containers-only option is available. +- To scan LXD Containers + ``` + [servers] + + [servers.172-31-4-82] + host = "172.31.4.82" + user = "ec2-user" + keyPath = "/home/username/.ssh/id_rsa" + containers = ["${running}"] + [servers.172-31-4-82.container] + type = "lxd" + ``` + ---- # Usage: Report diff --git a/commands/discover.go b/commands/discover.go index 7d674250ee..d9e0e903ab 100644 --- a/commands/discover.go +++ b/commands/discover.go @@ -116,11 +116,12 @@ subjectPrefix = "[vuls]" # "cpe:/a:rubyonrails:ruby_on_rails:4.2.1", #] #dependencyCheckXMLPath = "/tmp/dependency-check-report.xml" -#containers = ["${running}"] #ignoreCves = ["CVE-2014-6271"] #optional = [ # ["key", "value"], #] +#containers = ["${running}"] + [servers] {{- $names:= .Names}} @@ -134,11 +135,15 @@ host = "{{$ip}}" # "cpe:/a:rubyonrails:ruby_on_rails:4.2.1", #] #dependencyCheckXMLPath = "/tmp/dependency-check-report.xml" -#containers = ["${running}"] #ignoreCves = ["CVE-2014-0160"] #optional = [ # ["key", "value"], #] +#containers = ["${running}"] +#[servers.{{index $names $i}}.container] +#type = "docker" #or "lxd" defualt: docker + + {{end}} ` diff --git a/config/tomlloader.go b/config/tomlloader.go index 7cf07a167a..0c5c9082e9 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -62,15 +62,6 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { s := ServerInfo{ServerName: name} - switch { - case v.User != "": - s.User = v.User - case d.User != "": - s.User = d.User - default: - return fmt.Errorf("%s is invalid. User is empty", name) - } - s.Host = v.Host if len(s.Host) == 0 { return fmt.Errorf("%s is invalid. host is empty", name) @@ -85,6 +76,17 @@ func (c TOMLLoader) Load(pathToToml, keyPass string) error { s.Port = "22" } + switch { + case v.User != "": + s.User = v.User + case d.User != "": + s.User = d.User + default: + if s.Port != "local" { + return fmt.Errorf("%s is invalid. User is empty", name) + } + } + s.KeyPath = v.KeyPath if len(s.KeyPath) == 0 { s.KeyPath = d.KeyPath diff --git a/img/vuls-architecture-localscan.graphml b/img/vuls-architecture-localscan.graphml new file mode 100644 index 0000000000..ba18f6fb86 --- /dev/null +++ b/img/vuls-architecture-localscan.graphml @@ -0,0 +1,1600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vulnerbility Database + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + JVN +(Japanese) + + + + + + + + + + + + + + + + + NVD + + + + + + + + + + + + + + + + + + + + + + Distribution Support + + + + + + + + + + Folder 2 + + + + + + + + + + + + + + + + apptitude +changelog + + + + + + + + + + + + + + + + + yum +changelog + + + + + + + + + + + + + + + + + RHSA (RedHat) +ALAS (Amazon) + + + + + + + + + + + + + + + + + FreeBSD Support + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System Operator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + go-cve-dictionary + + + + + + + + + + Folder 4 + + + + + + + + + + + + + + + + + + + + + + + + SQLite3 + + + + + + + + + + + + + + + + HTTP server + + + + + + + + + + + + + + + + + Fetcher + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Azure +BLOB + + + + + + + + + + + + + + + + + .xml + + + + + + + + + + + + + + + + .txt + + + + + + + + + + + + + + + + .json + + + + + + + + + + + + + + + + .gz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vuls Reporting Server + + + + + + + + + + Folder 10 + + + + + + + + + + + + + + + + + + + + Vuls + + + + + + + + + + Folder 9 + + + + + + + + + + + + + + + + Report + + + + + + + + + + + + + + + + + VulsRepo +(WebUI) + + + + + + + + + + + + + + + + + TUI + + + + + + + + + + + + + + + + + + + + results dir + + + + + + + + + + Folder 7 + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + + + + + + + + + + + Scan Target Server + + + + + + + + + + Folder 10 + + + + + + + + + + + + + + + + + + + Docker/LXD + + + + + + + + + + Folder 5 + + + + + + + + + + + + + + + + Host + + + + + + + + + + + + + + + + + Container + + + + + + + + + + + + + + + + + + + Package Manager + + + + + + + + + + + + + + + + + + + + + Vuls + + + + + + + + + + Folder 10 + + + + + + + + + + + + + + + + Scan + + + + + + + + + + + + + + + + + + + + results dir + + + + + + + + + + Folder 7 + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + JSON + + + + + + + + + + + + + + + + + + + + + + + Fetch +Vulnerability data + + + + + + + + + + + + + + + + + + HTTP + + + + + + + + + + + + + + + + + + HTTP + + + + + + + + + + + + + + + + + + WebUI + + + + + + + + + + + + + + + + + + os.exec + + + + + + + + + + + + + + + + + + docker exec +lxc exec + + + + + + + + + + + + + + + + + + + + + + + + + + + + Insert + + + + + + + + + + + + + + + + + + Notify + + + + + + + + + + + + + + + + + + Select + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Put + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + View Results + on Terminal + + + + + + + + + + + + + + + + + + + os.exec + + + + + + + + + + + + + + + + + + + HTTP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + transfer + + + + + + + + + + + + + + + <?xml version="1.0" encoding="utf-8"?> +<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + width="57px" height="65px" viewBox="0 0 57 65" enable-background="new 0 0 57 65" xml:space="preserve"> +<g> + + <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="26.3398" y1="3115.7266" x2="27.5807" y2="3145.5239" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)"> + <stop offset="0.2711" style="stop-color:#FFAB4F"/> + <stop offset="1" style="stop-color:#FFD28F"/> + </linearGradient> + <path fill="url(#SVGID_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M49.529,51.225c-4.396-4.396-10.951-5.884-12.063-6.109 + V37.8H19.278c0,0,0.038,6.903,0,6.868c0,0-6.874,0.997-12.308,6.432C1.378,56.691,0.5,62.77,0.5,62.77 + c0,1.938,1.575,3.492,3.523,3.492h48.51c1.947,0,3.521-1.558,3.521-3.492C56.055,62.768,54.211,55.906,49.529,51.225z"/> + + <radialGradient id="face_x5F_white_1_" cx="27.5835" cy="3117.4922" r="23.425" fx="23.0139" fy="3115.0024" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#FFD28F"/> + <stop offset="1" style="stop-color:#FFAB4F"/> + </radialGradient> + <path id="face_x5F_white_3_" fill="url(#face_x5F_white_1_)" stroke="#ED9135" stroke-miterlimit="10" d="M43.676,23.357 + c0.086,10.2-6.738,18.52-15.25,18.586c-8.5,0.068-15.464-8.146-15.55-18.344C12.794,13.4,19.618,5.079,28.123,5.012 + C36.627,4.945,43.59,13.158,43.676,23.357z"/> + + <linearGradient id="face_highlight_1_" gradientUnits="userSpaceOnUse" x1="6468.501" y1="-12291.5195" x2="6492.1304" y2="-12384.9688" gradientTransform="matrix(0.275 0 0 -0.2733 -1752.8849 -3351.7349)"> + <stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.24"/> + <stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0.16"/> + </linearGradient> + <path id="face_highlight_3_" fill="url(#face_highlight_1_)" d="M28.415,5.625c-6.035,0.047-10.747,4.493-12.787,10.386 + c-0.664,1.919-0.294,4.043,0.98,5.629c2.73,3.398,5.729,6.283,9.461,8.088c3.137,1.518,7.535,2.385,11.893,1.247 + c2.274-0.592,3.988-2.459,4.375-4.766c0.187-1.094,0.293-2.289,0.283-3.553C42.54,13.244,36.729,5.56,28.415,5.625z"/> + <path fill="#CC9869" stroke="#99724F" stroke-width="0.9271" stroke-linecap="round" stroke-linejoin="round" d="M28.02,31.921 + c-6.78,0-6.717,3.708-6.717,3.708c0,8.133,2.985,8.788,6.955,8.788c4.243,0,6.792-0.926,6.792-8.595 + C35.051,35.822,35.881,31.921,28.02,31.921z M23.989,35.678c0-0.556,1.838-1.005,4.107-1.005c2.27,0,4.107,0.449,4.107,1.005 + C32.204,36.232,23.989,36.232,23.989,35.678z"/> + <path id="hair_x5F_gray_2_" fill="#CC9869" stroke="#99724F" stroke-linecap="round" stroke-linejoin="round" d="M20.278,13.25 + c0,0,5.321,7.25,15,3.75c2.729-0.563,9.058,1.035,9.058,1.035S40.68,1.865,27.289,2.744C9.403,4.125,12.058,25.678,12.058,25.678 + s2.768-0.684,5.036-4.802C18.068,19.106,20.278,13.25,20.278,13.25z"/> + + <radialGradient id="collar_x5F_body_1_" cx="14.9609" cy="3148.9336" r="32.4004" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#B0E8FF"/> + <stop offset="1" style="stop-color:#74AEEE"/> + </radialGradient> + <path id="collar_x5F_body_3_" fill="url(#collar_x5F_body_1_)" stroke="#5491CF" d="M0.5,62.768c0,1.938,1.575,3.494,3.523,3.494 + h48.51c1.947,0,3.521-1.559,3.521-3.494c0,0-1.844-6.861-6.525-11.543c-4.815-4.813-11.244-6.146-11.244-6.146 + c-1.771,1.655-5.61,2.802-10.063,2.802c-4.453,0-8.292-1.146-10.063-2.802c0,0-5.755,0.586-11.189,6.021 + C1.378,56.689,0.5,62.768,0.5,62.768z"/> + + <radialGradient id="collar_x5F_r_1_" cx="31.2998" cy="3139.0605" r="9.2823" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#80CCFF"/> + <stop offset="1" style="stop-color:#74AEEE"/> + </radialGradient> + <path id="collar_x5F_r_3_" fill="url(#collar_x5F_r_1_)" stroke="#5491CF" d="M38.159,41.381c0,0-0.574,2.369-3.013,4.441 + c-2.108,1.795-5.783,2.072-5.783,2.072l3.974,6.217c0,0,2.957-1.637,5.009-3.848c1.922-2.072,1.37-5.479,1.37-5.479L38.159,41.381z + "/> + + <radialGradient id="collar_x5F_l_1_" cx="18.9375" cy="3139.1016" r="9.2843" gradientTransform="matrix(1 0 0 1 0.3203 -3091.7656)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#80CCFF"/> + <stop offset="1" style="stop-color:#74AEEE"/> + </radialGradient> + <path id="collar_x5F_l_3_" fill="url(#collar_x5F_l_1_)" stroke="#5491CF" d="M18.63,41.422c0,0,0.576,2.369,3.012,4.441 + c2.109,1.793,5.785,2.072,5.785,2.072l-3.974,6.217c0,0-2.957-1.637-5.007-3.85c-1.922-2.072-1.37-5.48-1.37-5.48L18.63,41.422z"/> + + <radialGradient id="Knob2_1_" cx="27.6895" cy="2375.2871" r="0.9669" gradientTransform="matrix(1 0 0 1 0.2402 -2319.0742)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#80CCFF"/> + <stop offset="1" style="stop-color:#74AEEE"/> + </radialGradient> + <circle id="Knob2_3_" fill="url(#Knob2_1_)" stroke="#5491CF" cx="28.258" cy="56.254" r="0.584"/> + + <radialGradient id="Knob1_1_" cx="27.7275" cy="2381.5283" r="0.9669" gradientTransform="matrix(1 0 0 1 0.2402 -2319.0742)" gradientUnits="userSpaceOnUse"> + <stop offset="0" style="stop-color:#80CCFF"/> + <stop offset="1" style="stop-color:#74AEEE"/> + </radialGradient> + <circle id="Knob1_3_" fill="url(#Knob1_1_)" stroke="#5491CF" cx="28.297" cy="62.499" r="0.584"/> + <path id="path5135_5_" fill="#D54A30" stroke="#B51A19" d="M27.442,55.23c0,0-1.852,2.057-2.082,6.543c-0.23,4.488,0,4.488,0,4.488 + h6.546c0,0,0.23,0.063-0.154-4.367c-0.4-4.604-2.389-6.668-2.389-6.668L27.442,55.23L27.442,55.23z"/> + <path id="path5131_5_" fill="#D54A30" stroke="#B51A19" d="M28.325,48.688h0.125L31,52.691c0.516,0.953-1.207,1.797-1.457,2.547 + l-2.277-0.018c-0.242-0.761-2.26-1.369-1.477-2.584L28.325,48.688z"/> +</g> +</svg> + + <?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 585.16241 167.58249" + height="167.58249" + width="585.16241" + xml:space="preserve" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="Slack CMYK.svg"><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="705" + id="namedview3358" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="1.4633713" + inkscape:cx="271.33569" + inkscape:cy="125.32114" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><metadata + id="metadata8"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs6"><clipPath + id="clipPath20" + clipPathUnits="userSpaceOnUse"><path + id="path18" + d="M 0,1256.87 0,0 l 4388.72,0 0,1256.87 z" + inkscape:connector-curvature="0" /></clipPath></defs><g + id="g3411" + transform="translate(12.322913,-242.28632)"><path + d="m 93.133967,257.26632 c -2.4724,-7.60934 -10.644271,-11.77334 -18.253204,-9.30001 -7.608267,2.472 -11.7724,10.64401 -9.300533,18.252 l 37.45734,115.24281 c 2.57133,7.10786 10.24946,11.10266 17.62079,8.98133 7.68814,-2.2136 12.3748,-10.37867 9.92147,-17.93027 -0.0933,-0.2864 -37.445863,-115.24586 -37.445863,-115.24586" + style="fill:#e7a213;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path22-7" + inkscape:connector-curvature="0" /><path + d="m 35.095431,276.12365 c -2.4724,-7.608 -10.644267,-11.772 -18.2532,-9.3 -7.6082657,2.472 -11.7723997,10.64426 -9.3005327,18.2532 L 44.99903,400.3195 c 2.571334,7.10834 10.249467,11.1026 17.620267,8.98021 7.688133,-2.21198 12.375599,-10.37761 9.921866,-17.92913 -0.0932,-0.28706 -37.445732,-115.24693 -37.445732,-115.24693" + style="fill:#4dc088;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path24-8" + inkscape:connector-curvature="0" /><path + d="m 140.27983,352.71018 c 7.60933,-2.4724 11.772,-10.64427 9.3,-18.2532 -2.472,-7.60827 -10.644,-11.7724 -18.252,-9.30053 L 16.085031,362.61378 c -7.1083997,2.57134 -11.1026657,10.24947 -8.9813327,17.62027 2.213067,7.688 10.3781327,12.37507 17.9302657,9.92133 0.2864,-0.0932 115.245866,-37.4452 115.245866,-37.4452" + style="fill:#e10d63;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path26-3" + inkscape:connector-curvature="0" /><path + d="m 40.103697,385.25965 c 7.5016,-2.4376 17.169866,-5.57867 27.543733,-8.94947 -2.436934,-7.50106 -5.579067,-17.17093 -8.950533,-27.5464 l -27.5452,8.95254 8.952,27.54333" + style="fill:#3f2543;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path28-4" + inkscape:connector-curvature="0" /><path + d="m 98.142767,366.40125 c 10.413603,-3.38333 20.091733,-6.52813 27.543733,-8.94947 -2.43734,-7.50213 -5.58014,-17.174 -8.95254,-27.5516 l -27.545333,8.95267 8.95414,27.5484" + style="fill:#d01e25;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path30-9" + inkscape:connector-curvature="0" /><path + d="m 121.4225,294.67165 c 7.608,-2.4724 11.772,-10.64427 9.3,-18.25334 -2.472,-7.60933 -10.64427,-11.772 -18.2532,-9.3 L -2.7733549,304.57525 c -7.1078131,2.57133 -11.1026001,10.25 -8.9807301,17.62026 2.2130236,7.688 10.3781303,12.3756 17.9296503,9.92187 C 6.4626313,332.02418 121.4225,294.67165 121.4225,294.67165" + style="fill:#7cd3dc;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path32-2" + inkscape:connector-curvature="0" /><path + d="m 21.240764,327.22258 c 7.501467,-2.438 17.1724,-5.5796 27.548933,-8.95147 -3.383867,-10.41413 -6.528667,-20.0928 -8.950533,-27.5464 l -27.550533,8.95467 8.952133,27.5432" + style="fill:#36987b;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path34-0" + inkscape:connector-curvature="0" /><path + d="m 79.279296,308.36431 c 10.415601,-3.38386 20.095731,-6.5292 27.548934,-8.95106 -3.3844,-10.41667 -6.53026,-20.09787 -8.952133,-27.55227 l -27.551067,8.95533 8.954266,27.548" + style="fill:#5a872d;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path36-4" + inkscape:connector-curvature="0" /><path + d="m 257.17716,287.60071 c 5.09733,2.224 5.468,3.89227 1.48267,11.49067 -4.076,7.78387 -5.096,8.24733 -10.1,6.20827 -6.30133,-2.68694 -14.364,-4.726 -19.55333,-4.726 -8.524,0 -14.17734,3.0584 -14.17734,7.69173 0,15.29 48.836,7.04213 48.836,39.66094 0,16.40253 -14.08667,27.33746 -35.21333,27.33746 -11.12,0 -24.836,-3.7068 -34.288,-8.526 -4.72533,-2.40893 -5.00267,-3.79906 -0.92667,-11.5828 3.52267,-6.85786 4.63334,-7.59906 9.73067,-5.46773 8.06133,3.52133 18.256,6.2088 25.11333,6.2088 7.784,0 12.97334,-3.15107 12.97334,-7.78387 0,-14.82707 -49.66934,-7.7844 -49.66934,-39.38387 0,-16.7724 13.992,-27.98533 34.93467,-27.98533 9.82267,0 22.24,2.96507 30.85733,6.85773" + style="fill:#373d47;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path38-9" + inkscape:connector-curvature="0" /><path + d="m 296.56516,248.86498 0,121.57947 c 0,1.85373 -1.94667,3.79947 -4.72533,3.79947 l -12.78934,0 c -2.78,0 -4.72666,-1.94574 -4.72666,-3.79947 l 0,-121.57947 c 0,-6.02266 1.66933,-6.57866 11.12133,-6.57866 10.74933,0 11.12,0.74133 11.12,6.57866" + style="fill:#373d47;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path40-2" + inkscape:connector-curvature="0" /><path + d="m 328.53449,347.55592 c 0,6.20893 5.28267,10.37866 13.252,10.37866 9.63734,0 18.34667,-4.63333 23.444,-12.50986 l 0,-8.15467 c -5.09733,-1.9464 -11.30533,-3.0584 -16.86533,-3.0584 -11.86133,0 -19.83067,5.6532 -19.83067,13.34427 z m 58.936,-30.30201 0,52.35667 c 0,2.77974 -1.94533,4.72614 -4.72533,4.72614 l -12.604,0 c -2.872,0 -4.91067,-2.13134 -4.72533,-5.00374 l 0.18533,-5.65253 c -6.85733,7.59787 -16.68,11.58333 -26.50267,11.58333 -19.08933,0 -31.87733,-11.02813 -31.87733,-27.5224 0,-17.514 14.456,-29.2828 36.14,-29.37547 8.248,0 15.75333,1.4828 21.86933,3.98427 l 0,-6.02333 c 0,-9.63707 -7.59866,-15.3824 -20.47866,-15.3824 -6.024,0 -13.43734,2.41 -19.368,5.83813 -4.54134,2.59467 -5.65334,2.40933 -10.564,-4.91093 -4.81867,-7.32147 -4.63334,-8.71094 0,-11.67667 8.896,-5.74533 20.94266,-9.452 32.06266,-9.452 24.92667,0 40.588,13.5296 40.588,36.51093" + style="fill:#373d47;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path42-7" + inkscape:connector-curvature="0" /><path + d="m 474.21182,287.78618 c 4.632,2.68693 4.81867,4.16973 -0.27866,12.0464 -4.81734,7.41347 -5.652,7.87707 -10.748,5.28227 -3.89334,-2.03907 -10.10134,-3.79947 -15.19867,-3.79947 -16.03067,0 -26.688,10.56347 -26.688,26.50253 0,16.58747 10.65733,27.70734 26.688,27.70734 5.56133,0 12.51067,-2.13173 17.05067,-4.63333 4.63333,-2.68747 5.65333,-2.50214 10.564,4.63333 4.448,6.6724 4.356,8.2468 0.37066,11.0276 -7.13466,4.91147 -18.44,8.71093 -28.35466,8.71093 -29.65334,0 -49.48534,-18.99693 -49.48534,-47.44587 0,-28.26293 19.832,-47.07493 49.66934,-47.07493 9.08133,0 19.73866,3.05827 26.41066,7.0432" + style="fill:#373d47;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path44-1" + inkscape:connector-curvature="0" /><path + d="m 570.95849,362.75338 c 3.70666,4.72547 2.224,6.39427 -7.04267,9.73014 -9.452,3.42813 -10.74933,3.2428 -14.084,-1.11187 l -26.504,-35.39907 -11.86133,11.49014 0,22.98173 c 0,1.85373 -1.94533,3.79947 -4.72533,3.79947 l -12.788,0 c -2.78,0 -4.72667,-1.94574 -4.72667,-3.79947 l 0,-121.57947 c 0,-6.02266 1.668,-6.57866 11.12,-6.57866 10.74933,0 11.12,0.74133 11.12,6.57866 l 0,69.13 36.32533,-34.84213 c 3.98533,-3.8 6.20933,-3.52134 13.344,1.20466 7.87733,5.0964 8.43333,6.4864 4.63333,10.19267 l -27.05866,26.31773 32.248,41.88547" + 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-localscan.png b/img/vuls-architecture-localscan.png new file mode 100644 index 0000000000..5da77d9c26 Binary files /dev/null and b/img/vuls-architecture-localscan.png differ diff --git a/img/vuls-architecture.graphml b/img/vuls-architecture.graphml index 0639fa6c7d..b29d958c65 100644 --- a/img/vuls-architecture.graphml +++ b/img/vuls-architecture.graphml @@ -471,7 +471,7 @@ ALAS (Amazon) - Docker Containers + Docker/LXD @@ -497,7 +497,7 @@ ALAS (Amazon) - DockerHost + Host @@ -514,8 +514,7 @@ ALAS (Amazon) - Docker -Container + Container @@ -676,7 +675,6 @@ Container - @@ -706,13 +704,12 @@ BLOB - - .xml + .xml @@ -723,13 +720,12 @@ BLOB - - .txt + .txt @@ -740,13 +736,12 @@ BLOB - - .json + .json @@ -757,7 +752,6 @@ BLOB - @@ -888,11 +882,12 @@ Vulnerability data - docker exec + docker exec +lxc exec - + @@ -985,7 +980,6 @@ Vulnerability data - @@ -996,7 +990,6 @@ Vulnerability data - @@ -1007,7 +1000,6 @@ Vulnerability data - @@ -1018,7 +1010,6 @@ Vulnerability data - @@ -1029,7 +1020,6 @@ Vulnerability data - @@ -1040,7 +1030,6 @@ Vulnerability data - @@ -1059,7 +1048,6 @@ Vulnerability data - @@ -1070,7 +1058,6 @@ Vulnerability data - @@ -1081,7 +1068,6 @@ Vulnerability data - diff --git a/img/vuls-architecture.png b/img/vuls-architecture.png index 08a81457c7..07305b9ce9 100644 Binary files a/img/vuls-architecture.png and b/img/vuls-architecture.png differ diff --git a/models/models.go b/models/models.go index 1ad9976863..1ac3e127a5 100644 --- a/models/models.go +++ b/models/models.go @@ -162,47 +162,43 @@ func (r ScanResult) ReportKeyName() (name string) { // ServerInfo returns server name one line func (r ScanResult) ServerInfo() string { - hostinfo := "" if len(r.Container.ContainerID) == 0 { - hostinfo = fmt.Sprintf( - "%s (%s%s)", - r.ServerName, - r.Family, - r.Release, - ) - } else { - hostinfo = fmt.Sprintf( - "%s / %s (%s%s) on %s", - r.Container.Name, - r.Container.ContainerID, - r.Family, - r.Release, - r.ServerName, + return fmt.Sprintf("%s (%s%s)", + r.ServerName, r.Family, r.Release, ) } - return hostinfo + return fmt.Sprintf( + "%s / %s (%s%s) on %s", + r.Container.Name, + r.Container.ContainerID, + r.Family, + r.Release, + r.ServerName, + ) } // ServerInfoTui returns server infromation for TUI sidebar func (r ScanResult) ServerInfoTui() string { - hostinfo := "" if len(r.Container.ContainerID) == 0 { - hostinfo = fmt.Sprintf( - "%s (%s%s)", - r.ServerName, - r.Family, - r.Release, - ) - } else { - hostinfo = fmt.Sprintf( - "|-- %s (%s%s)", - r.Container.Name, - r.Family, - r.Release, - // r.Container.ContainerID, - ) + return fmt.Sprintf("%s (%s%s)", + r.ServerName, r.Family, r.Release) + } + return fmt.Sprintf( + "|-- %s (%s%s)", + r.Container.Name, + r.Family, + r.Release, + // r.Container.ContainerID, + ) +} + +// FormatServerName returns server and contianer name +func (r ScanResult) FormatServerName() string { + if len(r.Container.ContainerID) == 0 { + return r.ServerName } - return hostinfo + return fmt.Sprintf("%s@%s", + r.Container.Name, r.ServerName) } // CveSummary summarize the number of CVEs group by CVSSv2 Severity diff --git a/report/stdout.go b/report/stdout.go index ef963c58c6..21c655655d 100644 --- a/report/stdout.go +++ b/report/stdout.go @@ -30,8 +30,8 @@ type StdoutWriter struct{} // 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.Println("One Line Summary") + fmt.Println("================") fmt.Printf("%s\n", toScanSummary(rs...)) } diff --git a/report/util.go b/report/util.go index 667abe210f..1bc650efed 100644 --- a/report/util.go +++ b/report/util.go @@ -35,7 +35,7 @@ func toScanSummary(rs ...models.ScanResult) string { table.Wrap = true for _, r := range rs { cols := []interface{}{ - r.ServerName, + r.FormatServerName(), fmt.Sprintf("%s%s", r.Family, r.Release), fmt.Sprintf("%d CVEs", len(r.ScannedCves)), r.Packages.ToUpdatablePacksSummary(), @@ -51,7 +51,7 @@ func toOneLineSummary(rs ...models.ScanResult) string { table.Wrap = true for _, r := range rs { cols := []interface{}{ - r.ServerName, + r.FormatServerName(), r.CveSummary(), r.Packages.ToUpdatablePacksSummary(), } diff --git a/scan/base.go b/scan/base.go index f6f55fafe5..313475c1b3 100644 --- a/scan/base.go +++ b/scan/base.go @@ -41,8 +41,8 @@ type base struct { errs []error } -func (l *base) ssh(cmd string, sudo bool) sshResult { - return sshExec(l.ServerInfo, cmd, sudo, l.log) +func (l *base) exec(cmd string, sudo bool) execResult { + return exec(l.ServerInfo, cmd, sudo, l.log) } func (l *base) setServerInfo(c config.ServerInfo) { @@ -143,7 +143,7 @@ func (l *base) exitedContainers() (containers []config.Container, err error) { func (l *base) dockerPs(option string) (string, error) { cmd := fmt.Sprintf("docker ps %s", option) - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if !r.isSuccess() { return "", fmt.Errorf("Failed to SSH: %s", r) } @@ -152,7 +152,7 @@ func (l *base) dockerPs(option string) (string, error) { func (l *base) lxdPs(option string) (string, error) { cmd := fmt.Sprintf("lxc list %s", option) - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if !r.isSuccess() { return "", fmt.Errorf("failed to SSH: %s", r) } @@ -180,7 +180,7 @@ func (l *base) parseDockerPs(stdout string) (containers []config.Container, err func (l *base) parseLxdPs(stdout string) (containers []config.Container, err error) { lines := strings.Split(stdout, "\n") for i, line := range lines[3:] { - if i % 2 == 1 { + if i%2 == 1 { continue } fields := strings.Fields(strings.Replace(line, "|", " ", -1)) @@ -219,9 +219,9 @@ func (l *base) detectPlatform() error { } func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { - if r := l.ssh("type curl", noSudo); r.isSuccess() { + if r := l.exec("type curl", noSudo); r.isSuccess() { cmd := "curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id" - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { @@ -239,9 +239,9 @@ func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { } } - if r := l.ssh("type wget", noSudo); r.isSuccess() { + if r := l.exec("type wget", noSudo); r.isSuccess() { cmd := "wget --tries=3 --timeout=1 --no-proxy -q -O - http://169.254.169.254/latest/meta-data/instance-id" - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { diff --git a/scan/debian.go b/scan/debian.go index 22f4248d1a..9dd2dce25b 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -48,9 +48,9 @@ func newDebian(c config.ServerInfo) *debian { func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) { deb = newDebian(c) - if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { + if r := exec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { if r.Error != nil { - return false, deb, r.Error + return false, deb, nil } if r.ExitStatus == 255 { return false, deb, fmt.Errorf( @@ -60,7 +60,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return false, deb, nil } - if r := sshExec(c, "lsb_release -ir", noSudo); r.isSuccess() { + if r := exec(c, "lsb_release -ir", noSudo); r.isSuccess() { // e.g. // root@fa3ec524be43:/# lsb_release -ir // Distributor ID: Ubuntu @@ -79,7 +79,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return true, deb, nil } - if r := sshExec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() { // e.g. // DISTRIB_ID=Ubuntu // DISTRIB_RELEASE=14.04 @@ -100,7 +100,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err // Debian cmd := "cat /etc/debian_version" - if r := sshExec(c, cmd, noSudo); r.isSuccess() { + if r := exec(c, cmd, noSudo); r.isSuccess() { deb.setDistro("debian", trim(r.Stdout)) return true, deb, nil } @@ -114,7 +114,7 @@ func trim(str string) string { } func (o *debian) checkIfSudoNoPasswd() error { - r := o.ssh("apt-get -v", sudo) + r := o.exec("apt-get -v", sudo) if !r.isSuccess() { o.log.Errorf("sudo error on %s", r) return fmt.Errorf("Failed to sudo: %s", r) @@ -133,7 +133,7 @@ func (o *debian) checkDependencies() error { // Because unable to get changelogs via apt-get changelog on Debian. name := "aptitude" cmd := name + " -h" - if r := o.ssh(cmd, noSudo); !r.isSuccess() { + if r := o.exec(cmd, noSudo); !r.isSuccess() { o.lackDependencies = []string{name} } return nil @@ -151,7 +151,7 @@ func (o *debian) install() error { // apt-get update o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to SSH: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -159,7 +159,7 @@ func (o *debian) install() error { for _, name := range o.lackDependencies { cmd = util.PrependProxyEnv("apt-get install -y " + name) - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to SSH: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -188,7 +188,7 @@ func (o *debian) scanPackages() error { } func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) { - r := o.ssh("dpkg-query -W", noSudo) + r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return packs, fmt.Errorf("Failed to SSH: %s", r) } @@ -232,7 +232,7 @@ func (o *debian) parseScannedPackagesLine(line string) (name, version string, er func (o *debian) checkRequiredPackagesInstalled() error { if o.Distro.Family == "debian" { - if r := o.ssh("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { + if r := o.exec("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { msg := fmt.Sprintf("aptitude is not installed: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -244,7 +244,7 @@ func (o *debian) checkRequiredPackagesInstalled() 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() { + if r := o.exec(cmd, sudo); !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -328,7 +328,7 @@ func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []m names = append(names, p.Name) } cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) - r := o.ssh(cmd, sudo) + r := o.exec(cmd, sudo) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -350,7 +350,7 @@ func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []m func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get upgrade --dry-run") - r := o.ssh(cmd, sudo) + r := o.exec(cmd, sudo) if r.isSuccess(0, 1) { return o.parseAptGetUpgrade(r.Stdout) } @@ -529,7 +529,7 @@ func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]string, error) { } cmd = util.PrependProxyEnv(cmd) - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess() { o.log.Warnf("Failed to SSH: %s", r) // Ignore this Error. @@ -603,8 +603,7 @@ func (o *debian) parseChangelog(changelog string, } func (o *debian) splitAptCachePolicy(stdout string) map[string]string { - // re := regexp.MustCompile(`(?m:^[^ \t]+:$)`) - re := regexp.MustCompile(`(?m:^[^ \t]+:\r\n)`) + re := regexp.MustCompile(`(?m:^[^ \t]+:\r?\n)`) ii := re.FindAllStringIndex(stdout, -1) ri := []int{} for i := len(ii) - 1; 0 <= i; i-- { diff --git a/scan/freebsd.go b/scan/freebsd.go index 0abee79e6c..e1ffe5c5f0 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -46,9 +46,9 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { // Prevent from adding `set -o pipefail` option c.Distro = config.Distro{Family: "FreeBSD"} - if r := sshExec(c, "uname", noSudo); r.isSuccess() { + if r := exec(c, "uname", noSudo); r.isSuccess() { if strings.Contains(r.Stdout, "FreeBSD") == true { - if b := sshExec(c, "uname -r", noSudo); b.isSuccess() { + if b := exec(c, "uname -r", noSudo); b.isSuccess() { rel := strings.TrimSpace(b.Stdout) bsd.setDistro("FreeBSD", rel) return true, bsd @@ -97,7 +97,7 @@ func (o *bsd) scanPackages() error { func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { cmd := util.PrependProxyEnv("pkg version -v") - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -107,13 +107,13 @@ func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { const vulndbPath = "/tmp/vuln.db" cmd := "rm -f " + vulndbPath - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess(0) { return nil, fmt.Errorf("Failed to SSH: %s", r) } cmd = util.PrependProxyEnv("pkg audit -F -r -f " + vulndbPath) - r = o.ssh(cmd, noSudo) + r = o.exec(cmd, noSudo) if !r.isSuccess(0, 1) { return nil, fmt.Errorf("Failed to SSH: %s", r) } diff --git a/scan/redhat.go b/scan/redhat.go index cbf9ed429d..1f228f75d6 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -48,18 +48,18 @@ func newRedhat(c config.ServerInfo) *redhat { func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { red = newRedhat(c) - if r := sshExec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { red.setDistro("fedora", "unknown") Log.Warn("Fedora not tested yet: %s", r) return true, red } - if r := sshExec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() { // https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/ // e.g. // $ cat /etc/redhat-release // CentOS release 6.5 (Final) - if r := sshExec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() { re := regexp.MustCompile(`(.*) release (\d[\d.]*)`) result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout)) if len(result) != 3 { @@ -79,10 +79,10 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { return true, red } - if r := sshExec(c, "ls /etc/system-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() { family := "amazon" release := "unknown" - if r := sshExec(c, "cat /etc/system-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/system-release", noSudo); r.isSuccess() { fields := strings.Fields(r.Stdout) if len(fields) == 5 { release = fields[4] @@ -97,7 +97,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { } func (o *redhat) checkIfSudoNoPasswd() error { - r := o.ssh("yum --version", o.sudo()) + r := o.exec("yum --version", o.sudo()) if !r.isSuccess() { o.log.Errorf("sudo error on %s", r) return fmt.Errorf("Failed to sudo: %s", r) @@ -128,7 +128,7 @@ func (o *redhat) checkDependencies() error { } cmd := "rpm -q " + name - if r := o.ssh(cmd, noSudo); r.isSuccess() { + if r := o.exec(cmd, noSudo); r.isSuccess() { return nil } o.lackDependencies = []string{name} @@ -142,7 +142,7 @@ func (o *redhat) checkDependencies() error { func (o *redhat) install() error { for _, name := range o.lackDependencies { cmd := util.PrependProxyEnv("yum install -y " + name) - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { return fmt.Errorf("Failed to SSH: %s", r) } o.log.Infof("Installed: %s", name) @@ -165,7 +165,7 @@ func (o *redhat) checkRequiredPackagesInstalled() error { } cmd := "rpm -q " + packName - if r := o.ssh(cmd, noSudo); !r.isSuccess() { + if r := o.exec(cmd, noSudo); !r.isSuccess() { msg := fmt.Sprintf("%s is not installed", packName) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -194,7 +194,7 @@ func (o *redhat) scanPackages() error { func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) { cmd := "rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\n'" - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if r.isSuccess() { // e.g. // openssl 1.0.1e 30.el6.11 @@ -249,7 +249,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er cmd = fmt.Sprintf(cmd, "") } - r := o.ssh(util.PrependProxyEnv(cmd), sudo) + r := o.exec(util.PrependProxyEnv(cmd), sudo) if !r.isSuccess(0, 100) { //returns an exit code of 100 if there are available updates. return nil, fmt.Errorf("Failed to SSH: %s", r) @@ -543,7 +543,7 @@ func (o *redhat) getAllChangelog(packInfoList models.PackageInfoList) (stdout st // yum update --changelog doesn't have --color option. command += fmt.Sprintf(" LANGUAGE=en_US.UTF-8 yum %s --changelog update ", yumopts) + packageNames - r := o.ssh(command, sudo) + r := o.exec(command, sudo) if !r.isSuccess(0, 1) { return "", fmt.Errorf( "Failed to get changelog. status: %d, stdout: %s, stderr: %s", @@ -568,7 +568,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } cmd := "yum --color=never repolist" - r := o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -584,7 +584,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } else { cmd = "yum --color=never updateinfo list available --security" } - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -593,7 +593,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, // get package name, version, rel to be upgrade. // cmd = "yum check-update --security" cmd = "LANGUAGE=en_US.UTF-8 yum --color=never check-update" - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess(0, 100) { //returns an exit code of 100 if there are available updates. return nil, fmt.Errorf("Failed to SSH: %s", r) @@ -628,7 +628,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } else { cmd = "yum --color=never updateinfo --security update" } - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } diff --git a/scan/redhat_test.go b/scan/redhat_test.go index b89188f28a..d1616dd936 100644 --- a/scan/redhat_test.go +++ b/scan/redhat_test.go @@ -91,47 +91,6 @@ func TestChangeSectionState(t *testing.T) { } } -func TestParseYumUpdateinfoHeader(t *testing.T) { - r := newRedhat(config.ServerInfo{}) - var tests = []struct { - in string - out []models.PackageInfo - }{ - { - " nodejs-0.10.36-3.el6,libuv-0.10.34-1.el6,v8-3.14.5.10-17.el6 ", - []models.PackageInfo{ - { - Name: "nodejs", - Version: "0.10.36", - Release: "3.el6", - }, - { - Name: "libuv", - Version: "0.10.34", - Release: "1.el6", - }, - { - Name: "v8", - Version: "3.14.5.10", - Release: "17.el6", - }, - }, - }, - } - - for _, tt := range tests { - if a, err := r.parseYumUpdateinfoHeaderCentOS(tt.in); err != nil { - t.Errorf("err: %s", err) - } else { - if !reflect.DeepEqual(a, tt.out) { - e := pp.Sprintf("%#v", tt.out) - a := pp.Sprintf("%#v", a) - t.Errorf("expected %s, actual %s", e, a) - } - } - } -} - func TestParseYumUpdateinfoLineToGetCveIDs(t *testing.T) { r := newRedhat(config.ServerInfo{}) var tests = []struct { @@ -804,31 +763,6 @@ if-not-architecture 100-200 amzn-main } } -func TestParseYumUpdateinfoAmazonLinuxHeader(t *testing.T) { - r := newRedhat(config.ServerInfo{}) - var tests = []struct { - in string - out models.DistroAdvisory - }{ - { - "Amazon Linux AMI 2014.03 - ALAS-2015-598: low priority package update for grep", - models.DistroAdvisory{ - AdvisoryID: "ALAS-2015-598", - Severity: "low", - }, - }, - } - - for _, tt := range tests { - a, _, _ := r.parseYumUpdateinfoHeaderAmazon(tt.in) - if !reflect.DeepEqual(a, tt.out) { - e := pp.Sprintf("%v", tt.out) - a := pp.Sprintf("%v", a) - t.Errorf("expected %s, actual %s", e, a) - } - } -} - func TestParseYumUpdateinfoListAvailable(t *testing.T) { r := newRedhat(config.ServerInfo{}) rhelStdout := `RHSA-2015:2315 Moderate/Sec. NetworkManager-1:1.0.6-27.el7.x86_64 diff --git a/scan/serverapi.go b/scan/serverapi.go index 2b8fa9aed9..efc28dfce1 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -105,6 +105,9 @@ func detectOS(c config.ServerInfo) (osType osTypeInterface) { Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port) return } + + //TODO darwin https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/darwin.rb + osType.setServerInfo(c) osType.setErrs([]error{fmt.Errorf("Unknown OS Type")}) return diff --git a/scan/sshutil.go b/scan/sshutil.go index bf029259cd..fa094ce7fd 100644 --- a/scan/sshutil.go +++ b/scan/sshutil.go @@ -25,7 +25,7 @@ import ( "io/ioutil" "net" "os" - "os/exec" + ex "os/exec" "strings" "syscall" "time" @@ -39,7 +39,7 @@ import ( "github.com/future-architect/vuls/util" ) -type sshResult struct { +type execResult struct { Servername string Host string Port string @@ -50,16 +50,13 @@ type sshResult struct { Error error } -func (s sshResult) String() string { +func (s execResult) String() string { return fmt.Sprintf( - "SSHResult: servername: %s, cmd: %s, exitstatus: %d, stdout: %s, stderr: %s, err: %s", + "execResult: servername: %s\n cmd: %s\n exitstatus: %d\n stdout: %s\n stderr: %s\n err: %s", s.Servername, s.Cmd, s.ExitStatus, s.Stdout, s.Stderr, s.Error) } -func (s sshResult) isSuccess(expectedStatusCodes ...int) bool { - if s.Error != nil { - return false - } +func (s execResult) isSuccess(expectedStatusCodes ...int) bool { if len(expectedStatusCodes) == 0 { return s.ExitStatus == 0 } @@ -68,6 +65,9 @@ func (s sshResult) isSuccess(expectedStatusCodes ...int) bool { return true } } + if s.Error != nil { + return false + } return false } @@ -148,8 +148,11 @@ func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs [] return } -func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) { - if conf.Conf.SSHExternal { +func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result execResult) { + if c.Port == "local" && + (c.Host == "127.0.0.1" || c.Host == "localhost") { + result = localExec(c, cmd, sudo) + } else if conf.Conf.SSHExternal { result = sshExecExternal(c, cmd, sudo) } else { result = sshExecNative(c, cmd, sudo) @@ -160,7 +163,37 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re return } -func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) { +func localExec(c conf.ServerInfo, cmdstr string, sudo bool) (result execResult) { + cmdstr = decolateCmd(c, cmdstr, sudo) + var cmd *ex.Cmd + if c.Distro.Family == "FreeBSD" { + cmd = ex.Command("/bin/sh", "-c", cmdstr) + } else { + cmd = ex.Command("/bin/bash", "-c", cmdstr) + } + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + if err := cmd.Run(); err != nil { + result.Error = err + if exitError, ok := err.(*ex.ExitError); ok { + waitStatus := exitError.Sys().(syscall.WaitStatus) + result.ExitStatus = waitStatus.ExitStatus() + } else { + result.ExitStatus = 999 + } + } else { + result.ExitStatus = 0 + } + + result.Stdout = stdoutBuf.String() + result.Stderr = stderrBuf.String() + result.Cmd = strings.Replace(cmdstr, "\n", "", -1) + return +} + +func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result execResult) { result.Servername = c.ServerName result.Host = c.Host result.Port = c.Port @@ -219,8 +252,8 @@ func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) return } -func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) { - sshBinaryPath, err := exec.LookPath("ssh") +func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result execResult) { + sshBinaryPath, err := ex.LookPath("ssh") if err != nil { return sshExecNative(c, cmd, sudo) } @@ -256,13 +289,13 @@ func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result sshResult // cmd = fmt.Sprintf("stty cols 256; set -o pipefail; %s", cmd) args = append(args, cmd) - execCmd := exec.Command(sshBinaryPath, args...) + execCmd := ex.Command(sshBinaryPath, args...) var stdoutBuf, stderrBuf bytes.Buffer execCmd.Stdout = &stdoutBuf execCmd.Stderr = &stderrBuf if err := execCmd.Run(); err != nil { - if e, ok := err.(*exec.ExitError); ok { + if e, ok := err.(*ex.ExitError); ok { if s, ok := e.Sys().(syscall.WaitStatus); ok { result.ExitStatus = s.ExitStatus() } else {