├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── audit ├── audit.go ├── audit_suite_test.go ├── audit_test.go ├── generate.go └── generate_test.go ├── bosh ├── bosh.go ├── bosh_suite_test.go ├── bosh_test.go └── mock_target_deployment.go ├── cmd ├── proc_scan │ └── main.go └── scantron │ ├── main.go │ ├── main_test.go │ └── scantron_suite_test.go ├── commands ├── audit.go ├── audit_test.go ├── bosh_scan.go ├── commands_suite_test.go ├── direct_scan.go ├── generate.go ├── report.go ├── report_test.go └── scantron.go ├── data └── assets │ └── tls-parameters.csv ├── db ├── db_suite_test.go ├── schema.go ├── sqlite.go └── sqlite_test.go ├── docker ├── Dockerfile ├── scan.sh └── scan.yml ├── example_queries ├── hosts_on_port.sql ├── no_tls.sql └── root_processes.sql ├── filesystem ├── file_linux.go ├── file_scanner.go ├── file_scanner_test.go ├── file_walker.go ├── file_walker_test.go ├── file_windows.go ├── filesystem_suite_test.go ├── mock_file_scanner.go └── mock_file_walker.go ├── manifest ├── broken.yml ├── example.yml ├── manifest.go ├── manifest_suite_test.go ├── manifest_test.go ├── parser.go ├── parser_test.go ├── semantic_err_command.yml ├── semantic_err_prefix.yml ├── semantic_err_processes.yml └── semantic_err_specs.yml ├── netstat ├── netstat.go ├── netstat_suite_test.go └── netstat_test.go ├── process ├── mock_system_resources.go ├── process_linux.go ├── process_scanner.go ├── process_scanner_test.go ├── process_suite_test.go ├── process_windows.go └── system_resources.go ├── remotemachine ├── mock_remote_machine.go └── remote_machine.go ├── report ├── insecure_ssh_key_report.go ├── insecure_ssh_key_report_test.go ├── report.go ├── report_suite_test.go ├── root_processes_report.go ├── root_processes_report_test.go ├── tls_violations_report.go ├── tls_violations_report_test.go ├── world_readable_files_report.go └── world_readable_files_report_test.go ├── scanlog └── log.go ├── scanner ├── bosh.go ├── bosh_test.go ├── direct.go ├── direct_test.go ├── scanner.go └── scanner_suite_test.go ├── scantron.go ├── scripts ├── build ├── build_in_docker ├── low_coverage └── test ├── ssh ├── ssh_scanner.go ├── ssh_scanner_test.go └── ssh_suite_test.go ├── tlsscan ├── ciphers.go ├── ciphers_test.go ├── dialer.go ├── mock_tls_scanner.go ├── tls.go ├── tls_scan.go ├── tls_scan_test.go ├── tls_scanner.go ├── tls_test.go └── tlsscan_suite_test.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /spec/examples.txt 4 | *.swp 5 | vendor/** 6 | *.coverprofile 7 | 8 | # Scanning Artifacts 9 | /scantron 10 | /proc_scan 11 | /*.db 12 | cmd/proc_scan/proc_scan 13 | cmd/scantron/scantron 14 | 15 | 16 | # statik 17 | /data/proc_scan/** 18 | /statik* 19 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 2 | # for detailed Gopkg.toml documentation. 3 | # 4 | # required = ["github.com/user/thing/cmd/thing"] 5 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 6 | # 7 | # [[constraint]] 8 | # name = "github.com/user/project" 9 | # version = "1.0.0" 10 | # 11 | # [[constraint]] 12 | # name = "github.com/user/project2" 13 | # branch = "dev" 14 | # source = "github.com/myfork/project2" 15 | # 16 | # [[override]] 17 | # name = "github.com/x/y" 18 | # version = "2.4.0" 19 | # 20 | # [prune] 21 | # non-go = false 22 | # go-tests = true 23 | # unused-packages = true 24 | 25 | [[constraint]] 26 | name = "github.com/cloudfoundry/bosh-cli" 27 | version = "5.2.2" 28 | 29 | [[override]] 30 | revision = "407dd7546455f1a0a50feba2dcaef0ae88da1810" 31 | name = "github.com/cloudfoundry/bosh-utils" 32 | 33 | [[constraint]] 34 | revision = "576b6af77ae4f3ccef356902fb3b9a9ae22e0b11" 35 | name = "github.com/cppforlife/go-semi-semantic" 36 | 37 | [[constraint]] 38 | name = "github.com/jessevdk/go-flags" 39 | version = "1.4.0" 40 | 41 | [[constraint]] 42 | revision = "668c8856d9992f97248b3177d45743d2cc1068db" 43 | name = "github.com/keybase/go-ps" 44 | 45 | [[constraint]] 46 | name = "github.com/mattn/go-sqlite3" 47 | version = "1.9.0" 48 | 49 | [[constraint]] 50 | revision = "d4647c9c7a84d847478d890b816b7d8b62b0b279" 51 | name = "github.com/olekukonko/tablewriter" 52 | 53 | [[constraint]] 54 | name = "github.com/onsi/ginkgo" 55 | version = "1.6.0" 56 | 57 | [[constraint]] 58 | name = "github.com/onsi/gomega" 59 | version = "1.4.1" 60 | 61 | [[override]] 62 | revision = "a64ae2051c20c5cde13292a41db01029566134ba" 63 | name = "github.com/pivotal-cf/paraphernalia" 64 | 65 | [[constraint]] 66 | name = "github.com/pkg/sftp" 67 | version = "1.8.2" 68 | 69 | [[constraint]] 70 | name = "go.uber.org/zap" 71 | version = "1.9.1" 72 | 73 | [[override]] 74 | revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4" 75 | name = "golang.org/x/crypto" 76 | 77 | [[override]] 78 | revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" 79 | name = "golang.org/x/sync" 80 | 81 | [[override]] 82 | name = "golang.org/x/sys" 83 | revision = "2b024373dcd9800f0cae693839fac6ede8d64a8c" 84 | 85 | [[override]] 86 | name = "golang.org/x/net" 87 | revision = "8a410e7b638dca158bf9e766925842f6651ff828" 88 | 89 | [[constraint]] 90 | name = "gopkg.in/yaml.v2" 91 | version = "2.2.1" 92 | 93 | # https://github.com/go-fsnotify/fsnotify/issues/1#issuecomment-374967614 94 | [[override]] 95 | name = "gopkg.in/fsnotify.v1" 96 | source = "github.com/fsnotify/fsnotify" 97 | version = "1.4.7" 98 | 99 | [[override]] 100 | name = "github.com/nu7hatch/gouuid" 101 | revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3" 102 | 103 | [[override]] 104 | name = "github.com/charlievieth/fs" 105 | revision = "7dc373669fa10ddf827c37c595dee30a2f001be9" 106 | 107 | [[override]] 108 | name = "code.cloudfoundry.org/clock" 109 | revision = "02e53af36e6c978af692887ed449b74026d76fec" 110 | 111 | [[override]] 112 | name = "github.com/cheggaaa/pb" 113 | version = "1.0.25" 114 | 115 | [[override]] 116 | name = "github.com/cloudfoundry/go-socks5" 117 | revision = "54f73bdb8a8e85a6611d3d29ab9bbf98c2a060d7" 118 | 119 | [[override]] 120 | name = "github.com/cloudfoundry/socks5-proxy" 121 | revision = "3659db090cb23a57807b53e2f8580b2427dc2659" 122 | 123 | [[override]] 124 | name = "github.com/dustin/go-humanize" 125 | revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e" 126 | 127 | [[constraint]] 128 | name = "github.com/golang/mock" 129 | version = "1.1.1" 130 | 131 | [[constraint]] 132 | name = "github.com/rakyll/statik" 133 | version = "0.1.4" 134 | 135 | [[constraint]] 136 | name = "github.com/hectane/go-acl" 137 | revision = "7f56832555fc229dad908c67d65ed3ce6156b70c" 138 | 139 | [prune] 140 | go-tests = true 141 | unused-packages = true 142 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bin/scantron: data/proc_scan 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Scantron 2 | 3 | Copyright (c) 2016 - Present Pivotal Software, Inc. All Rights Reserved. 4 | 5 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | You may not use this product except in compliance with the License. 7 | 8 | This product may include a number of subcomponents with separate copyright notices 9 | and license terms. Your use of these subcomponents is subject to the terms and 10 | conditions of the subcomponent's license, as noted in the LICENSE file. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scantron 2 | 3 | > scan machines for unexpected processes, ports, or permissions 4 | 5 | ## Purpose 6 | 7 | Scantron is a tool for performing a census of processes, ports, protocols, and 8 | file permissions on VMs. Scans can be performed against single VMs, or against 9 | all VMs in a bosh deployment. The intention is to provide a point-in-time scan 10 | which can be analysed offline. Scans are stored in a SQLite database. The CLI 11 | provides a summary report format or the SQLite db can be queried directly. 12 | 13 | ## Usage 14 | 15 | Whether you scan a single host (`direct-scan`) or all VMs in a bosh deployment 16 | (`bosh-scan`) the results of the scan will be stored in a SQLite file, or 17 | appended to an existing file. 18 | 19 | #### single host scan 20 | 21 | You can perform a direct scan (a scan of a single host) by using the following 22 | command: 23 | 24 | scantron direct-scan \ 25 | --address scanme.example.com 26 | --username ubuntu \ 27 | --password hunter2 \ 28 | [--private-key ~/.ssh/id_rsa_scantron] 29 | 30 | The password is always required because we use it to `sudo` on the machine for 31 | the scan. You may optionally pass a private key for authenticating SSH. 32 | 33 | #### bosh deployment scan 34 | 35 | Scantron is typically used in CI jobs and by other machines and so only 36 | supports authenticating with client credentials with a BOSH director at the 37 | moment. You can create a client for use with Scantron like so: 38 | 39 | 1. `uaac target :` 40 | 2. `uaac token owner get login admin` 41 | 3. `uaac client add scantron -s --authorized_grant_types client_credentials --scope bosh.admin --authorities bosh.admin --access_token_validity 600 --refresh_token_validity 86400` 42 | 43 | You can then scan a BOSH deployment with the following command: 44 | 45 | scantron bosh-scan \ 46 | --director-url \ 47 | --bosh-deployment \ 48 | --client scantron \ 49 | --client-secret \ 50 | [--ca-cert bosh.pem] 51 | 52 | Multiple deployments can be specified and the results merged into a single database. 53 | 54 | scantron bosh-scan \ 55 | --director-url \ 56 | --bosh-deployment \ 57 | --bosh-deployment \ 58 | --client scantron \ 59 | --client-secret \ 60 | [--ca-cert bosh.pem] 61 | 62 | **Note:** The scan expects to be able to reach the BOSH machines directly at 63 | the moment so that it can scan the endpoints for their TLS configuration. A 64 | jumpbox is normally a good machine to run this from. 65 | 66 | #### File Content Check 67 | 68 | The file scan can optionally flag files if the content matches a specified regex. For performance optimization 69 | an optional path regex and maximum file size can be specified to limit which files have to be read. The maximum 70 | file size defaults to 1MB. 71 | 72 | scantron bosh-scan|direct-scan \ 73 | --content \ 74 | [--path ] \ 75 | [--max ] 76 | 77 | Regexes use the [golang syntax](https://golang.org/pkg/regexp/syntax/). 78 | 79 | ### Checking Reports 80 | 81 | After you run a scan a report is saved to a SQLite database, by default 82 | `database.db`. 83 | 84 | With this report it is possible to do the following: 85 | 86 | * Generate a summary of the findings. 87 | 88 | scantron report 89 | 90 | The report has sections for: 91 | * Externally-accessible processes running as root 92 | * Excluding sshd and rpcbind 93 | * Processes using non-approved SSL/TLS settings 94 | * Current recommendation is TLS 1.2 and ciphers recommended by 95 | https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4 96 | * World-readable files 97 | * Filtered for files from bosh releases (/var/vcap/data/jobs/%) 98 | * Duplicate SSH keys 99 | 100 | * Check to see if any unexpected processes or ports are present in your 101 | cluster. 102 | 103 | scantron audit --manifest manifest-of-expected-things.yml 104 | 105 | The output from `audit` lists the audited host(s) along with either `err` or 106 | `ok`. Where there are discrepancies with the manifest are highlighted. If 107 | there are any discrepancies the exit code will be `3`, otherwise it is `0`. 108 | 109 | * Generate a manifest (preliminary) of "known good" ports and processes. 110 | 111 | scantron generate-manifest > manifest.yml 112 | 113 | **Note:** Some hand-tweaking may be necessary if there are non-deterministic 114 | ports in your cluster as the generated manifest will contain exactly those 115 | found in the latest scan. 116 | 117 | ## Notes 118 | 119 | ### Scan Filter 120 | 121 | Scantron only scans regular files and skips the following directories: 122 | 123 | * `/proc` 124 | * `/sys` 125 | * `/dev` 126 | * `/run` 127 | 128 | ### Database Schema 129 | 130 | Scantron produces a SQLite database for scan reports. The database schema can 131 | be found in [schema.go](https://github.com/pivotal-cf/scantron/blob/master/db/schema.go). 132 | 133 | Scantron does not currently support database migrations. You will be prompted 134 | to create a new database when there are backwards-incompatible changes to the 135 | schema. 136 | 137 | Each scan creates a report with many hosts in it. Hosts represent scanned VMs 138 | which contain the list of world writable files and processes running on that 139 | machine. Each process is referenced by the port it is listening on and its 140 | environment variables. TLS information is provided for a port when the port is 141 | expecting TLS connections. 142 | 143 | ### Queries 144 | 145 | To analyze the results of the database, you can use the database schema documented 146 | above to craft your own SQL query, or use some of the example queries stored in the 147 | `example_queries` directory. 148 | 149 | * Finding all of the hosts which are listening on a particular port: 150 | - hosts_on_port.sql 151 | * Finding all connections not using TLS: 152 | - no_tls.sql 153 | * Finding all processes running as `root` 154 | - root_processes.sql 155 | 156 | Once you have your query, run `sqlite` and specify the query you want to run to generate 157 | results. Tip: You can include `.mode.csv` at the end of your argument to spit out the results 158 | in a CSV format that can be imported into the spreadsheet software of your choice. 159 | 160 | ### Manifest Format 161 | 162 | Scantron audits the hosts, processes, and ports in the database against the 163 | user-generated manifest file. 164 | 165 | For Ops Manager where VMs can have the same prefix, such as cloud_controller 166 | and cloud_controller_worker, append "-" to the prefixes: "cloud_controller-" 167 | and "cloud_controller_worker-". 168 | 169 | Many hosts (especially those which are based of the BOSH stemcell) will start 170 | processes that bind to an ephemeral, random port when they start. To avoid 171 | caring about these ports when we perform an audit you can add `ignore_ports: 172 | true` to the process. There is an example of this below for the `rpc.statd` 173 | process. 174 | 175 | This is an example of the manifest file: 176 | 177 | ``` yaml 178 | specs: 179 | - prefix: cloud_controller- 180 | processes: 181 | - command: sshd 182 | user: root 183 | ports: 184 | - 22 185 | - command: rpcbind 186 | user: root 187 | ports: 188 | - 111 189 | - command: metron 190 | user: vcap 191 | ports: 192 | - 6061 193 | - command: consul 194 | user: vcap 195 | ports: 196 | - 8301 197 | - command: nginx 198 | user: root 199 | ports: 200 | - 9022 201 | - command: ruby 202 | user: vcap 203 | ports: 204 | - 33861 205 | - command: rpc.statd 206 | user: root 207 | ignore_ports: true 208 | ``` 209 | 210 | ## Development 211 | 212 | ### Building 213 | 214 | 1. Install dep, the vendor package manager: https://github.com/golang/dep 215 | 2. `go get github.com/pivotal-cf/scantron` 216 | 3. `cd $GOPATH/src/github.com/pivotal-cf/scantron` 217 | 4. `dep ensure` # Note: will fail with exit code 1 due to `doublestar` test files 218 | 5. `./scripts/build` 219 | 220 | ### Testing 221 | 222 | 1. `./scripts/test` 223 | 2. There is no step 2. 224 | -------------------------------------------------------------------------------- /audit/audit_suite_test.go: -------------------------------------------------------------------------------- 1 | package audit_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestAudit(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Audit Suite") 13 | } 14 | -------------------------------------------------------------------------------- /audit/generate.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "io" 7 | 8 | "github.com/pivotal-cf/scantron/manifest" 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func GenerateManifest(writer io.Writer, db *sql.DB) error { 13 | m := manifest.Manifest{} 14 | 15 | specs, err := getSpecsFor(db) 16 | if err != nil { 17 | return err 18 | } 19 | m.Specs = specs 20 | 21 | bs, err := yaml.Marshal(m) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | _, err = io.Copy(writer, bytes.NewReader(bs)) 27 | return err 28 | } 29 | 30 | func getSpecsFor(db *sql.DB) ([]manifest.Spec, error) { 31 | rows, err := db.Query(`SELECT hosts.id, hosts.name FROM hosts`) 32 | 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | defer rows.Close() 38 | 39 | var hostId int 40 | var hostName string 41 | var specs []manifest.Spec 42 | 43 | for rows.Next() { 44 | err := rows.Scan(&hostId, &hostName) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | spec := manifest.Spec{ 50 | Prefix: hostName, 51 | } 52 | 53 | processes, err := getProcessesFor(db, hostId) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | spec.Processes = processes 59 | specs = append(specs, spec) 60 | } 61 | 62 | return specs, nil 63 | } 64 | 65 | func getProcessesFor(db *sql.DB, hostId int) ([]manifest.Process, error) { 66 | processes := []manifest.Process{} 67 | 68 | processRows, err := db.Query(` 69 | SELECT DISTINCT processes.user, processes.name, processes.id 70 | FROM processes 71 | JOIN ports 72 | ON processes.id = ports.process_id 73 | WHERE processes.host_id = ? 74 | AND ports.address != "127.0.0.1" 75 | AND ports.state = "LISTEN" 76 | `, hostId) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | defer processRows.Close() 82 | 83 | var processUser, processName string 84 | 85 | for processRows.Next() { 86 | var processId int 87 | err := processRows.Scan(&processUser, &processName, &processId) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | process := manifest.Process{ 93 | Command: processName, 94 | User: processUser, 95 | } 96 | ports, err := getPortsFor(db, processId) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | process.Ports = ports 102 | processes = append(processes, process) 103 | } 104 | 105 | return processes, nil 106 | } 107 | 108 | func getPortsFor(db *sql.DB, processId int) ([]manifest.Port, error) { 109 | ports := []manifest.Port{} 110 | 111 | rows, err := db.Query(` 112 | SELECT ports.number 113 | FROM ports 114 | WHERE ports.process_id = ? 115 | AND ports.address != "127.0.0.1" 116 | AND ports.state = "LISTEN" 117 | `, processId) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | defer rows.Close() 123 | 124 | var portNumber manifest.Port 125 | 126 | for rows.Next() { 127 | err := rows.Scan(&portNumber) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | ports = append(ports, portNumber) 133 | } 134 | 135 | return ports, nil 136 | } 137 | -------------------------------------------------------------------------------- /audit/generate_test.go: -------------------------------------------------------------------------------- 1 | package audit_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | yaml "gopkg.in/yaml.v2" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/pivotal-cf/scantron" 14 | "github.com/pivotal-cf/scantron/audit" 15 | "github.com/pivotal-cf/scantron/db" 16 | "github.com/pivotal-cf/scantron/manifest" 17 | "github.com/pivotal-cf/scantron/scanner" 18 | ) 19 | 20 | var _ = Describe("Generate", func() { 21 | var ( 22 | database *db.Database 23 | tmpdir string 24 | 25 | writer *bytes.Buffer 26 | hosts scanner.ScanResult 27 | ) 28 | 29 | BeforeEach(func() { 30 | writer = &bytes.Buffer{} 31 | var err error 32 | tmpdir, err = ioutil.TempDir("", "audit") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | database, err = db.CreateDatabase(filepath.Join(tmpdir, "database.db")) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | AfterEach(func() { 40 | database.Close() 41 | os.RemoveAll(tmpdir) 42 | }) 43 | 44 | JustBeforeEach(func() { 45 | err := database.SaveReport("cf1", hosts) 46 | Expect(err).NotTo(HaveOccurred()) 47 | err = audit.GenerateManifest(writer, database.DB()) 48 | Expect(err).NotTo(HaveOccurred()) 49 | }) 50 | 51 | Context("when hosts is empty", func() { 52 | BeforeEach(func() { 53 | hosts = scanner.ScanResult{ 54 | JobResults: []scanner.JobResult{}, 55 | } 56 | }) 57 | 58 | It("shows empty manifest", func() { 59 | var m manifest.Manifest 60 | err := yaml.Unmarshal(writer.Bytes(), &m) 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(m.Specs).To(BeEmpty()) 63 | }) 64 | }) 65 | 66 | Context("when a single host exists", func() { 67 | BeforeEach(func() { 68 | hosts = scanner.ScanResult{ 69 | JobResults: []scanner.JobResult{ 70 | { 71 | Job: "My Host", 72 | Services: []scantron.Process{ 73 | { 74 | CommandName: "some process", 75 | User: "some user", 76 | Ports: []scantron.Port{ 77 | port(22), 78 | port(80), 79 | }, 80 | }, 81 | { 82 | CommandName: "another process", 83 | User: "another user", 84 | Ports: []scantron.Port{ 85 | port(443), 86 | port(8080), 87 | { 88 | Number: 1024, 89 | Address: "127.0.0.1", 90 | }, 91 | { 92 | Number: 1024, 93 | State: "ESTABLISHED", 94 | }, 95 | { 96 | Number: 1012, 97 | State: "CLOSE_WAIT", 98 | }, 99 | }, 100 | }, 101 | { 102 | CommandName: "non-listening process", 103 | User: "non-listening user", 104 | }, 105 | { 106 | CommandName: "only-bad-ports process", 107 | User: "only-bad-ports user", 108 | Ports: []scantron.Port{ 109 | { 110 | Number: 1024, 111 | Address: "127.0.0.1", 112 | }, 113 | { 114 | Number: 1024, 115 | State: "ESTABLISHED", 116 | }, 117 | { 118 | Number: 1012, 119 | State: "CLOSE_WAIT", 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | } 127 | }) 128 | 129 | It("shows host with services and processes", func() { 130 | var m manifest.Manifest 131 | err := yaml.Unmarshal(writer.Bytes(), &m) 132 | Expect(err).NotTo(HaveOccurred()) 133 | Expect(m.Specs).To(HaveLen(1)) 134 | Expect(m.Specs[0].Prefix).To(Equal(hosts.JobResults[0].Job)) 135 | Expect(m.Specs[0].Processes).To(ConsistOf( 136 | manifest.Process{ 137 | Command: hosts.JobResults[0].Services[0].CommandName, 138 | User: hosts.JobResults[0].Services[0].User, 139 | Ports: []manifest.Port{22, 80}, 140 | }, 141 | manifest.Process{ 142 | Command: hosts.JobResults[0].Services[1].CommandName, 143 | User: hosts.JobResults[0].Services[1].User, 144 | Ports: []manifest.Port{443, 8080}, 145 | }, 146 | )) 147 | }) 148 | 149 | It("does not show the ignore_ports field", func() { 150 | Expect(writer.String()).NotTo(ContainSubstring("ignore_ports")) 151 | }) 152 | }) 153 | 154 | Context("when multiple hosts exists", func() { 155 | BeforeEach(func() { 156 | hosts = scanner.ScanResult{ 157 | JobResults: []scanner.JobResult{ 158 | { 159 | Job: "My Host", 160 | Services: []scantron.Process{ 161 | { 162 | CommandName: "some process", 163 | User: "some user", 164 | Ports: []scantron.Port{ 165 | port(22), 166 | port(80), 167 | }, 168 | }, 169 | }, 170 | }, 171 | { 172 | Job: "My Other Host", 173 | Services: []scantron.Process{ 174 | { 175 | CommandName: "some other process", 176 | User: "some other user", 177 | Ports: []scantron.Port{ 178 | port(443), 179 | port(8080), 180 | }, 181 | }, 182 | }, 183 | }, 184 | }, 185 | } 186 | }) 187 | 188 | It("shows host with processes", func() { 189 | var m manifest.Manifest 190 | err := yaml.Unmarshal(writer.Bytes(), &m) 191 | Expect(err).NotTo(HaveOccurred()) 192 | Expect(m.Specs).To(HaveLen(2)) 193 | Expect(m.Specs[0].Prefix).To(Equal(hosts.JobResults[0].Job)) 194 | Expect(m.Specs[0].Processes).To(ConsistOf(manifest.Process{ 195 | Command: hosts.JobResults[0].Services[0].CommandName, 196 | User: hosts.JobResults[0].Services[0].User, 197 | Ports: []manifest.Port{22, 80}, 198 | })) 199 | 200 | Expect(m.Specs[1].Prefix).To(Equal(hosts.JobResults[1].Job)) 201 | Expect(m.Specs[1].Processes).To(ConsistOf(manifest.Process{ 202 | Command: hosts.JobResults[1].Services[0].CommandName, 203 | User: hosts.JobResults[1].Services[0].User, 204 | Ports: []manifest.Port{443, 8080}, 205 | })) 206 | }) 207 | }) 208 | }) 209 | 210 | func port(number int) scantron.Port { 211 | return scantron.Port{ 212 | Number: number, 213 | State: "LISTEN", 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /bosh/bosh.go: -------------------------------------------------------------------------------- 1 | package bosh 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | 9 | boshconfig "github.com/cloudfoundry/bosh-cli/cmd/config" 10 | boshdir "github.com/cloudfoundry/bosh-cli/director" 11 | boshuaa "github.com/cloudfoundry/bosh-cli/uaa" 12 | boshlog "github.com/cloudfoundry/bosh-utils/logger" 13 | boshuuid "github.com/cloudfoundry/bosh-utils/uuid" 14 | 15 | "github.com/pivotal-cf/scantron" 16 | "github.com/pivotal-cf/scantron/remotemachine" 17 | "github.com/pivotal-cf/scantron/scanlog" 18 | "golang.org/x/crypto/ssh" 19 | ) 20 | 21 | type TargetDeployment interface { 22 | Name() string 23 | VMs() []boshdir.VMInfo 24 | Releases() []boshdir.Release 25 | 26 | Setup() error 27 | ConnectTo(boshdir.VMInfo) remotemachine.RemoteMachine 28 | Cleanup() error 29 | } 30 | 31 | type TargetDeploymentImpl struct { 32 | sshOpts boshdir.SSHOpts 33 | signer ssh.Signer 34 | deployment boshdir.Deployment 35 | logger scanlog.Logger 36 | } 37 | 38 | func GetDeployments( 39 | creds boshconfig.Creds, 40 | caCertPath string, 41 | deploymentNames []string, 42 | boshURL string, 43 | logger scanlog.Logger) ([]TargetDeployment, error) { 44 | 45 | var caCert string 46 | 47 | if caCertPath != "" { 48 | caCertBytes, err := ioutil.ReadFile(caCertPath) 49 | if err != nil { 50 | logger.Errorf("Failed to load CA certificate (%s): %s", caCertPath, err) 51 | return nil, err 52 | } 53 | 54 | caCert = string(caCertBytes) 55 | } 56 | 57 | out := bufio.NewWriter(os.Stdout) 58 | boshLogger := boshlog.NewWriterLogger(boshlog.LevelNone, out) 59 | director, err := getDirector(boshURL, creds, caCert, boshLogger) 60 | if err != nil { 61 | logger.Errorf("Could not reach BOSH Director (%s): %s", boshURL, err) 62 | return nil, err 63 | } 64 | 65 | deps := []TargetDeployment{} 66 | uuidgen := boshuuid.NewGenerator() 67 | for _, depName := range deploymentNames { 68 | 69 | sshOpts, privKey, err := boshdir.NewSSHOpts(uuidgen) 70 | if err != nil { 71 | logger.Errorf("Could not create SSH options: %s", err) 72 | return nil, err 73 | } 74 | logger.Debugf("Generated user %s for deployment %s", sshOpts.Username, depName) 75 | 76 | signer, err := ssh.ParsePrivateKey([]byte(privKey)) 77 | if err != nil { 78 | logger.Errorf("Failed to parse SSH key: %s", err) 79 | return nil, err 80 | } 81 | 82 | deployment, err := director.FindDeployment(depName) 83 | if err != nil { 84 | logger.Errorf("Failed to find deployment (%s): %s", depName, err) 85 | return nil, err 86 | } 87 | logger.Debugf("Found deployment %s", deployment.Name()) 88 | deps = append(deps, &TargetDeploymentImpl{ 89 | sshOpts: sshOpts, 90 | signer: signer, 91 | deployment: deployment, 92 | logger: logger, 93 | }) 94 | } 95 | 96 | for _, d := range deps { 97 | logger.Debugf("Generated Target deployment %s", d.Name()) 98 | } 99 | 100 | return deps, nil 101 | } 102 | 103 | func (d *TargetDeploymentImpl) Name() string { 104 | return d.deployment.Name() 105 | } 106 | 107 | func (d *TargetDeploymentImpl) VMs() []boshdir.VMInfo { 108 | vms, _ := d.deployment.VMInfos() 109 | return vms 110 | } 111 | 112 | func (d *TargetDeploymentImpl) Releases() []boshdir.Release { 113 | releases, _ := d.deployment.Releases() 114 | return releases 115 | } 116 | 117 | func (d *TargetDeploymentImpl) Setup() error { 118 | d.logger.Debugf("About to setup SSH for deployment %s", d.Name()) 119 | slug := boshdir.NewAllOrInstanceGroupOrInstanceSlug("", "") 120 | 121 | _, err := d.deployment.SetUpSSH(slug, d.sshOpts) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func (d *TargetDeploymentImpl) ConnectTo(vm boshdir.VMInfo) remotemachine.RemoteMachine { 130 | stemcells, _ := d.deployment.Stemcells() 131 | return remotemachine.NewRemoteMachine(scantron.Machine{ 132 | Address: BestAddress(vm.IPs), 133 | Username: d.sshOpts.Username, 134 | Key: d.signer, 135 | OSName: stemcells[0].Name(), 136 | }) 137 | } 138 | 139 | func (d *TargetDeploymentImpl) Cleanup() error { 140 | d.logger.Debugf("About to cleanup SSH for deployment %s", d.Name()) 141 | slug := boshdir.NewAllOrInstanceGroupOrInstanceSlug("", "") 142 | err := d.deployment.CleanUpSSH(slug, d.sshOpts) 143 | return err 144 | } 145 | 146 | func getDirector( 147 | boshURL string, 148 | creds boshconfig.Creds, 149 | caCert string, 150 | logger boshlog.Logger, 151 | ) (boshdir.Director, error) { 152 | dirConfig, err := boshdir.NewConfigFromURL(boshURL) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | dirConfig.CACert = caCert 158 | 159 | directorInfo, err := getDirectorInfo(logger, dirConfig) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | if directorInfo.Auth.Type != "uaa" { 165 | dirConfig.Client = creds.Client 166 | dirConfig.ClientSecret = creds.ClientSecret 167 | } else if creds.IsUAA() { 168 | uaa, err := getUAA(dirConfig, creds, caCert, logger) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | if creds.IsUAAClient() { 174 | dirConfig.TokenFunc = boshuaa.NewClientTokenSession(uaa).TokenFunc 175 | } else { 176 | origToken := uaa.NewStaleAccessToken(creds.RefreshToken) 177 | dirConfig.TokenFunc = boshuaa.NewAccessTokenSession(origToken).TokenFunc 178 | } 179 | } 180 | 181 | director, err := boshdir.NewFactory(logger).New(dirConfig, boshdir.NewNoopTaskReporter(), boshdir.NewNoopFileReporter()) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | return director, nil 187 | } 188 | 189 | func getUAA(dirConfig boshdir.FactoryConfig, creds boshconfig.Creds, caCert string, logger boshlog.Logger) (boshuaa.UAA, error) { 190 | director, err := boshdir.NewFactory(logger).New(dirConfig, boshdir.NewNoopTaskReporter(), boshdir.NewNoopFileReporter()) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | info, err := director.Info() 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | uaaURL := info.Auth.Options["url"] 201 | 202 | uaaURLStr, ok := uaaURL.(string) 203 | if !ok { 204 | return nil, err 205 | } 206 | 207 | uaaConfig, err := boshuaa.NewConfigFromURL(uaaURLStr) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | uaaConfig.CACert = caCert 213 | 214 | uaaConfig.Client = creds.Client 215 | uaaConfig.ClientSecret = creds.ClientSecret 216 | 217 | return boshuaa.NewFactory(logger).New(uaaConfig) 218 | } 219 | 220 | func getDirectorInfo(logger boshlog.Logger, dirConfig boshdir.FactoryConfig) (boshdir.Info, error) { 221 | anonymousDirector, err := boshdir.NewFactory(logger).New(dirConfig, nil, nil) 222 | if err != nil { 223 | return boshdir.Info{}, err 224 | } 225 | 226 | directorInfo, err := anonymousDirector.Info() 227 | if err != nil { 228 | return boshdir.Info{}, err 229 | } 230 | 231 | return directorInfo, nil 232 | } 233 | 234 | func BestAddress(addresses []string) string { 235 | if len(addresses) == 0 { 236 | panic("BestAddress: candidate list is empty") 237 | } 238 | 239 | for _, addr := range addresses { 240 | if ip := net.ParseIP(addr).To4(); ip != nil { 241 | if ip[0] == 10 { 242 | return addr 243 | } 244 | } 245 | } 246 | 247 | return addresses[0] 248 | } 249 | -------------------------------------------------------------------------------- /bosh/bosh_suite_test.go: -------------------------------------------------------------------------------- 1 | package bosh_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestBosh(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "BOSH Suite") 13 | } 14 | -------------------------------------------------------------------------------- /bosh/bosh_test.go: -------------------------------------------------------------------------------- 1 | package bosh_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/pivotal-cf/scantron/bosh" 8 | ) 9 | 10 | var _ = Describe("BestAddress", func() { 11 | It("will pick the private address if it exists (10.*.*.*)", func() { 12 | addresses := []string{ 13 | "203.0.113.1", 14 | "10.0.2.4", 15 | } 16 | 17 | best := bosh.BestAddress(addresses) 18 | Expect(best).To(Equal("10.0.2.4")) 19 | }) 20 | 21 | It("gives up and picks the first one if none of the addresses are private", func() { 22 | addresses := []string{ 23 | "203.0.113.2", 24 | "203.0.113.1", 25 | } 26 | 27 | best := bosh.BestAddress(addresses) 28 | Expect(best).To(Equal("203.0.113.2")) 29 | }) 30 | 31 | It("panics if the input list is empty", func() { 32 | Expect(func() { 33 | bosh.BestAddress([]string{}) 34 | }).To(Panic()) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /bosh/mock_target_deployment.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: bosh/bosh.go 3 | 4 | // Package bosh is a generated GoMock package. 5 | package bosh 6 | 7 | import ( 8 | director "github.com/cloudfoundry/bosh-cli/director" 9 | gomock "github.com/golang/mock/gomock" 10 | remotemachine "github.com/pivotal-cf/scantron/remotemachine" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockTargetDeployment is a mock of TargetDeployment interface 15 | type MockTargetDeployment struct { 16 | ctrl *gomock.Controller 17 | recorder *MockTargetDeploymentMockRecorder 18 | } 19 | 20 | // MockTargetDeploymentMockRecorder is the mock recorder for MockTargetDeployment 21 | type MockTargetDeploymentMockRecorder struct { 22 | mock *MockTargetDeployment 23 | } 24 | 25 | // NewMockTargetDeployment creates a new mock instance 26 | func NewMockTargetDeployment(ctrl *gomock.Controller) *MockTargetDeployment { 27 | mock := &MockTargetDeployment{ctrl: ctrl} 28 | mock.recorder = &MockTargetDeploymentMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockTargetDeployment) EXPECT() *MockTargetDeploymentMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Name mocks base method 38 | func (m *MockTargetDeployment) Name() string { 39 | ret := m.ctrl.Call(m, "Name") 40 | ret0, _ := ret[0].(string) 41 | return ret0 42 | } 43 | 44 | // Name indicates an expected call of Name 45 | func (mr *MockTargetDeploymentMockRecorder) Name() *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockTargetDeployment)(nil).Name)) 47 | } 48 | 49 | // VMs mocks base method 50 | func (m *MockTargetDeployment) VMs() []director.VMInfo { 51 | ret := m.ctrl.Call(m, "VMs") 52 | ret0, _ := ret[0].([]director.VMInfo) 53 | return ret0 54 | } 55 | 56 | // VMs indicates an expected call of VMs 57 | func (mr *MockTargetDeploymentMockRecorder) VMs() *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMs", reflect.TypeOf((*MockTargetDeployment)(nil).VMs)) 59 | } 60 | 61 | // Releases mocks base method 62 | func (m *MockTargetDeployment) Releases() []director.Release { 63 | ret := m.ctrl.Call(m, "Releases") 64 | ret0, _ := ret[0].([]director.Release) 65 | return ret0 66 | } 67 | 68 | // Releases indicates an expected call of Releases 69 | func (mr *MockTargetDeploymentMockRecorder) Releases() *gomock.Call { 70 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Releases", reflect.TypeOf((*MockTargetDeployment)(nil).Releases)) 71 | } 72 | 73 | // Setup mocks base method 74 | func (m *MockTargetDeployment) Setup() error { 75 | ret := m.ctrl.Call(m, "Setup") 76 | ret0, _ := ret[0].(error) 77 | return ret0 78 | } 79 | 80 | // Setup indicates an expected call of Setup 81 | func (mr *MockTargetDeploymentMockRecorder) Setup() *gomock.Call { 82 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setup", reflect.TypeOf((*MockTargetDeployment)(nil).Setup)) 83 | } 84 | 85 | // ConnectTo mocks base method 86 | func (m *MockTargetDeployment) ConnectTo(arg0 director.VMInfo) remotemachine.RemoteMachine { 87 | ret := m.ctrl.Call(m, "ConnectTo", arg0) 88 | ret0, _ := ret[0].(remotemachine.RemoteMachine) 89 | return ret0 90 | } 91 | 92 | // ConnectTo indicates an expected call of ConnectTo 93 | func (mr *MockTargetDeploymentMockRecorder) ConnectTo(arg0 interface{}) *gomock.Call { 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectTo", reflect.TypeOf((*MockTargetDeployment)(nil).ConnectTo), arg0) 95 | } 96 | 97 | // Cleanup mocks base method 98 | func (m *MockTargetDeployment) Cleanup() error { 99 | ret := m.ctrl.Call(m, "Cleanup") 100 | ret0, _ := ret[0].(error) 101 | return ret0 102 | } 103 | 104 | // Cleanup indicates an expected call of Cleanup 105 | func (mr *MockTargetDeploymentMockRecorder) Cleanup() *gomock.Call { 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockTargetDeployment)(nil).Cleanup)) 107 | } 108 | -------------------------------------------------------------------------------- /cmd/proc_scan/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/jessevdk/go-flags" 7 | "github.com/pivotal-cf/scantron/filesystem" 8 | "github.com/pivotal-cf/scantron/ssh" 9 | "github.com/pivotal-cf/scantron/tlsscan" 10 | "log" 11 | "os" 12 | 13 | "github.com/pivotal-cf/scantron" 14 | "github.com/pivotal-cf/scantron/process" 15 | "github.com/pivotal-cf/scantron/scanlog" 16 | ) 17 | 18 | func main() { 19 | var opts struct { 20 | Debug bool `long:"debug" description:"Show debug logs in output"` 21 | Context string `long:"context" description:"Log context"` 22 | FileRegexes scantron.FileMatch `group:"File Content Check"` 23 | } 24 | 25 | _, err := flags.Parse(&opts) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | logger, err := scanlog.NewLogger(opts.Debug) 31 | if err != nil { 32 | log.Fatalln("failed to set up logger:", err) 33 | } 34 | logger = logger.With( 35 | "context", opts.Context, 36 | ) 37 | 38 | processScanner := process.ProcessScanner{ 39 | SysRes: &process.SystemResourceImpl{}, 40 | TlsScan: &tlsscan.TlsScannerImpl{}, 41 | } 42 | 43 | processes, err := processScanner.ScanProcesses(logger) 44 | if err != nil { 45 | fmt.Fprintln(os.Stderr, "error: failed to get process list:", err) 46 | os.Exit(1) 47 | } 48 | 49 | fileWalker, err := filesystem.NewWalker(filesystem.GetFileConfig(), opts.FileRegexes, logger) 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, "error: failed to instantiate filewalker:", err) 52 | os.Exit(1) 53 | } 54 | fs := filesystem.FileScanner{ 55 | Walker: fileWalker, 56 | Metadata: filesystem.GetFileMetadata(), 57 | Logger: logger, 58 | } 59 | files, err := fs.ScanFiles() 60 | if err != nil { 61 | fmt.Fprintln(os.Stderr, "error: failed to scan filesystem:", err) 62 | os.Exit(1) 63 | } 64 | 65 | sshKeys, err := ssh.ScanSSH("localhost:22") 66 | if err != nil { 67 | fmt.Fprintln(os.Stderr, "error: failed to scan ssh keys:", err) 68 | os.Exit(1) 69 | } 70 | 71 | systemInfo := scantron.SystemInfo{ 72 | Processes: processes, 73 | Files: files, 74 | SSHKeys: sshKeys, 75 | } 76 | 77 | json.NewEncoder(os.Stdout).Encode(systemInfo) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/scantron/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/jessevdk/go-flags" 7 | "github.com/pivotal-cf/scantron/commands" 8 | ) 9 | 10 | func main() { 11 | parser := flags.NewParser(&commands.Scantron, flags.Default) 12 | 13 | _, err := parser.Parse() 14 | if err != nil { 15 | if esErr, ok := err.(commands.ExitStatusError); ok { 16 | os.Exit(esErr.ExitStatus()) 17 | } 18 | 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/scantron/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | yaml "gopkg.in/yaml.v2" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/pivotal-cf/scantron/db" 14 | "github.com/pivotal-cf/scantron/manifest" 15 | "github.com/pivotal-cf/scantron/scanner" 16 | ) 17 | 18 | var _ = Describe("Main", func() { 19 | Describe("audit", func() { 20 | var ( 21 | tmpdir string 22 | manifestPath string 23 | databasePath string 24 | 25 | mani manifest.Manifest 26 | hosts scanner.ScanResult 27 | ) 28 | 29 | BeforeEach(func() { 30 | var err error 31 | 32 | tmpdir, err = ioutil.TempDir("", "scantron-main-test") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | manifestPath = filepath.Join(tmpdir, "manifest.yml") 36 | mani = manifest.Manifest{ 37 | Specs: []manifest.Spec{ 38 | { 39 | Prefix: "prefix", 40 | }, 41 | }, 42 | } 43 | 44 | databasePath = filepath.Join(tmpdir, "database.db") 45 | hosts = scanner.ScanResult{ 46 | JobResults: []scanner.JobResult{ 47 | { 48 | Job: "prefix-name-1", 49 | }, 50 | { 51 | Job: "prefix-name-2", 52 | }, 53 | }, 54 | } 55 | }) 56 | 57 | JustBeforeEach(func() { 58 | manifestBytes, err := yaml.Marshal(mani) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | err = ioutil.WriteFile(manifestPath, manifestBytes, 0600) 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | database, err := db.CreateDatabase(databasePath) 65 | Expect(err).NotTo(HaveOccurred()) 66 | defer database.Close() 67 | 68 | err = database.SaveReport("cf1", hosts) 69 | Expect(err).NotTo(HaveOccurred()) 70 | }) 71 | 72 | AfterEach(func() { 73 | os.RemoveAll(tmpdir) 74 | }) 75 | 76 | Context("when the manifest does not exist", func() { 77 | JustBeforeEach(func() { 78 | err := os.RemoveAll(manifestPath) 79 | Expect(err).NotTo(HaveOccurred()) 80 | }) 81 | 82 | It("exits 1", func() { 83 | session := runCommand("audit", "--database", databasePath, "--manifest", manifestPath) 84 | 85 | Eventually(session).Should(gexec.Exit(1)) 86 | }) 87 | }) 88 | 89 | Context("when the manifest is malformed", func() { 90 | JustBeforeEach(func() { 91 | err := ioutil.WriteFile(manifestPath, []byte("not-yaml"), 0600) 92 | Expect(err).NotTo(HaveOccurred()) 93 | }) 94 | 95 | It("exits 1", func() { 96 | session := runCommand("audit", "--database", databasePath, "--manifest", manifestPath) 97 | 98 | Eventually(session).Should(gexec.Exit(1)) 99 | }) 100 | }) 101 | 102 | Context("when the audit fails", func() { 103 | BeforeEach(func() { 104 | hosts = scanner.ScanResult{ 105 | JobResults: []scanner.JobResult{ 106 | { 107 | Job: "not-the-right-prefix-name", 108 | }, 109 | }, 110 | } 111 | }) 112 | 113 | It("exits 3", func() { 114 | session := runCommand("audit", "--database", databasePath, "--manifest", manifestPath) 115 | 116 | Eventually(session).Should(gexec.Exit(3)) 117 | }) 118 | }) 119 | 120 | It("shows ok for each host", func() { 121 | session := runCommand("audit", "--database", databasePath, "--manifest", manifestPath) 122 | 123 | Eventually(session).Should(gexec.Exit(0)) 124 | 125 | output := session.Out.Contents() 126 | 127 | Expect(output).To(ContainSubstring("ok prefix-name-1")) 128 | Expect(output).To(ContainSubstring("ok prefix-name-2")) 129 | }) 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /cmd/scantron/scantron_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/onsi/gomega/gexec" 8 | 9 | "os/exec" 10 | "testing" 11 | ) 12 | 13 | func TestCredAlertCli(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "CLI Suite") 16 | } 17 | 18 | var scantronPath string 19 | 20 | var _ = SynchronizedBeforeSuite(func() []byte { 21 | var err error 22 | scantronPath, err = gexec.Build("github.com/pivotal-cf/scantron/cmd/scantron") 23 | Expect(err).NotTo(HaveOccurred()) 24 | 25 | return []byte(scantronPath) 26 | }, func(data []byte) { 27 | scantronPath = string(data) 28 | }) 29 | 30 | var _ = SynchronizedAfterSuite(func() {}, func() { 31 | gexec.CleanupBuildArtifacts() 32 | }) 33 | 34 | func runCommand(args ...string) *gexec.Session { 35 | cmd := exec.Command(scantronPath, args...) 36 | 37 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 38 | Expect(err).NotTo(HaveOccurred()) 39 | <-session.Exited 40 | 41 | return session 42 | } 43 | -------------------------------------------------------------------------------- /commands/audit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/pivotal-cf/scantron/audit" 9 | "github.com/pivotal-cf/scantron/db" 10 | "github.com/pivotal-cf/scantron/manifest" 11 | ) 12 | 13 | var AuditError = ExitStatusError{message: "audit mismatch", exitStatus: 3} 14 | 15 | type AuditCommand struct { 16 | Database string `long:"database" description:"path to report database" value-name:"PATH" default:"./database.db"` 17 | Manifest string `long:"manifest" description:"path to manifest" required:"true" value-name:"PATH"` 18 | } 19 | 20 | func (command *AuditCommand) Execute(args []string) error { 21 | man, err := manifest.Parse(command.Manifest) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | db, err := db.OpenDatabase(command.Database) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | report, err := audit.Audit(db.DB(), man) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return ShowReport(os.Stdout, report) 37 | } 38 | 39 | func ShowReport(output io.Writer, report audit.AuditResult) error { 40 | if len(report.ExtraHosts) > 0 { 41 | fmt.Fprintln(output, "found hosts in report that were not matched in the manifest:") 42 | 43 | for _, host := range report.ExtraHosts { 44 | fmt.Fprintln(output, host) 45 | } 46 | 47 | fmt.Fprintln(output) 48 | } 49 | 50 | if len(report.MissingHostType) > 0 { 51 | fmt.Fprintln(output, "found host types in the manifest that were not found in the scan:") 52 | 53 | for _, host := range report.MissingHostType { 54 | fmt.Fprintln(output, host) 55 | } 56 | 57 | fmt.Fprintln(output) 58 | } 59 | 60 | for host, hostReport := range report.Hosts { 61 | if hostReport.OK() { 62 | fmt.Fprintf(output, "ok %s\n", host) 63 | continue 64 | } else { 65 | fmt.Fprintf(output, "err %s\n", host) 66 | } 67 | 68 | if len(hostReport.UnexpectedPorts) > 0 { 69 | fmt.Fprintln(output, " found unexpected ports:") 70 | 71 | for _, port := range hostReport.UnexpectedPorts { 72 | fmt.Fprintf(output, " %d\n", port) 73 | } 74 | 75 | fmt.Fprintln(output) 76 | } 77 | 78 | if len(hostReport.MissingPorts) > 0 { 79 | fmt.Fprintln(output, " did not find ports that were mentioned in manifest:") 80 | 81 | for _, port := range hostReport.MissingPorts { 82 | fmt.Fprintf(output, " %d\n", port) 83 | } 84 | 85 | fmt.Fprintln(output) 86 | } 87 | 88 | if len(hostReport.MissingProcesses) > 0 { 89 | fmt.Fprintln(output, " did not find processes that were mentioned in manifest:") 90 | 91 | for _, process := range hostReport.MissingProcesses { 92 | fmt.Fprintf(output, " %s\n", process) 93 | } 94 | 95 | fmt.Fprintln(output) 96 | } 97 | 98 | if len(hostReport.MismatchedProcesses) > 0 { 99 | fmt.Fprintln(output, " processes were found but there mismatches between their attributes:") 100 | 101 | for _, process := range hostReport.MismatchedProcesses { 102 | fmt.Fprintf(output, " %s: %s should be '%s' but was actually '%s'\n", process.Command, 103 | process.Field, process.Expected, process.Actual) 104 | } 105 | 106 | fmt.Fprintln(output) 107 | } 108 | } 109 | 110 | if report.OK() { 111 | return nil 112 | } else { 113 | return AuditError 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /commands/audit_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "github.com/pivotal-cf/scantron/audit" 7 | "github.com/pivotal-cf/scantron/commands" 8 | ) 9 | 10 | var _ = Describe("Audit", func() { 11 | Describe("Show Report", func() { 12 | var ( 13 | err error 14 | auditReport audit.AuditResult 15 | ) 16 | 17 | JustBeforeEach(func() { 18 | err = commands.ShowReport(GinkgoWriter, auditReport) 19 | }) 20 | 21 | Context("When report does not have mismatch", func() { 22 | BeforeEach(func() { 23 | auditReport = audit.AuditResult{} 24 | }) 25 | 26 | It("does not error", func() { 27 | Expect(err).NotTo(HaveOccurred()) 28 | }) 29 | }) 30 | 31 | Context("When report has mismatch", func() { 32 | BeforeEach(func() { 33 | auditReport = audit.AuditResult{ 34 | ExtraHosts: []string{ 35 | "host1", 36 | "host2", 37 | }, 38 | } 39 | }) 40 | 41 | It("returns error", func() { 42 | Expect(err).To(HaveOccurred()) 43 | }) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /commands/bosh_scan.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | boshconfig "github.com/cloudfoundry/bosh-cli/cmd/config" 6 | "github.com/pivotal-cf/scantron" 7 | "github.com/pivotal-cf/scantron/bosh" 8 | "github.com/pivotal-cf/scantron/db" 9 | "github.com/pivotal-cf/scantron/scanlog" 10 | "github.com/pivotal-cf/scantron/scanner" 11 | "log" 12 | "sync" 13 | ) 14 | 15 | type BoshScanCommand struct { 16 | Director struct { 17 | URL string `long:"director-url" description:"BOSH Director URL" value-name:"URL" required:"true"` 18 | Deployments []string `long:"bosh-deployment" description:"BOSH Deployment" value-name:"DEPLOYMENT_NAME" required:"true"` 19 | CACert string `long:"ca-cert" description:"Director CA certificate path" value-name:"CA_CERT"` 20 | Client string `long:"client" description:"Username or UAA client" value-name:"CLIENT"` 21 | ClientSecret string `long:"client-secret" description:"Password or UAA client secret" value-name:"CLIENT_SECRET"` 22 | } `group:"Director & Deployment"` 23 | 24 | FileRegexes scantron.FileMatch `group:"File Content Check"` 25 | Database string `long:"database" description:"location of database where scan output will be stored" value-name:"PATH" default:"./database.db"` 26 | Serial bool `long:"serial" description:"run scans serially"` 27 | } 28 | 29 | type ScanResult struct { 30 | name string 31 | value scanner.ScanResult 32 | } 33 | 34 | func scan(dep bosh.TargetDeployment, command *BoshScanCommand, logger scanlog.Logger, results chan<- ScanResult) { 35 | result, err := scanner.Bosh(dep).Scan(&command.FileRegexes, logger) 36 | if err != nil { 37 | log.Fatalf("failed to scan: %s", err.Error()) 38 | } 39 | 40 | results <- ScanResult{dep.Name(), result} 41 | } 42 | 43 | func (command *BoshScanCommand) Execute(args []string) error { 44 | scantron.SetDebug(Scantron.Debug) 45 | 46 | logger, err := scanlog.NewLogger(Scantron.Debug) 47 | if err != nil { 48 | log.Fatalln("failed to set up logger:", err) 49 | } 50 | 51 | logger.Debugf("Requested deployments to scan: %v", command.Director.Deployments) 52 | 53 | deployments, err := bosh.GetDeployments( 54 | boshconfig.Creds{ 55 | Client: command.Director.Client, 56 | ClientSecret: command.Director.ClientSecret, 57 | }, 58 | command.Director.CACert, 59 | command.Director.Deployments, 60 | command.Director.URL, 61 | logger, 62 | ) 63 | 64 | if err != nil { 65 | log.Fatalf("failed to set up director: %s", err.Error()) 66 | } 67 | 68 | db, err := db.CreateDatabase(command.Database) 69 | if err != nil { 70 | log.Fatalf("failed to create database: %s", err.Error()) 71 | } 72 | 73 | noOfDeployments := len(deployments) 74 | wg := &sync.WaitGroup{} 75 | wg.Add(noOfDeployments) 76 | 77 | results := make(chan ScanResult, noOfDeployments) 78 | quit := make(chan bool) 79 | 80 | // Inform that it is time to quit after enough writes 81 | go func() { 82 | wg.Wait() 83 | db.Close() 84 | quit <- true 85 | }() 86 | 87 | // Scan all deployments 88 | for _, d := range deployments { 89 | if command.Serial { 90 | scan(d, command, logger, results) 91 | } else { 92 | go scan(d, command, logger, results) 93 | } 94 | } 95 | 96 | for { 97 | select { 98 | case result := <-results: 99 | err = db.SaveReport(result.name, result.value) 100 | 101 | if err != nil { 102 | log.Fatalf("failed to save to database: %s", err.Error()) 103 | } 104 | wg.Done() 105 | case <-quit: 106 | close(results) 107 | close(quit) 108 | 109 | fmt.Println("Report is saved in SQLite3 database:", command.Database) 110 | 111 | return nil 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /commands/commands_suite_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/onsi/gomega/gexec" 11 | ) 12 | 13 | func TestCmd(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Commands Suite") 16 | } 17 | 18 | var commandPath string 19 | 20 | var _ = SynchronizedBeforeSuite(func() []byte { 21 | var err error 22 | commandPath, err = gexec.Build("github.com/pivotal-cf/scantron/cmd/scantron") 23 | Expect(err).NotTo(HaveOccurred()) 24 | 25 | return []byte(commandPath) 26 | }, func(data []byte) { 27 | commandPath = string(data) 28 | }) 29 | 30 | var _ = SynchronizedAfterSuite(func() {}, func() { 31 | gexec.CleanupBuildArtifacts() 32 | }) 33 | 34 | func runCommand(args ...string) *gexec.Session { 35 | cmd := exec.Command(commandPath, args...) 36 | 37 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 38 | Expect(err).NotTo(HaveOccurred()) 39 | <-session.Exited 40 | 41 | return session 42 | } 43 | -------------------------------------------------------------------------------- /commands/direct_scan.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | 8 | "golang.org/x/crypto/ssh" 9 | 10 | "github.com/pivotal-cf/scantron" 11 | "github.com/pivotal-cf/scantron/db" 12 | "github.com/pivotal-cf/scantron/remotemachine" 13 | "github.com/pivotal-cf/scantron/scanlog" 14 | "github.com/pivotal-cf/scantron/scanner" 15 | ) 16 | 17 | type DirectScanCommand struct { 18 | Address string `long:"address" description:"Address of machine to scan" value-name:"ADDRESS" required:"true"` 19 | Username string `long:"username" description:"Username of machine to scan" value-name:"USERNAME" required:"true"` 20 | Password string `long:"password" description:"Password of machine to scan" value-name:"PASSWORD" required:"true"` 21 | PrivateKey string `long:"private-key" description:"Private key of machine to scan" value-name:"PATH"` 22 | Database string `long:"database" description:"location of database where scan output will be stored" value-name:"PATH" default:"./database.db"` 23 | OSName string `long:"os-name" description:"Name of stemcell OS of machine to scan" value-name:"STRING" required:"true"` 24 | 25 | FileRegexes scantron.FileMatch `group:"File Content Check"` 26 | } 27 | 28 | func (command *DirectScanCommand) Execute(args []string) error { 29 | scantron.SetDebug(Scantron.Debug) 30 | logger, err := scanlog.NewLogger(Scantron.Debug) 31 | if err != nil { 32 | log.Fatalln("failed to set up logger:", err) 33 | } 34 | 35 | var privateKey ssh.Signer 36 | 37 | if command.PrivateKey != "" { 38 | key, err := ioutil.ReadFile(command.PrivateKey) 39 | if err != nil { 40 | log.Fatalf("unable to read private key: %s", err.Error()) 41 | } 42 | 43 | privateKey, err = ssh.ParsePrivateKey(key) 44 | if err != nil { 45 | log.Fatalf("unable to parse private key: %s", err.Error()) 46 | } 47 | } 48 | 49 | machine := scantron.Machine{ 50 | Address: command.Address, 51 | Username: command.Username, 52 | Password: command.Password, 53 | Key: privateKey, 54 | OSName: command.OSName, 55 | } 56 | 57 | remoteMachine := remotemachine.NewRemoteMachine(machine) 58 | defer remoteMachine.Close() 59 | 60 | db, err := db.CreateDatabase(command.Database) 61 | if err != nil { 62 | log.Fatalf("failed to create database: %s", err.Error()) 63 | } 64 | 65 | results, err := scanner.Direct(remoteMachine).Scan(&command.FileRegexes, logger) 66 | if err != nil { 67 | log.Fatalf("failed to scan: %s", err.Error()) 68 | } 69 | 70 | err = db.SaveReport("direct-scan", results) 71 | if err != nil { 72 | log.Fatalf("failed to save to database: %s", err.Error()) 73 | } 74 | 75 | db.Close() 76 | 77 | fmt.Println("Report saved in SQLite3 database:", command.Database) 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /commands/generate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pivotal-cf/scantron/audit" 7 | "github.com/pivotal-cf/scantron/db" 8 | ) 9 | 10 | type GenerateManifestCommand struct { 11 | Database string `long:"database" description:"path to report database" value-name:"PATH" default:"./database.db"` 12 | } 13 | 14 | func (command *GenerateManifestCommand) Execute(args []string) error { 15 | db, err := db.OpenDatabase(command.Database) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return audit.GenerateManifest(os.Stdout, db.DB()) 21 | } 22 | -------------------------------------------------------------------------------- /commands/report.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "encoding/csv" 9 | 10 | "github.com/pivotal-cf/scantron/db" 11 | "github.com/pivotal-cf/scantron/report" 12 | ) 13 | 14 | type ReportCommand struct { 15 | Database string `long:"database" description:"path to report database" required:"true" value-name:"DB PATH"` 16 | CsvExportPath string `long:"csv" description:"path to csv output" value-name:"CSV PATH"` 17 | } 18 | 19 | func (command *ReportCommand) Execute(args []string) error { 20 | database, err := db.OpenDatabase(command.Database) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | rootReport, err := report.BuildRootProcessesReport(database) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | tlsReport, err := report.BuildTLSViolationsReport(database) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | filesReport, err := report.BuildWorldReadableFilesReport(database) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | sshKeysReport, err := report.BuildInsecureSshKeyReport(database) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if command.CsvExportPath != "" { 46 | _, err = os.Stat(command.CsvExportPath) 47 | 48 | if os.IsNotExist(err) { 49 | err = os.Mkdir(command.CsvExportPath, 0700) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | err = exportCsv(command.CsvExportPath, rootReport, "root_process_report.csv") 56 | if err != nil { 57 | return err 58 | } 59 | 60 | err = exportCsv(command.CsvExportPath, tlsReport, "tls_violation_report.csv") 61 | if err != nil { 62 | return err 63 | } 64 | 65 | err = exportCsv(command.CsvExportPath, filesReport, "world_readable_files_report.csv") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = exportCsv(command.CsvExportPath, sshKeysReport, "insecure_sshkey_report.csv") 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | rootReport.WriteTo(os.Stdout) 77 | tlsReport.WriteTo(os.Stdout) 78 | filesReport.WriteTo(os.Stdout) 79 | sshKeysReport.WriteTo(os.Stdout) 80 | 81 | if !rootReport.IsEmpty() || 82 | !tlsReport.IsEmpty() || 83 | !filesReport.IsEmpty() || 84 | !sshKeysReport.IsEmpty() { 85 | return errors.New("Violations were found!") 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func exportCsv(absDir string, report report.Report, reportFileName string) error { 92 | f, err := os.Create(filepath.Join(absDir, reportFileName)) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | err = f.Chmod(0600) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | csvWriter := csv.NewWriter(f) 103 | csvWriter.Write(report.Header) 104 | csvWriter.WriteAll(report.Rows) 105 | f.Sync() 106 | csvWriter.Flush() 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /commands/report_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | . "github.com/onsi/gomega/gbytes" 10 | . "github.com/onsi/gomega/gexec" 11 | 12 | "path/filepath" 13 | 14 | "github.com/pivotal-cf/scantron" 15 | "github.com/pivotal-cf/scantron/db" 16 | "github.com/pivotal-cf/scantron/scanner" 17 | ) 18 | 19 | var _ = Describe("Report", func() { 20 | var ( 21 | databasePath, tmpdir string 22 | database *db.Database 23 | ) 24 | 25 | BeforeEach(func() { 26 | var err error 27 | tmpdir, err = ioutil.TempDir("", "report-test") 28 | Expect(err).NotTo(HaveOccurred()) 29 | databasePath = filepath.Join(tmpdir, "db.db") 30 | 31 | database, err = db.CreateDatabase(databasePath) 32 | Expect(err).NotTo(HaveOccurred()) 33 | }) 34 | 35 | AfterEach(func() { 36 | err := database.Close() 37 | Expect(err).NotTo(HaveOccurred()) 38 | 39 | err = os.RemoveAll(tmpdir) 40 | Expect(err).NotTo(HaveOccurred()) 41 | }) 42 | 43 | Context("when there are violations", func() { 44 | BeforeEach(func() { 45 | hosts := scanner.ScanResult{ 46 | JobResults: []scanner.JobResult{ 47 | { 48 | Job: "host1", 49 | Files: []scantron.File{ 50 | { 51 | Path: "/var/vcap/data/jobs/my.cnf", 52 | Permissions: 0644, 53 | }, 54 | }, 55 | SSHKeys: []scantron.SSHKey{ 56 | { 57 | Type: "ssh-rsa", 58 | Key: "key-1", 59 | }, 60 | }, 61 | Services: []scantron.Process{ 62 | { 63 | CommandName: "command1", 64 | User: "root", 65 | Ports: []scantron.Port{ 66 | { 67 | State: "LISTEN", 68 | Address: "10.0.5.21", 69 | Number: 7890, 70 | TLSInformation: &scantron.TLSInformation{ 71 | Certificate: &scantron.Certificate{}, 72 | CipherInformation: scantron.CipherInformation{ 73 | "VersionSSL30": []string{"bad cipher"}, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | { 82 | Job: "host2", 83 | SSHKeys: []scantron.SSHKey{ 84 | { 85 | Type: "ssh-rsa", 86 | Key: "key-1", 87 | }, 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | err := database.SaveReport("cf1", hosts) 94 | Expect(err).NotTo(HaveOccurred()) 95 | }) 96 | 97 | It("shows externally-accessible processes running as root", func() { 98 | session := runCommand("report", "--database", databasePath) 99 | 100 | Expect(session).To(Exit(1)) 101 | 102 | Expect(session.Out).To(Say("Externally-accessible processes running as root:")) 103 | Expect(session.Out).To(Say(`\|\s+IDENTITY\s+\|\s+PORT\s+\|\s+PROCESS NAME\s+\|`)) 104 | 105 | Expect(session.Out).To(Say(`\|\s+host1\s+\|\s+7890\s+\|\s+command1\s+\|`)) 106 | }) 107 | 108 | It("shows processes using non-approved protocols or cipher suites", func() { 109 | session := runCommand("report", "--database", databasePath) 110 | 111 | Expect(session).To(Exit(1)) 112 | 113 | Expect(session.Out).To(Say("Processes using non-approved SSL/TLS settings:")) 114 | Expect(session.Out).To(Say(`\|\s+IDENTITY\s+\|\s+PORT\s+\|\s+PROCESS NAME\s+\|\s+NON-APPROVED PROTOCOL\(S\)\s+\|\s+NON-APPROVED CIPHER\(S\)\s+\|`)) 115 | Expect(session.Out).To(Say(`\|\s+host1\s+\|\s+7890\s+\|\s+command1\s+\|\s+VersionSSL30\s+\|\s+bad cipher\s+\|`)) 116 | Expect(session.Out).To(Say("If this is not an internal endpoint then please check with your PM and the security team before applying this change. This change is not backwards compatible.")) 117 | }) 118 | 119 | It("shows world-readable files", func() { 120 | session := runCommand("report", "--database", databasePath) 121 | 122 | Expect(session).To(Exit(1)) 123 | 124 | Expect(session.Out).To(Say("World-readable files:")) 125 | Expect(session.Out).To(Say(`\|\s+IDENTITY\s+\|\s+PATH\s+\|`)) 126 | 127 | Expect(session.Out).To(Say(`\|\s+host1\s+\|\s+/var/vcap/data/jobs/my.cnf\s+\|`)) 128 | }) 129 | 130 | It("shows hosts with duplicate ssh keys", func() { 131 | session := runCommand("report", "--database", databasePath) 132 | 133 | Expect(session).To(Exit(1)) 134 | Expect(session.Out).To(Say("Duplicate SSH keys:")) 135 | Expect(session.Out).To(Say(`\|\s+IDENTITY\s+\|`)) 136 | 137 | Expect(session.Out).To(Say(`\|\s+host1\s+\|`)) 138 | Expect(session.Out).To(Say(`\|\s+host2\s+\|`)) 139 | }) 140 | 141 | Context("and the csv flag is provided", func() { 142 | var ( 143 | path string 144 | err error 145 | ) 146 | 147 | BeforeEach(func() { 148 | var err error 149 | 150 | path, err = ioutil.TempDir("", "csv-export") 151 | Expect(err).NotTo(HaveOccurred()) 152 | 153 | err = os.RemoveAll(path) 154 | Expect(err).NotTo(HaveOccurred()) 155 | }) 156 | 157 | AfterEach(func() { 158 | err = os.RemoveAll(path) 159 | Expect(err).NotTo(HaveOccurred()) 160 | }) 161 | 162 | It("outputs root process results to csv", func() { 163 | session := runCommand("report", "--database", databasePath, "--csv", path) 164 | Expect(session).To(Exit(1)) 165 | 166 | result, err := ioutil.ReadFile(filepath.Join(path, "root_process_report.csv")) 167 | Expect(err).NotTo(HaveOccurred()) 168 | 169 | Expect(string(result)).To(ContainSubstring("Identity,Port,Process Name")) 170 | Expect(string(result)).To(ContainSubstring("host1,7890,command1")) 171 | 172 | result, err = ioutil.ReadFile(filepath.Join(path, "tls_violation_report.csv")) 173 | Expect(err).NotTo(HaveOccurred()) 174 | 175 | Expect(string(result)).To(ContainSubstring("Identity,Port,Process Name,Non-approved Protocol(s),Non-approved Cipher(s)")) 176 | Expect(string(result)).To(ContainSubstring("host1,7890,command1,VersionSSL30,bad cipher")) 177 | 178 | result, err = ioutil.ReadFile(filepath.Join(path, "world_readable_files_report.csv")) 179 | Expect(err).NotTo(HaveOccurred()) 180 | 181 | Expect(string(result)).To(ContainSubstring("Identity,Path")) 182 | Expect(string(result)).To(ContainSubstring("host1,/var/vcap/data/jobs/my.cnf")) 183 | 184 | result, err = ioutil.ReadFile(filepath.Join(path, "insecure_sshkey_report.csv")) 185 | Expect(err).NotTo(HaveOccurred()) 186 | 187 | Expect(string(result)).To(ContainSubstring("Identity")) 188 | Expect(string(result)).To(ContainSubstring("host1")) 189 | }) 190 | }) 191 | }) 192 | 193 | Context("when there are no violations", func() { 194 | BeforeEach(func() { 195 | hosts := scanner.ScanResult{ 196 | JobResults: []scanner.JobResult{ 197 | { 198 | Job: "host1", 199 | Services: []scantron.Process{ 200 | { 201 | CommandName: "command1", 202 | User: "vcap", 203 | Ports: []scantron.Port{ 204 | { 205 | State: "LISTEN", 206 | Address: "10.0.5.21", 207 | Number: 7890, 208 | TLSInformation: &scantron.TLSInformation{ 209 | Certificate: &scantron.Certificate{}, 210 | CipherInformation: scantron.CipherInformation{ 211 | "VersionTLS12": []string{"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"}, 212 | }, 213 | }, 214 | }, 215 | }, 216 | }, 217 | }, 218 | }, 219 | }, 220 | } 221 | 222 | err := database.SaveReport("cf1", hosts) 223 | Expect(err).NotTo(HaveOccurred()) 224 | }) 225 | 226 | It("exits without error", func() { 227 | session := runCommand("report", "--database", databasePath) 228 | 229 | Expect(session).To(Exit(0)) 230 | 231 | Expect(session.Out).To(Say("Externally-accessible processes running as root:")) 232 | Expect(session.Out).To(Say("Processes using non-approved SSL/TLS settings:")) 233 | }) 234 | }) 235 | }) 236 | -------------------------------------------------------------------------------- /commands/scantron.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | type ScantronCommand struct { 4 | Debug bool `long:"debug" description:"Show debug logs in output"` 5 | 6 | BoshScan BoshScanCommand `command:"bosh-scan" description:"Scan all of the machines in a BOSH deployment"` 7 | DirectScan DirectScanCommand `command:"direct-scan" description:"Scan a single machine"` 8 | Audit AuditCommand `command:"audit" description:"Audit a scan report for unexpected hosts, processes, and ports"` 9 | GenerateManifest GenerateManifestCommand `command:"generate-manifest" description:"Generate a audit manifest from the last report"` 10 | Report ReportCommand `command:"report" description:"Generate a human readable report from the given database"` 11 | } 12 | 13 | var Scantron ScantronCommand 14 | 15 | type ExitStatusError struct { 16 | message string 17 | exitStatus int 18 | } 19 | 20 | func (e ExitStatusError) ExitStatus() int { 21 | return e.exitStatus 22 | } 23 | 24 | func (e ExitStatusError) Error() string { 25 | return e.message 26 | } 27 | -------------------------------------------------------------------------------- /db/db_suite_test.go: -------------------------------------------------------------------------------- 1 | package db_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestDb(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "DB Suite") 13 | } 14 | -------------------------------------------------------------------------------- /db/schema.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // Update the schema version when the DDL changes 4 | const SchemaVersion = 8 5 | 6 | const createDDL = ` 7 | CREATE TABLE deployments ( 8 | id integer PRIMARY KEY AUTOINCREMENT, 9 | name text 10 | ); 11 | 12 | CREATE TABLE hosts ( 13 | id integer PRIMARY KEY AUTOINCREMENT, 14 | deployment_id integer, 15 | name text, 16 | ip text, 17 | UNIQUE(ip, name), 18 | FOREIGN KEY(deployment_id) REFERENCES deployments(id) 19 | ); 20 | 21 | CREATE TABLE processes ( 22 | id integer PRIMARY KEY AUTOINCREMENT, 23 | host_id integer, 24 | name text, 25 | pid integer, 26 | cmdline text, 27 | user text, 28 | FOREIGN KEY(host_id) REFERENCES hosts(id) 29 | ); 30 | 31 | CREATE TABLE ports ( 32 | id integer PRIMARY KEY AUTOINCREMENT, 33 | process_id integer, 34 | protocol string, 35 | address string, 36 | number integer, 37 | foreignAddress string, 38 | foreignNumber integer, 39 | state string, 40 | FOREIGN KEY(process_id) REFERENCES processes(id) 41 | ); 42 | 43 | CREATE TABLE tls_certificates ( 44 | id integer PRIMARY KEY AUTOINCREMENT, 45 | port_id integer, 46 | cert_expiration datetime, 47 | cert_bits integer, 48 | cert_country string, 49 | cert_province string, 50 | cert_locality string, 51 | cert_organization string, 52 | cert_common_name string, 53 | mutual bool, 54 | FOREIGN KEY(port_id) REFERENCES ports(id) 55 | ); 56 | 57 | CREATE TABLE tls_scan_errors ( 58 | id integer PRIMARY KEY AUTOINCREMENT, 59 | port_id integer, 60 | cert_scan_error string, 61 | FOREIGN KEY(port_id) REFERENCES ports(id) 62 | ); 63 | 64 | CREATE TABLE tls_suites ( 65 | id integer PRIMARY KEY AUTOINCREMENT, 66 | suite string NOT NULL 67 | ); 68 | 69 | CREATE TABLE tls_ciphers ( 70 | id integer PRIMARY KEY AUTOINCREMENT, 71 | cipher string NOT NULL 72 | ); 73 | 74 | CREATE TABLE certificate_to_ciphersuite ( 75 | certificate_id integer NOT NULL, 76 | suite_id integer NOT NULL, 77 | cipher_id integer NOT NULL, 78 | FOREIGN KEY(certificate_id) REFERENCES tls_certificates(id), 79 | FOREIGN KEY(suite_id) REFERENCES tls_suites(id), 80 | FOREIGN KEY(cipher_id) REFERENCES tls_ciphers(id) 81 | ); 82 | 83 | CREATE TABLE env_vars ( 84 | id integer PRIMARY KEY AUTOINCREMENT, 85 | process_id integer, 86 | var text, 87 | FOREIGN KEY(process_id) REFERENCES processes(id) 88 | ); 89 | 90 | CREATE TABLE files ( 91 | id integer PRIMARY KEY AUTOINCREMENT, 92 | host_id integer, 93 | path text, 94 | permissions integer, 95 | user text, 96 | file_group text, 97 | size integer, 98 | modified datetime, 99 | FOREIGN KEY(host_id) REFERENCES hosts(id) 100 | ); 101 | 102 | CREATE TABLE ssh_keys ( 103 | id integer PRIMARY KEY AUTOINCREMENT, 104 | host_id integer, 105 | type string, 106 | key string, 107 | FOREIGN KEY(host_id) REFERENCES hosts(id) 108 | ); 109 | 110 | CREATE TABLE version ( 111 | version integer 112 | ); 113 | 114 | CREATE TABLE releases ( 115 | id integer PRIMARY KEY AUTOINCREMENT, 116 | deployment_id integer, 117 | name string, 118 | version string, 119 | FOREIGN KEY(deployment_id) REFERENCES deployments(id) 120 | ); 121 | 122 | CREATE TABLE regexes ( 123 | id integer PRIMARY KEY AUTOINCREMENT, 124 | regex string NOT NULL 125 | ); 126 | 127 | CREATE TABLE file_to_regex ( 128 | file_id integer NOT NULL, 129 | path_regex_id integer, 130 | content_regex_id integer NOT NULL, 131 | FOREIGN KEY(file_id) REFERENCES files(id), 132 | FOREIGN KEY(path_regex_id) REFERENCES regexes(id), 133 | FOREIGN KEY(content_regex_id) REFERENCES regexes(id) 134 | ); 135 | 136 | INSERT INTO version(version) VALUES(?); 137 | ` 138 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie-slim 2 | 3 | RUN apt-get update && apt-get install -y sqlite3 openssh-client 4 | -------------------------------------------------------------------------------- /docker/scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | chmod +x ./scantron-binary/scantron 6 | 7 | CA_CERT="ca.crt" 8 | 9 | cat << EOF > "$CA_CERT" 10 | $BOSH_CA_CERT 11 | EOF 12 | 13 | ./scantron-binary/scantron bosh-scan \ 14 | --database scantron-reports/reports.db \ 15 | --director-url "$BOSH_ADDRESS" \ 16 | --bosh-deployment "$BOSH_DEPLOYMENT" \ 17 | --client "$BOSH_CLIENT_ID" \ 18 | --client-secret "$BOSH_CLIENT_SECRET" \ 19 | --ca-cert "$CA_CERT" 20 | 21 | ./scantron-binary/scantron report \ 22 | --database scantron-reports/reports.db || true 23 | -------------------------------------------------------------------------------- /docker/scan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: pcfsecurity/scantron-scan 8 | tag: latest 9 | 10 | inputs: 11 | - name: scantron 12 | - name: scantron-binary 13 | 14 | outputs: 15 | - name: scantron-reports 16 | 17 | run: 18 | path: docker/scan.sh 19 | 20 | params: 21 | BOSH_ADDRESS: 22 | BOSH_CLIENT_ID: 23 | BOSH_CLIENT_SECRET: 24 | BOSH_CA_CERT: 25 | BOSH_DEPLOYMENT: 26 | -------------------------------------------------------------------------------- /example_queries/hosts_on_port.sql: -------------------------------------------------------------------------------- 1 | SELECT hosts.NAME 2 | FROM ports 3 | JOIN processes 4 | ON ports.process_id = processes.id 5 | JOIN hosts 6 | ON processes.host_id = hosts.id 7 | WHERE ports.number = 6061 8 | AND upper(ports.state) = "LISTEN" -------------------------------------------------------------------------------- /example_queries/no_tls.sql: -------------------------------------------------------------------------------- 1 | SELECT substr(h.name, 1, instr(h.name, '/') - 1) AS host, pr.name AS process, po.number AS port 2 | FROM hosts h 3 | JOIN processes pr ON pr.host_id = h.id 4 | JOIN ports po ON po.process_id = pr.id 5 | WHERE po.state = "LISTEN" -- just listening ports 6 | AND po.address != "127.0.0.1" -- ignore processes just listening on localhost 7 | AND po.address NOT LIKE "169.254%" -- ignore processes listening on link-local addresses 8 | AND po.protocol = "tcp" -- only consider tcp connections (we don't do TLS over UDP) 9 | AND po.id NOT IN (SELECT port_id FROM tls_informations) -- find ports which don't have associated TLS information 10 | AND NOT (pr.name = "sshd" AND po.number = 22) -- ignore SSH 11 | AND NOT (pr.name = "rpcbind" AND po.number = 111) -- ignore NFS 12 | AND NOT (pr.name = "ssh-proxy" AND po.number = 2222) -- ignore SSH 13 | ORDER BY h.name, pr.name 14 | -------------------------------------------------------------------------------- /example_queries/root_processes.sql: -------------------------------------------------------------------------------- 1 | .width 20 80 2 | .mode csv 3 | 4 | SELECT DISTINCT substr(h.name, 1, instr(h.name, '/') - 1) AS host, pr.cmdline AS cmd 5 | FROM hosts h 6 | JOIN processes pr ON pr.host_id = h.id 7 | WHERE pr.user = "root" 8 | -- kernel services 9 | AND pr.cmdline != "" 10 | -- iaas services (gcp) 11 | AND pr.cmdline NOT LIKE "%google_network_daemon%" 12 | AND pr.cmdline NOT LIKE "%google_accounts_daemon%" 13 | AND pr.cmdline NOT LIKE "%google_clock_skew_daemon%" 14 | -- iaas services (vsphere) 15 | AND pr.cmdline NOT LIKE "%vmtoolsd%" 16 | AND pr.cmdline NOT LIKE "%VGAuthService%" 17 | -- scanner tool 18 | AND pr.cmdline NOT LIKE "%proc_scan%" 19 | -- os services 20 | AND pr.cmdline NOT LIKE "%/sbin/init%" 21 | AND pr.cmdline NOT LIKE "%systemd-journald%" 22 | AND pr.cmdline NOT LIKE "%systemd-logind%" 23 | AND pr.cmdline NOT LIKE "%systemd-udevd%" 24 | AND pr.cmdline NOT LIKE "%svlogd%" 25 | AND pr.cmdline NOT LIKE "%sshd%" 26 | AND pr.cmdline NOT LIKE "%audispd%" 27 | AND pr.cmdline NOT LIKE "%auditd%" 28 | AND pr.cmdline NOT LIKE "%agetty%" 29 | AND pr.cmdline NOT LIKE "%systemd/systemd%" 30 | AND pr.cmdline NOT LIKE "%cron%" 31 | AND pr.cmdline NOT LIKE "%dhclient%" 32 | -- bosh services 33 | AND pr.cmdline NOT LIKE "%runsv agent%" 34 | AND pr.cmdline NOT LIKE "%runsv monit%" 35 | AND pr.cmdline NOT LIKE "%monit%" 36 | AND pr.cmdline NOT LIKE "%runsvdir%" 37 | AND pr.cmdline NOT LIKE "%bosh-agent%" 38 | AND pr.cmdline NOT LIKE "%bosh-dns-nameserverconfig%" 39 | -- addon services 40 | AND pr.cmdline NOT LIKE "%clamd%" 41 | AND pr.cmdline NOT LIKE "%ipsec/starter%" 42 | AND pr.cmdline NOT LIKE "%filesnitch%" 43 | ORDER BY h.name, pr.name 44 | -------------------------------------------------------------------------------- /filesystem/file_linux.go: -------------------------------------------------------------------------------- 1 | //+build !windows 2 | 3 | package filesystem 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/user" 9 | "syscall" 10 | ) 11 | 12 | type metadata struct { 13 | } 14 | 15 | func GetFileMetadata() FileMetadata { 16 | return &metadata{} 17 | } 18 | 19 | func GetFileConfig() FileConfig { 20 | return FileConfig{ 21 | RootPath: "/", 22 | ExcludedPaths: []string{ 23 | "/dev", "/proc", "/sys", "/run", 24 | }, 25 | } 26 | } 27 | 28 | func (f *metadata) GetUser(_ string, fileInfo os.FileInfo) (string, error) { 29 | uid := fmt.Sprint(fileInfo.Sys().(*syscall.Stat_t).Uid) 30 | user, err := user.LookupId(uid) 31 | if err != nil { 32 | return uid, nil 33 | } 34 | return user.Username, nil 35 | } 36 | 37 | func (f *metadata) GetGroup(_ string, fileInfo os.FileInfo) (string, error) { 38 | gid := fmt.Sprint(fileInfo.Sys().(*syscall.Stat_t).Gid) 39 | group, err := user.LookupGroupId(gid) 40 | if err != nil { 41 | return gid, nil 42 | } 43 | return group.Name, nil 44 | } 45 | -------------------------------------------------------------------------------- /filesystem/file_scanner.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "github.com/pivotal-cf/scantron" 5 | "github.com/pivotal-cf/scantron/scanlog" 6 | "os" 7 | ) 8 | 9 | type FileMetadata interface { 10 | GetUser(path string, fileInfo os.FileInfo) (string, error) 11 | GetGroup(path string, fileInfo os.FileInfo) (string, error) 12 | } 13 | 14 | type FileScanner struct { 15 | Walker FileWalker 16 | Metadata FileMetadata 17 | Logger scanlog.Logger 18 | } 19 | 20 | func (fs *FileScanner) ScanFiles() ([]scantron.File, error) { 21 | 22 | walkedFiles, err := fs.Walker.Walk() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | files := []scantron.File{} 28 | for _, wf := range walkedFiles { 29 | user, err := fs.Metadata.GetUser(wf.Path, wf.Info) 30 | 31 | // Some files (e.g. C:\pagefile.sys) don't have user/group 32 | if err != nil { 33 | fs.Logger.Warnf("Error retrieving user for %s: %s", wf.Path, err) 34 | } 35 | group, err := fs.Metadata.GetGroup(wf.Path, wf.Info) 36 | if err != nil { 37 | fs.Logger.Warnf("Error retrieving group for %s: %s", wf.Path, err) 38 | } 39 | 40 | file := scantron.File{ 41 | Path: wf.Path, 42 | Permissions: wf.Info.Mode(), 43 | Size: wf.Info.Size(), 44 | User: user, 45 | Group: group, 46 | ModifiedTime: wf.Info.ModTime(), 47 | RegexMatches: wf.RegexMatches, 48 | } 49 | 50 | fs.Logger.Debugf("Record file %s: Permissions: '%d' User: '%s' Group: '%s' Size: '%d' Modified: '%s'", 51 | wf.Path, file.Permissions, file.User, file.Group, file.Size, file.ModifiedTime.String()) 52 | 53 | files = append(files, file) 54 | } 55 | 56 | return files, nil 57 | } 58 | -------------------------------------------------------------------------------- /filesystem/file_scanner_test.go: -------------------------------------------------------------------------------- 1 | package filesystem_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/golang/mock/gomock" 6 | "github.com/pivotal-cf/scantron" 7 | "github.com/pivotal-cf/scantron/filesystem" 8 | "github.com/pivotal-cf/scantron/scanlog" 9 | "os" 10 | "time" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | type fakeFileInfo struct { 17 | } 18 | 19 | func (f *fakeFileInfo) Name() string { 20 | return "fake" 21 | } 22 | func (f *fakeFileInfo) Size() int64 { 23 | return 123 24 | } 25 | func (f *fakeFileInfo) Mode() os.FileMode { 26 | return 0755 27 | } 28 | func (f *fakeFileInfo) ModTime() time.Time { 29 | loc, _ := time.LoadLocation("UTC") 30 | return time.Date(2018, time.August, 3, 5, 2, 5, 2, loc) 31 | } 32 | func (f *fakeFileInfo) IsDir() bool { 33 | return false 34 | } 35 | func (f *fakeFileInfo) Sys() interface{} { 36 | return nil 37 | } 38 | 39 | var _ = Describe("FileScanner", func() { 40 | var ( 41 | mockCtrl *gomock.Controller 42 | subject *filesystem.FileScanner 43 | mockFileMetadata *filesystem.MockFileMetadata 44 | mockFileWalker *filesystem.MockFileWalker 45 | ) 46 | 47 | BeforeEach(func() { 48 | 49 | mockCtrl = gomock.NewController(GinkgoT()) 50 | mockFileMetadata = filesystem.NewMockFileMetadata(mockCtrl) 51 | mockFileWalker = filesystem.NewMockFileWalker(mockCtrl) 52 | subject = &filesystem.FileScanner{ 53 | Walker: mockFileWalker, 54 | Metadata: mockFileMetadata, 55 | Logger: scanlog.NewNopLogger(), 56 | } 57 | 58 | }) 59 | 60 | AfterEach(func() { 61 | mockCtrl.Finish() 62 | }) 63 | 64 | It("aborts if walk fails", func() { 65 | mockFileWalker.EXPECT().Walk().Return(nil, errors.New("an error")).Times(1) 66 | 67 | _, err := subject.ScanFiles() 68 | 69 | Expect(err).To(HaveOccurred()) 70 | }) 71 | 72 | It("returns files with metadata", func() { 73 | info := &fakeFileInfo{} 74 | path := "some/path/fake" 75 | mockFileWalker.EXPECT().Walk().Return([]filesystem.WalkedFile{ 76 | { 77 | Path: path, 78 | Info: info, 79 | }, 80 | }, nil).Times(1) 81 | mockFileMetadata.EXPECT().GetUser(path, info).Return("user", nil).Times(1) 82 | mockFileMetadata.EXPECT().GetGroup(path, info).Return("group", nil).Times(1) 83 | 84 | files, err := subject.ScanFiles() 85 | 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Expect(files).To(ConsistOf(scantron.File{ 89 | Path: path, 90 | Permissions: info.Mode(), 91 | Size: info.Size(), 92 | User: "user", 93 | Group: "group", 94 | ModifiedTime: info.ModTime(), 95 | RegexMatches: nil, 96 | })) 97 | }) 98 | 99 | It("does assign regexes to files that do match path and content", func() { 100 | info := &fakeFileInfo{} 101 | path := "some/valuable/fake" 102 | 103 | mockFileWalker.EXPECT().Walk().Return([]filesystem.WalkedFile{ 104 | { 105 | Path: path, 106 | Info: info, 107 | RegexMatches: []scantron.RegexMatch{ 108 | { 109 | ContentRegex: "content", 110 | PathRegex: "path", 111 | }, 112 | }, 113 | }, 114 | }, nil).Times(1) 115 | mockFileMetadata.EXPECT().GetUser(path, info).Return("user", nil).Times(1) 116 | mockFileMetadata.EXPECT().GetGroup(path, info).Return("group", nil).Times(1) 117 | 118 | files, err := subject.ScanFiles() 119 | 120 | Expect(err).NotTo(HaveOccurred()) 121 | 122 | Expect(files).To(ConsistOf(scantron.File{ 123 | Path: path, 124 | Permissions: info.Mode(), 125 | Size: info.Size(), 126 | User: "user", 127 | Group: "group", 128 | ModifiedTime: info.ModTime(), 129 | RegexMatches: []scantron.RegexMatch{ 130 | { 131 | ContentRegex: "content", 132 | PathRegex: "path", 133 | }, 134 | }, 135 | })) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /filesystem/file_walker.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "bufio" 5 | "github.com/pivotal-cf/scantron" 6 | "github.com/pivotal-cf/scantron/scanlog" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "sync" 11 | ) 12 | 13 | type WalkedFile struct { 14 | Path string 15 | Info os.FileInfo 16 | RegexMatches []scantron.RegexMatch 17 | } 18 | 19 | type FileConfig struct { 20 | ExcludedPaths []string 21 | RootPath string 22 | } 23 | 24 | type FileWalker interface { 25 | Walk() ([]WalkedFile, error) 26 | } 27 | 28 | type fileWalker struct { 29 | config FileConfig 30 | logger scanlog.Logger 31 | compiledPathRegexes []*regexp.Regexp 32 | compiledContentRegexes []*regexp.Regexp 33 | maxRegexFileSize int64 34 | } 35 | 36 | type regexJob struct { 37 | wf WalkedFile 38 | matchedPaths []string 39 | } 40 | 41 | func NewWalker(config FileConfig, 42 | fileMatch scantron.FileMatch, 43 | logger scanlog.Logger) (FileWalker, error) { 44 | 45 | compiledPathRegexes, err := compileRegexes(logger, fileMatch.PathRegexes) 46 | if err != nil { 47 | return nil, err 48 | } 49 | compiledContentRegexes, err := compileRegexes(logger, fileMatch.ContentRegexes) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &fileWalker{ 55 | config: config, 56 | logger: logger, 57 | compiledPathRegexes: compiledPathRegexes, 58 | compiledContentRegexes: compiledContentRegexes, 59 | maxRegexFileSize: fileMatch.MaxRegexFileSize, 60 | }, nil 61 | } 62 | 63 | func compileRegexes(logger scanlog.Logger, regexes []string) ([]*regexp.Regexp, error) { 64 | compiledRegexes := []*regexp.Regexp{} 65 | 66 | for _, regex := range regexes { 67 | logger.Debugf("Compiling regex for %s", regex) 68 | compiled, err := regexp.Compile(regex) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | compiledRegexes = append(compiledRegexes, compiled) 74 | } 75 | 76 | return compiledRegexes, nil 77 | } 78 | 79 | func (fw *fileWalker) Walk() ([]WalkedFile, error) { 80 | files := []WalkedFile{} 81 | done := make(chan error, 1) 82 | defer close(done) 83 | const ( 84 | maxInFlight = 100 85 | ) 86 | wf := make(chan WalkedFile, maxInFlight) 87 | regexQueue := make(chan regexJob, maxInFlight) 88 | wg := &sync.WaitGroup{} 89 | 90 | go func() { 91 | done <- filepath.Walk(fw.config.RootPath, func(path string, info os.FileInfo, err error) error { 92 | fw.logger.Debugf("Visiting file %s", path) 93 | if err != nil { 94 | fw.logger.Errorf("Error accessing %s: %s", path, err) 95 | return err 96 | } 97 | 98 | if info.IsDir() { 99 | for _, excludedPath := range fw.config.ExcludedPaths { 100 | if excludedPath == path { 101 | fw.logger.Infof("Skipping excluded directory %s", path) 102 | return filepath.SkipDir 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | if !info.Mode().IsRegular() { 110 | fw.logger.Debugf("Skipping irregular file %s", path) 111 | return nil 112 | } 113 | 114 | matchedPathRegexes := fw.matchPath(path) 115 | pathMatch := len(fw.compiledPathRegexes) == 0 || len(matchedPathRegexes) > 0 116 | sizeMatch := info.Size() <= fw.maxRegexFileSize 117 | checkContent := pathMatch && sizeMatch && len(fw.compiledContentRegexes) > 0 118 | if pathMatch && !sizeMatch { 119 | fw.logger.Debugf("Skipping content scan for %s: file too large", path) 120 | } 121 | 122 | file := WalkedFile{ 123 | Path: path, 124 | Info: info, 125 | RegexMatches: nil, 126 | } 127 | if checkContent { 128 | wg.Add(1) 129 | regexQueue <- regexJob{ 130 | file, 131 | matchedPathRegexes, 132 | } 133 | fw.logger.Debugf("Queued file %s for content check", path) 134 | } else { 135 | wf <- file 136 | fw.logger.Debugf("Recorded file %s", path) 137 | } 138 | 139 | return nil 140 | }) 141 | }() 142 | 143 | if len(fw.compiledContentRegexes) > 0 { 144 | for i := 0; i < maxInFlight; i++ { 145 | go func() { 146 | for job := range regexQueue { 147 | fw.logger.Debugf("Checking file %s", job.wf.Path) 148 | regexMatches, err := fw.matchContent(job.wf.Path, job.matchedPaths) 149 | if err != nil { 150 | fw.logger.Warnf("Error checking content of %s: %s", job.wf.Path, err) 151 | } 152 | 153 | job.wf.RegexMatches = regexMatches 154 | wf <- job.wf 155 | 156 | fw.logger.Debugf("Recorded file %s", job.wf.Path) 157 | wg.Done() 158 | } 159 | }() 160 | } 161 | } 162 | 163 | go func() { 164 | fw.logger.Debugf("Waiting for walker") 165 | err := <-done 166 | fw.logger.Debugf("Waiting for wait group") 167 | wg.Wait() 168 | fw.logger.Debugf("Done waiting for files") 169 | close(wf) 170 | close(regexQueue) 171 | done <- err 172 | fw.logger.Debugf("Walker result forwarded") 173 | }() 174 | 175 | for file := range wf { 176 | files = append(files, file) 177 | } 178 | fw.logger.Debugf("File scan results aggregated") 179 | 180 | err := <-done 181 | fw.logger.Debugf("Walker result received") 182 | if err != nil { 183 | fw.logger.Errorf("Error scanning files: %s", err) 184 | return nil, err 185 | } 186 | 187 | return files, nil 188 | } 189 | 190 | func (fw *fileWalker) matchPath(path string) []string { 191 | var matchedPathRegexes []string 192 | 193 | for _, pathRegex := range fw.compiledPathRegexes { 194 | if pathRegex.MatchString(path) { 195 | fw.logger.Debugf("File path %s matches %s", path, pathRegex.String()) 196 | matchedPathRegexes = append(matchedPathRegexes, pathRegex.String()) 197 | } 198 | } 199 | 200 | return matchedPathRegexes 201 | } 202 | 203 | func (fw *fileWalker) matchContent(path string, matchedPathRegexes []string) ([]scantron.RegexMatch, error) { 204 | var regexMatches []scantron.RegexMatch 205 | 206 | for _, contentRegex := range fw.compiledContentRegexes { 207 | match, err := fw.checkContent(contentRegex, path) 208 | if err != nil { 209 | return nil, err 210 | } 211 | if match { 212 | fw.logger.Debugf("Content of %s matches %s", path, contentRegex.String()) 213 | if len(matchedPathRegexes) == 0 { 214 | regexMatches = append(regexMatches, scantron.RegexMatch{ 215 | ContentRegex: contentRegex.String(), 216 | PathRegex: "", 217 | }) 218 | } else { 219 | for _, pr := range matchedPathRegexes { 220 | regexMatches = append(regexMatches, scantron.RegexMatch{ 221 | ContentRegex: contentRegex.String(), 222 | PathRegex: pr, 223 | }) 224 | } 225 | } 226 | } 227 | } 228 | 229 | return regexMatches, nil 230 | } 231 | 232 | func (fw *fileWalker) checkContent(contentRegex *regexp.Regexp, path string) (bool, error) { 233 | f, err := os.Open(path) 234 | if err != nil { 235 | return false, err 236 | } 237 | defer f.Close() 238 | 239 | rr := bufio.NewReader(f) 240 | return contentRegex.MatchReader(rr), nil 241 | } 242 | -------------------------------------------------------------------------------- /filesystem/file_walker_test.go: -------------------------------------------------------------------------------- 1 | package filesystem_test 2 | 3 | import ( 4 | "github.com/pivotal-cf/scantron" 5 | "github.com/pivotal-cf/scantron/filesystem" 6 | "github.com/pivotal-cf/scantron/scanlog" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "syscall" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("FileWalker", func() { 17 | var ( 18 | umask int 19 | root string 20 | excludedPaths []string 21 | contentRegex []string 22 | pathRegex []string 23 | subject filesystem.FileWalker 24 | ) 25 | 26 | createSubject := func() { 27 | config := filesystem.FileConfig{ 28 | RootPath: root, 29 | ExcludedPaths: excludedPaths, 30 | } 31 | subject, _ = filesystem.NewWalker( 32 | config, 33 | scantron.FileMatch{ 34 | pathRegex, 35 | contentRegex, 36 | 1000, 37 | }, 38 | scanlog.NewNopLogger()) 39 | } 40 | 41 | createFile := func(dirPath string, content string) string { 42 | filePath := path.Join(dirPath, "some-file") 43 | 44 | err := ioutil.WriteFile(filePath, []byte(content), 0600) 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | return filePath 48 | } 49 | 50 | createDir := func(dirName string) string { 51 | dirPath := path.Join(root, dirName) 52 | 53 | err := os.Mkdir(dirPath, 0755) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | return dirPath 57 | } 58 | 59 | BeforeEach(func() { 60 | var err error 61 | root, err = ioutil.TempDir("", "proc-scan-test") 62 | Expect(err).NotTo(HaveOccurred()) 63 | umask = syscall.Umask(0000) 64 | 65 | createSubject() 66 | }) 67 | 68 | AfterEach(func() { 69 | os.RemoveAll(root) 70 | syscall.Umask(umask) 71 | }) 72 | 73 | It("detects files", func() { 74 | filePath := createFile(root, "data") 75 | 76 | files, err := subject.Walk() 77 | Expect(err).NotTo(HaveOccurred()) 78 | 79 | Expect(files).To(HaveLen(1)) 80 | Expect(files[0].Path).To(Equal(filePath)) 81 | Expect(files[0].RegexMatches).To(BeNil()) 82 | }) 83 | 84 | It("does not record path matches if the content regex does not match", func() { 85 | contentRegex = []string{"valuable"} 86 | pathRegex = []string{"interesting"} 87 | createSubject() 88 | procDir := createDir("interesting") 89 | filePath := createFile(procDir, "data") 90 | 91 | files, err := subject.Walk() 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | Expect(files).To(HaveLen(1)) 95 | Expect(files[0].Path).To(Equal(filePath)) 96 | Expect(files[0].RegexMatches).To(BeNil()) 97 | }) 98 | 99 | It("does record regex matches if both path and content match", func() { 100 | contentRegex = []string{"valuable"} 101 | pathRegex = []string{"interesting"} 102 | createSubject() 103 | procDir := createDir("interesting") 104 | filePath := createFile(procDir, "valuable") 105 | 106 | files, err := subject.Walk() 107 | Expect(err).NotTo(HaveOccurred()) 108 | 109 | Expect(files).To(HaveLen(1)) 110 | Expect(files[0].Path).To(Equal(filePath)) 111 | Expect(files[0].RegexMatches).To(ConsistOf( 112 | scantron.RegexMatch{ 113 | PathRegex: "interesting", 114 | ContentRegex: "valuable", 115 | }, 116 | )) 117 | }) 118 | 119 | It("does record regex matches if content matches and no path regex supplied", func() { 120 | contentRegex = []string{"valuable"} 121 | pathRegex = []string{} 122 | createSubject() 123 | procDir := createDir("anywhere") 124 | filePath := createFile(procDir, "valuable") 125 | 126 | files, err := subject.Walk() 127 | Expect(err).NotTo(HaveOccurred()) 128 | 129 | Expect(files).To(HaveLen(1)) 130 | Expect(files[0].Path).To(Equal(filePath)) 131 | Expect(files[0].RegexMatches).To(ConsistOf( 132 | scantron.RegexMatch{ 133 | PathRegex: "", 134 | ContentRegex: "valuable", 135 | }, 136 | )) 137 | }) 138 | 139 | It("does not record directories", func() { 140 | createDir("some-dir") 141 | 142 | files, err := subject.Walk() 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | Expect(files).To(BeEmpty()) 146 | }) 147 | 148 | It("excludes files from the exclude list", func() { 149 | procDir := createDir("proc") 150 | createFile(procDir, "data") 151 | 152 | excludedPaths = []string{procDir} 153 | createSubject() 154 | 155 | files, err := subject.Walk() 156 | Expect(err).NotTo(HaveOccurred()) 157 | 158 | Expect(files).To(BeEmpty()) 159 | }) 160 | 161 | It("returns an error when it fails to walk the filesystem", func() { 162 | root = "/doesnotexist" 163 | createSubject() 164 | 165 | _, err := subject.Walk() 166 | Expect(err).To(HaveOccurred()) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /filesystem/file_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package filesystem 4 | 5 | import ( 6 | "fmt" 7 | "github.com/hectane/go-acl/api" 8 | "golang.org/x/sys/windows" 9 | "os" 10 | ) 11 | 12 | type metadata struct { 13 | } 14 | 15 | func GetFileMetadata() FileMetadata { 16 | return &metadata{} 17 | } 18 | 19 | func GetFileConfig() FileConfig { 20 | return FileConfig{ 21 | RootPath: "C:\\", 22 | ExcludedPaths: []string{}, 23 | } 24 | } 25 | 26 | func (f *metadata) GetUser(path string, fileInfo os.FileInfo) (string, error) { 27 | ownerSid, _, err := f.getSids(path, fileInfo) 28 | if err != nil { 29 | return "", err 30 | } 31 | return f.lookupSid(ownerSid) 32 | } 33 | 34 | func (f *metadata) GetGroup(path string, fileInfo os.FileInfo) (string, error) { 35 | _, groupSid, err := f.getSids(path, fileInfo) 36 | if err != nil { 37 | return "", err 38 | } 39 | return f.lookupSid(groupSid) 40 | } 41 | 42 | func (f *metadata) getSids(path string, fileInfo os.FileInfo) (*windows.SID, *windows.SID, error) { 43 | var ( 44 | owner *windows.SID 45 | group *windows.SID 46 | ) 47 | err := api.GetNamedSecurityInfo( 48 | fmt.Sprintf("\\\\?\\%s", path), // prefix \\?\ to enable extended-length paths > 260 characters 49 | api.SE_FILE_OBJECT, 50 | api.OWNER_SECURITY_INFORMATION|api.GROUP_SECURITY_INFORMATION, 51 | &owner, 52 | &group, 53 | nil, 54 | nil, 55 | nil, 56 | ) 57 | 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | return owner, group, nil 63 | } 64 | 65 | func (f *metadata) lookupSid(sid *windows.SID) (string, error) { 66 | account, domain, _, err := sid.LookupAccount("") 67 | if err != nil { 68 | sidString, err := sid.String() 69 | if err != nil { 70 | return "", err 71 | } 72 | return sidString, nil 73 | } 74 | return fmt.Sprintf("%s\\%s", domain, account), nil 75 | } 76 | -------------------------------------------------------------------------------- /filesystem/filesystem_suite_test.go: -------------------------------------------------------------------------------- 1 | package filesystem_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestFilesystem(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Filesystem Suite") 13 | } 14 | -------------------------------------------------------------------------------- /filesystem/mock_file_scanner.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: filesystem/file_scanner.go 3 | 4 | // Package filesystem is a generated GoMock package. 5 | package filesystem 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | os "os" 10 | reflect "reflect" 11 | time "time" 12 | ) 13 | 14 | // MockFileMetadata is a mock of FileMetadata interface 15 | type MockFileMetadata struct { 16 | ctrl *gomock.Controller 17 | recorder *MockFileMetadataMockRecorder 18 | } 19 | 20 | // MockFileMetadataMockRecorder is the mock recorder for MockFileMetadata 21 | type MockFileMetadataMockRecorder struct { 22 | mock *MockFileMetadata 23 | } 24 | 25 | // NewMockFileMetadata creates a new mock instance 26 | func NewMockFileMetadata(ctrl *gomock.Controller) *MockFileMetadata { 27 | mock := &MockFileMetadata{ctrl: ctrl} 28 | mock.recorder = &MockFileMetadataMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockFileMetadata) EXPECT() *MockFileMetadataMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // GetUser mocks base method 38 | func (m *MockFileMetadata) GetUser(path string, fileInfo os.FileInfo) (string, error) { 39 | ret := m.ctrl.Call(m, "GetUser", path, fileInfo) 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // GetUser indicates an expected call of GetUser 46 | func (mr *MockFileMetadataMockRecorder) GetUser(path, fileInfo interface{}) *gomock.Call { 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockFileMetadata)(nil).GetUser), path, fileInfo) 48 | } 49 | 50 | // GetGroup mocks base method 51 | func (m *MockFileMetadata) GetGroup(path string, fileInfo os.FileInfo) (string, error) { 52 | ret := m.ctrl.Call(m, "GetGroup", path, fileInfo) 53 | ret0, _ := ret[0].(string) 54 | ret1, _ := ret[1].(error) 55 | return ret0, ret1 56 | } 57 | 58 | // GetGroup indicates an expected call of GetGroup 59 | func (mr *MockFileMetadataMockRecorder) GetGroup(path, fileInfo interface{}) *gomock.Call { 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroup", reflect.TypeOf((*MockFileMetadata)(nil).GetGroup), path, fileInfo) 61 | } 62 | 63 | // GetModifiedTime mocks base method 64 | func (m *MockFileMetadata) GetModifiedTime(path string, fileInfo os.FileInfo) time.Time { 65 | ret := m.ctrl.Call(m, "GetModifiedTime", path, fileInfo) 66 | ret0, _ := ret[0].(time.Time) 67 | return ret0 68 | } 69 | 70 | // GetModifiedTime indicates an expected call of GetModifiedTime 71 | func (mr *MockFileMetadataMockRecorder) GetModifiedTime(path, fileInfo interface{}) *gomock.Call { 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModifiedTime", reflect.TypeOf((*MockFileMetadata)(nil).GetModifiedTime), path, fileInfo) 73 | } 74 | -------------------------------------------------------------------------------- /filesystem/mock_file_walker.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: filesystem/file_walker.go 3 | 4 | // Package filesystem is a generated GoMock package. 5 | package filesystem 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockFileWalker is a mock of FileWalker interface 13 | type MockFileWalker struct { 14 | ctrl *gomock.Controller 15 | recorder *MockFileWalkerMockRecorder 16 | } 17 | 18 | // MockFileWalkerMockRecorder is the mock recorder for MockFileWalker 19 | type MockFileWalkerMockRecorder struct { 20 | mock *MockFileWalker 21 | } 22 | 23 | // NewMockFileWalker creates a new mock instance 24 | func NewMockFileWalker(ctrl *gomock.Controller) *MockFileWalker { 25 | mock := &MockFileWalker{ctrl: ctrl} 26 | mock.recorder = &MockFileWalkerMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockFileWalker) EXPECT() *MockFileWalkerMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // Walk mocks base method 36 | func (m *MockFileWalker) Walk() ([]WalkedFile, error) { 37 | ret := m.ctrl.Call(m, "Walk") 38 | ret0, _ := ret[0].([]WalkedFile) 39 | ret1, _ := ret[1].(error) 40 | return ret0, ret1 41 | } 42 | 43 | // Walk indicates an expected call of Walk 44 | func (mr *MockFileWalkerMockRecorder) Walk() *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Walk", reflect.TypeOf((*MockFileWalker)(nil).Walk)) 46 | } 47 | -------------------------------------------------------------------------------- /manifest/broken.yml: -------------------------------------------------------------------------------- 1 | ]]]] not a yaml file 2 | -------------------------------------------------------------------------------- /manifest/example.yml: -------------------------------------------------------------------------------- 1 | specs: 2 | - prefix: host1 3 | processes: 4 | - command: command1 5 | user: root 6 | ports: 7 | - 1234 8 | - 5678 9 | - 9012 10 | - command: command2 11 | user: user2 12 | ports: 13 | - 8230 14 | - 2852 15 | 16 | - prefix: host2 17 | processes: 18 | - command: command3 19 | user: user3 20 | ports: 21 | - 9876 22 | - 5432 23 | -------------------------------------------------------------------------------- /manifest/manifest.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | type Manifest struct { 4 | Specs []Spec `yaml:"specs"` 5 | } 6 | 7 | type Spec struct { 8 | Prefix string `yaml:"prefix"` 9 | Processes []Process 10 | } 11 | 12 | type Process struct { 13 | Command string `yaml:"command"` 14 | User string `yaml:"user"` 15 | Ports []Port `yaml:"ports"` 16 | Ignore bool `yaml:"ignore_ports,omitempty"` 17 | } 18 | 19 | type Port int 20 | 21 | func (h Spec) ExpectedPorts() []Port { 22 | var ports []Port 23 | 24 | for _, proc := range h.Processes { 25 | ports = append(ports, proc.Ports...) 26 | } 27 | 28 | return ports 29 | } 30 | 31 | func (h Spec) ExpectedCommands() []string { 32 | var commands []string 33 | 34 | for _, proc := range h.Processes { 35 | commands = append(commands, proc.Command) 36 | } 37 | 38 | return commands 39 | } 40 | 41 | func (h Spec) ShouldIgnorePortsForCommand(command string) bool { 42 | for _, process := range h.Processes { 43 | if process.Command == command { 44 | return process.Ignore 45 | } 46 | } 47 | 48 | return false 49 | } 50 | -------------------------------------------------------------------------------- /manifest/manifest_suite_test.go: -------------------------------------------------------------------------------- 1 | package manifest_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestManifest(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Manifest Suite") 13 | } 14 | -------------------------------------------------------------------------------- /manifest/manifest_test.go: -------------------------------------------------------------------------------- 1 | package manifest_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/pivotal-cf/scantron/manifest" 8 | ) 9 | 10 | var _ = Describe("Manifest", func() { 11 | host := manifest.Spec{ 12 | Prefix: "a-host", 13 | Processes: []manifest.Process{ 14 | { 15 | Command: "command-1", 16 | Ports: []manifest.Port{1, 6, 8, 2}, 17 | }, 18 | { 19 | Command: "command-2", 20 | Ports: []manifest.Port{93, 235, 2493}, 21 | }, 22 | { 23 | Command: "ignore-command", 24 | Ignore: true, 25 | }, 26 | }, 27 | } 28 | 29 | Describe("getting the expected ports for a host", func() { 30 | It("gets all of the ports across every process", func() { 31 | Expect(host.ExpectedPorts()).To(ConsistOf( 32 | manifest.Port(1), 33 | manifest.Port(2), 34 | manifest.Port(6), 35 | manifest.Port(8), 36 | manifest.Port(93), 37 | manifest.Port(235), 38 | manifest.Port(2493), 39 | )) 40 | }) 41 | }) 42 | 43 | Describe("getting the expected processes for a host", func() { 44 | It("gets all of the commands across every process", func() { 45 | Expect(host.ExpectedCommands()).To(ConsistOf( 46 | "command-1", 47 | "command-2", 48 | "ignore-command", 49 | )) 50 | }) 51 | }) 52 | 53 | Describe("checking if we should ignore ports for a process", func() { 54 | It("checks if the flag is set", func() { 55 | Expect(host.ShouldIgnorePortsForCommand("command-1")).To(BeFalse()) 56 | Expect(host.ShouldIgnorePortsForCommand("missing")).To(BeFalse()) 57 | 58 | Expect(host.ShouldIgnorePortsForCommand("ignore-command")).To(BeTrue()) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /manifest/parser.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | 7 | yaml "gopkg.in/yaml.v2" 8 | ) 9 | 10 | func Parse(filePath string) (Manifest, error) { 11 | bs, err := ioutil.ReadFile(filePath) 12 | if err != nil { 13 | return Manifest{}, err 14 | } 15 | 16 | var manifest Manifest 17 | 18 | err = yaml.Unmarshal(bs, &manifest) 19 | if err != nil { 20 | return Manifest{}, errors.New("incorrect yaml format") 21 | } 22 | 23 | err = validate(manifest) 24 | if err != nil { 25 | return Manifest{}, err 26 | } 27 | 28 | return manifest, nil 29 | } 30 | 31 | func validate(m Manifest) error { 32 | if len(m.Specs) == 0 { 33 | return errors.New("file is empty") 34 | } 35 | for _, spec := range m.Specs { 36 | if len(spec.Prefix) == 0 { 37 | return errors.New("prefix undefined") 38 | } else { 39 | for _, proc := range spec.Processes { 40 | if len(proc.Command) == 0 || 41 | len(proc.User) == 0 || 42 | (len(proc.Ports) == 0 && proc.Ignore == false) { 43 | return errors.New("process info missing") 44 | } 45 | } 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /manifest/parser_test.go: -------------------------------------------------------------------------------- 1 | package manifest_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/pivotal-cf/scantron/manifest" 8 | ) 9 | 10 | var _ = Describe("Parser", func() { 11 | It("parses the file", func() { 12 | m, err := manifest.Parse("example.yml") 13 | Expect(err).NotTo(HaveOccurred()) 14 | 15 | Expect(m).To(Equal(manifest.Manifest{ 16 | Specs: []manifest.Spec{ 17 | { 18 | Prefix: "host1", 19 | Processes: []manifest.Process{ 20 | { 21 | Command: "command1", 22 | User: "root", 23 | Ports: []manifest.Port{1234, 5678, 9012}, 24 | }, 25 | { 26 | Command: "command2", 27 | User: "user2", 28 | Ports: []manifest.Port{8230, 2852}, 29 | }, 30 | }, 31 | }, 32 | { 33 | Prefix: "host2", 34 | Processes: []manifest.Process{ 35 | { 36 | Command: "command3", 37 | User: "user3", 38 | Ports: []manifest.Port{9876, 5432}, 39 | }, 40 | }, 41 | }, 42 | }, 43 | })) 44 | }) 45 | 46 | Context("when the file does not exist", func() { 47 | It("returns an error", func() { 48 | _, err := manifest.Parse("this/does/not/exist") 49 | Expect(err).To(HaveOccurred()) 50 | }) 51 | }) 52 | 53 | Context("when the file is mangled", func() { 54 | It("returns an error", func() { 55 | _, err := manifest.Parse("broken.yml") 56 | Expect(err).To(HaveOccurred()) 57 | }) 58 | }) 59 | 60 | Context("when the file has semantic errors", func() { 61 | It("returns an error when specs is misnamed", func() { 62 | _, err := manifest.Parse("semantic_err_specs.yml") 63 | Expect(err).To(HaveOccurred()) 64 | Expect(err).To(MatchError("file is empty")) 65 | }) 66 | 67 | It("returns an error when prefix is misnamed", func() { 68 | _, err := manifest.Parse("semantic_err_prefix.yml") 69 | Expect(err).To(HaveOccurred()) 70 | Expect(err).To(MatchError("prefix undefined")) 71 | }) 72 | 73 | It("returns an error when process info is missing", func() { 74 | _, err := manifest.Parse("semantic_err_command.yml") 75 | Expect(err).To(HaveOccurred()) 76 | Expect(err).To(MatchError("process info missing")) 77 | }) 78 | 79 | It("returns an error when processes are undefined", func() { 80 | _, err := manifest.Parse("semantic_err_processes.yml") 81 | Expect(err).To(HaveOccurred()) 82 | Expect(err).To(MatchError("incorrect yaml format")) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /manifest/semantic_err_command.yml: -------------------------------------------------------------------------------- 1 | specs: 2 | - prefix: host1 3 | processes: 4 | - command: command1 5 | user: root 6 | ports: 7 | - 1234 8 | - 5678 9 | - 9012 10 | - user: user2 11 | ports: 12 | - 8230 13 | -------------------------------------------------------------------------------- /manifest/semantic_err_prefix.yml: -------------------------------------------------------------------------------- 1 | specs: 2 | - prefix: host1 3 | processes: 4 | - command: command1 5 | user: root 6 | ports: 7 | - 1234 8 | - 5678 9 | - 9012 10 | - command: command2 11 | user: user2 12 | ports: 13 | - 8230 14 | - 2852 15 | 16 | - name: host2 17 | processes: 18 | - command: command3 19 | user: user3 20 | ports: 21 | - 9876 22 | - 5432 23 | 24 | - name: host3 25 | processes: 26 | - processes: command3 27 | user: user3 28 | ports: 29 | -------------------------------------------------------------------------------- /manifest/semantic_err_processes.yml: -------------------------------------------------------------------------------- 1 | specs: 2 | - prefix: host1 3 | - command: command1 4 | user: root 5 | ports: 6 | - 1234 7 | - 5678 8 | - 9012 9 | - command: command2 10 | user: user2 11 | ports: 12 | - 8230 13 | - 2852 14 | -------------------------------------------------------------------------------- /manifest/semantic_err_specs.yml: -------------------------------------------------------------------------------- 1 | hosts: 2 | - prefix: host1 3 | processes: 4 | - command: command1 5 | user: root 6 | ports: 7 | - 1234 8 | - 5678 9 | - 9012 10 | - command: command2 11 | user: user2 12 | ports: 13 | - 8230 14 | - 2852 15 | 16 | - name: host2 17 | processes: 18 | - command: command3 19 | user: user3 20 | ports: 21 | - 9876 22 | - 5432 23 | 24 | - name: host3 25 | processes: 26 | - processes: command3 27 | user: user3 28 | ports: 29 | -------------------------------------------------------------------------------- /netstat/netstat.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package netstat 4 | 5 | import ( 6 | "bufio" 7 | "log" 8 | "net" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/pivotal-cf/scantron" 13 | ) 14 | 15 | type NetstatInfo struct { 16 | CommandName string 17 | PID string 18 | LocalAddress string 19 | ForeignAddress string 20 | State string 21 | Protocol string 22 | } 23 | 24 | type NetstatPort struct { 25 | PID int 26 | Port scantron.Port 27 | } 28 | 29 | type NetstatPorts []NetstatPort 30 | 31 | func ParseNetstatOutputForPort(output string) []NetstatPort { 32 | scanner := bufio.NewScanner(strings.NewReader(output)) 33 | result := []NetstatPort{} 34 | 35 | for scanner.Scan() { 36 | line := scanner.Text() 37 | info, valid := parseNetstatLine(line) 38 | 39 | if valid { 40 | result = append(result, createNetstatPort(info)) 41 | } 42 | } 43 | 44 | return result 45 | } 46 | 47 | func parseNetstatLine(line string) (NetstatInfo, bool) { 48 | netstat := strings.Fields(line) 49 | 50 | var protocol, localAddress, foreignAddress, state, process string 51 | 52 | if len(netstat) < 6 { 53 | return NetstatInfo{}, false 54 | } 55 | 56 | protocol = netstat[0] 57 | localAddress = netstat[3] 58 | foreignAddress = netstat[4] 59 | 60 | if len(netstat) == 6 { 61 | state = "" 62 | process = netstat[5] 63 | } else { 64 | state = netstat[5] 65 | process = netstat[6] 66 | } 67 | 68 | switch protocol { 69 | case "tcp", "tcp6", "udp", "udp6": 70 | break 71 | default: 72 | return NetstatInfo{}, false 73 | } 74 | 75 | processTokens := strings.Split(process, "/") 76 | if len(processTokens) < 2 { 77 | return NetstatInfo{}, false 78 | } 79 | 80 | pid := processTokens[0] 81 | cmd := processTokens[1] 82 | 83 | return NetstatInfo{ 84 | CommandName: cmd, 85 | PID: pid, 86 | LocalAddress: localAddress, 87 | ForeignAddress: foreignAddress, 88 | State: state, 89 | Protocol: protocol, 90 | }, true 91 | } 92 | 93 | func splitAddress(infoAddress string) (string, int) { 94 | // XXX(cb): In the netstat output IPv6 addresses are shown as :::22 whereas 95 | // Go parsing requires [::]:22. We try and fix this up here but this is 96 | // undoubtedly imperfect. 97 | conformAddr := strings.Replace(infoAddress, "::", "[::]", 1) 98 | address, port, err := net.SplitHostPort(conformAddr) 99 | if err != nil { 100 | log.Printf("failed to split address %q: %s", conformAddr, err) 101 | } 102 | number, err := strconv.Atoi(port) 103 | if err != nil { 104 | number = -1 105 | } 106 | return address, number 107 | } 108 | 109 | func createPortFromAddress(localAddressAndPort string, foreignAddressAndPort string, protocol string, state string) scantron.Port { 110 | localAddress, localNumber := splitAddress(localAddressAndPort) 111 | foreignAddress, foreignNumber := splitAddress(foreignAddressAndPort) 112 | return scantron.Port{ 113 | Protocol: protocol, 114 | Address: localAddress, 115 | Number: localNumber, 116 | ForeignAddress: foreignAddress, 117 | ForeignNumber: foreignNumber, 118 | State: state, 119 | } 120 | } 121 | 122 | func createNetstatPort(info NetstatInfo) NetstatPort { 123 | port := createPortFromAddress(info.LocalAddress, info.ForeignAddress, info.Protocol, info.State) 124 | 125 | id, _ := strconv.Atoi(info.PID) 126 | return NetstatPort{ 127 | PID: id, 128 | Port: port, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /netstat/netstat_suite_test.go: -------------------------------------------------------------------------------- 1 | package netstat_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestNetstat(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Netstat Suite") 13 | } 14 | -------------------------------------------------------------------------------- /netstat/netstat_test.go: -------------------------------------------------------------------------------- 1 | package netstat_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/pivotal-cf/scantron" 8 | "github.com/pivotal-cf/scantron/netstat" 9 | ) 10 | 11 | var _ = Describe("NetstatParser", func() { 12 | It("parses and converts a single line correctly", func() { 13 | input := ` 14 | tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 1317/java 15 | ` 16 | Expect(netstat.ParseNetstatOutputForPort(input)).To(Equal([]netstat.NetstatPort{ 17 | { 18 | PID: 1317, 19 | Port: scantron.Port{ 20 | Protocol: "tcp", 21 | Address: "127.0.0.1", 22 | Number: 8080, 23 | ForeignAddress: "0.0.0.0", 24 | ForeignNumber: -1, 25 | State: "LISTEN", 26 | }, 27 | }, 28 | })) 29 | }) 30 | 31 | It("parses and converts multiple lines correctly", func() { 32 | input := ` 33 | tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 1317/java 34 | udp 0 0 127.0.0.2:8080 127.0.0.3:8443 ESTABLISHED 1318/java 35 | ` 36 | Expect(netstat.ParseNetstatOutputForPort(input)).To(Equal([]netstat.NetstatPort{ 37 | { 38 | PID: 1317, 39 | Port: scantron.Port{ 40 | Protocol: "tcp", 41 | Address: "127.0.0.1", 42 | Number: 8080, 43 | ForeignAddress: "0.0.0.0", 44 | ForeignNumber: -1, 45 | State: "LISTEN", 46 | }, 47 | }, 48 | { 49 | PID: 1318, 50 | Port: scantron.Port{ 51 | Protocol: "udp", 52 | Address: "127.0.0.2", 53 | Number: 8080, 54 | ForeignAddress: "127.0.0.3", 55 | ForeignNumber: 8443, 56 | State: "ESTABLISHED", 57 | }, 58 | }, 59 | })) 60 | }) 61 | 62 | It("parses both IPv4 and IPv6", func() { 63 | input := ` 64 | tcp6 0 0 :::50051 :::* LISTEN 5149/rolodexd 65 | udp6 0 0 :::111 :::* 777/rpcbind` 66 | 67 | Expect(netstat.ParseNetstatOutputForPort(input)).To(Equal([]netstat.NetstatPort{ 68 | { 69 | PID: 5149, 70 | Port: scantron.Port{ 71 | Protocol: "tcp6", 72 | Address: "::", 73 | Number: 50051, 74 | ForeignAddress: "::", 75 | ForeignNumber: -1, 76 | State: "LISTEN", 77 | }, 78 | }, 79 | { 80 | PID: 777, 81 | Port: scantron.Port{ 82 | Protocol: "udp6", 83 | Address: "::", 84 | Number: 111, 85 | ForeignAddress: "::", 86 | ForeignNumber: -1, 87 | State: "", 88 | }, 89 | }, 90 | })) 91 | }) 92 | 93 | Context("when the socket state is missing because it is a raw socket", func() { 94 | It("still parses that", func() { 95 | input := "udp 0 0 127.0.0.1:53 0.0.0.0:* 4113/consul" 96 | 97 | Expect(netstat.ParseNetstatOutputForPort(input)).To(Equal([]netstat.NetstatPort{ 98 | { 99 | PID: 4113, 100 | Port: scantron.Port{ 101 | Protocol: "udp", 102 | Address: "127.0.0.1", 103 | Number: 53, 104 | ForeignAddress: "0.0.0.0", 105 | ForeignNumber: -1, 106 | State: "", 107 | }, 108 | }, 109 | })) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /process/mock_system_resources.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: process/system_resources.go 3 | 4 | // Package process is a generated GoMock package. 5 | package process 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | scantron "github.com/pivotal-cf/scantron" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockSystemResources is a mock of SystemResources interface 14 | type MockSystemResources struct { 15 | ctrl *gomock.Controller 16 | recorder *MockSystemResourcesMockRecorder 17 | } 18 | 19 | // MockSystemResourcesMockRecorder is the mock recorder for MockSystemResources 20 | type MockSystemResourcesMockRecorder struct { 21 | mock *MockSystemResources 22 | } 23 | 24 | // NewMockSystemResources creates a new mock instance 25 | func NewMockSystemResources(ctrl *gomock.Controller) *MockSystemResources { 26 | mock := &MockSystemResources{ctrl: ctrl} 27 | mock.recorder = &MockSystemResourcesMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockSystemResources) EXPECT() *MockSystemResourcesMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetProcesses mocks base method 37 | func (m *MockSystemResources) GetProcesses() ([]scantron.Process, error) { 38 | ret := m.ctrl.Call(m, "GetProcesses") 39 | ret0, _ := ret[0].([]scantron.Process) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetProcesses indicates an expected call of GetProcesses 45 | func (mr *MockSystemResourcesMockRecorder) GetProcesses() *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProcesses", reflect.TypeOf((*MockSystemResources)(nil).GetProcesses)) 47 | } 48 | 49 | // GetPorts mocks base method 50 | func (m *MockSystemResources) GetPorts() ProcessPorts { 51 | ret := m.ctrl.Call(m, "GetPorts") 52 | ret0, _ := ret[0].(ProcessPorts) 53 | return ret0 54 | } 55 | 56 | // GetPorts indicates an expected call of GetPorts 57 | func (mr *MockSystemResourcesMockRecorder) GetPorts() *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPorts", reflect.TypeOf((*MockSystemResources)(nil).GetPorts)) 59 | } 60 | -------------------------------------------------------------------------------- /process/process_linux.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package process 4 | 5 | import ( 6 | "fmt" 7 | "github.com/keybase/go-ps" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/pivotal-cf/scantron" 14 | "github.com/pivotal-cf/scantron/netstat" 15 | ) 16 | 17 | type SystemResourceImpl struct { 18 | } 19 | 20 | func (s *SystemResourceImpl) GetProcesses() ([]scantron.Process, error) { 21 | rawProcesses, err := ps.Processes() 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | processes := []scantron.Process{} 27 | for _, rawProcess := range rawProcesses { 28 | pid := rawProcess.Pid() 29 | process := scantron.Process{ 30 | CommandName: rawProcess.Executable(), 31 | PID: pid, 32 | User: getUser(pid), 33 | Cmdline: getCmdline(pid), 34 | Env: getEnv(pid), 35 | } 36 | processes = append(processes, process) 37 | } 38 | 39 | return processes, nil 40 | } 41 | 42 | func (s *SystemResourceImpl) GetPorts() ProcessPorts { 43 | bs, err := exec.Command("netstat", "-at", "-4", "-6", "--numeric-ports", "-u", "-p").Output() 44 | if err != nil { 45 | return nil 46 | } 47 | 48 | netstatPorts := netstat.ParseNetstatOutputForPort(string(bs)) 49 | processPorts := []ProcessPort{} 50 | for _, np := range netstatPorts { 51 | processPorts = append(processPorts, ProcessPort{ 52 | PID: np.PID, 53 | Port: np.Port, 54 | }) 55 | } 56 | 57 | return processPorts 58 | } 59 | 60 | func getUser(pid int) string { 61 | bs, err := exec.Command("ps", "-e", "-o", "uname:20=", "-f", strconv.Itoa(pid)).CombinedOutput() 62 | if err != nil { 63 | fmt.Fprintln(os.Stderr, "error getting user:", err) 64 | return "" 65 | } 66 | 67 | return strings.TrimSpace(string(bs)) 68 | } 69 | 70 | func getCmdline(pid int) []string { 71 | cmdline, err := readFile(fmt.Sprintf("/proc/%d/cmdline", pid)) 72 | if err != nil { 73 | fmt.Fprintln(os.Stderr, "error getting cmdline:", err) 74 | return []string{} 75 | } 76 | 77 | return cmdline 78 | } 79 | 80 | func getEnv(pid int) []string { 81 | env, err := readFile(fmt.Sprintf("/proc/%d/environ", pid)) 82 | if err != nil { 83 | fmt.Fprintln(os.Stderr, "error getting env:", err) 84 | return []string{} 85 | } 86 | 87 | return env 88 | } 89 | -------------------------------------------------------------------------------- /process/process_scanner.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "io/ioutil" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/pivotal-cf/scantron" 9 | "github.com/pivotal-cf/scantron/scanlog" 10 | "github.com/pivotal-cf/scantron/tlsscan" 11 | ) 12 | 13 | type ProcessPort struct { 14 | PID int 15 | Port scantron.Port 16 | } 17 | 18 | type ProcessPorts []ProcessPort 19 | 20 | type ProcessScanner struct { 21 | SysRes SystemResources 22 | TlsScan tlsscan.TlsScanner 23 | } 24 | 25 | func (ps *ProcessScanner) ScanProcesses(logger scanlog.Logger) ([]scantron.Process, error) { 26 | processes, err := ps.SysRes.GetProcesses() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | ports := ps.SysRes.GetPorts() 32 | for i := range processes { 33 | portsForPid := ports.LocalPortsForPID(processes[i].PID) 34 | 35 | for j := range portsForPid { 36 | 37 | if strings.ToUpper(portsForPid[j].State) != "LISTEN" { 38 | continue 39 | } 40 | 41 | if portsForPid[j].Protocol == "udp" { 42 | continue 43 | } 44 | 45 | portsForPid[j].TLSInformation = ps.getTLSInformation(logger, portsForPid[j]) 46 | } 47 | 48 | processes[i].Ports = portsForPid 49 | } 50 | 51 | return processes, nil 52 | } 53 | 54 | func (ps ProcessPorts) LocalPortsForPID(pid int) []scantron.Port { 55 | result := []scantron.Port{} 56 | 57 | for _, nsPort := range ps { 58 | if nsPort.PID == pid { 59 | result = append(result, nsPort.Port) 60 | } 61 | } 62 | 63 | return result 64 | } 65 | 66 | func readFile(path string) ([]string, error) { 67 | bs, err := ioutil.ReadFile(path) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | inputs := strings.Split(string(bs), "\x00") 73 | output := []string{} 74 | 75 | for _, input := range inputs { 76 | if input != "" { 77 | output = append(output, input) 78 | } 79 | } 80 | 81 | return output, nil 82 | } 83 | 84 | func (ps *ProcessScanner) getTLSInformation(logger scanlog.Logger, port scantron.Port) *scantron.TLSInformation { 85 | portNum := strconv.Itoa(port.Number) 86 | 87 | portLogger := logger.With("port", portNum) 88 | 89 | tlsInformation := &scantron.TLSInformation{} 90 | 91 | results, err := ps.TlsScan.Scan(portLogger, "localhost", portNum) 92 | if err != nil { 93 | tlsInformation.ScanError = err 94 | return tlsInformation 95 | } 96 | 97 | if !results.HasTLS() { 98 | return nil 99 | } 100 | 101 | tlsInformation.CipherInformation = results 102 | 103 | cert, mutual, err := ps.TlsScan.FetchTLSInformation("localhost", portNum) 104 | if err != nil { 105 | tlsInformation.ScanError = err 106 | return tlsInformation 107 | } 108 | 109 | tlsInformation.Certificate = cert 110 | tlsInformation.Mutual = mutual 111 | 112 | return tlsInformation 113 | } 114 | -------------------------------------------------------------------------------- /process/process_scanner_test.go: -------------------------------------------------------------------------------- 1 | package process_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang/mock/gomock" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | . "github.com/onsi/gomega/gstruct" 9 | "github.com/pivotal-cf/scantron" 10 | "github.com/pivotal-cf/scantron/process" 11 | "github.com/pivotal-cf/scantron/scanlog" 12 | "github.com/pivotal-cf/scantron/tlsscan" 13 | "time" 14 | ) 15 | 16 | var _ = Describe("ProcessScanner", func() { 17 | 18 | var ( 19 | mockCtrl *gomock.Controller 20 | mockSystemResources *process.MockSystemResources 21 | mockTlsScanner *tlsscan.MockTlsScanner 22 | subject *process.ProcessScanner 23 | ) 24 | 25 | BeforeEach(func() { 26 | mockCtrl = gomock.NewController(GinkgoT()) 27 | mockSystemResources = process.NewMockSystemResources(mockCtrl) 28 | mockTlsScanner = tlsscan.NewMockTlsScanner(mockCtrl) 29 | subject = &process.ProcessScanner{ 30 | SysRes: mockSystemResources, 31 | TlsScan: mockTlsScanner, 32 | } 33 | }) 34 | 35 | AfterEach(func() { 36 | mockCtrl.Finish() 37 | }) 38 | 39 | It("Should associate ports with processes", func() { 40 | 41 | systemProcesses := []scantron.Process{ 42 | { 43 | CommandName: "command", 44 | PID: 123, 45 | User: "user", 46 | Cmdline: []string{"cmd", "arg"}, 47 | Env: []string{"foo=bar"}, 48 | }, 49 | } 50 | 51 | port := scantron.Port{ 52 | Protocol: "tcp", 53 | Address: "1.2.3.4", 54 | Number: 4567, 55 | ForeignAddress: "2.3.4.5", 56 | ForeignNumber: 6789, 57 | State: "Established", 58 | } 59 | systemPorts := []process.ProcessPort{ 60 | { 61 | PID: 123, 62 | Port: port, 63 | }, 64 | } 65 | 66 | portIdFn := func(element interface{}) string { 67 | return fmt.Sprintf("%d", element.(scantron.Port).Number) 68 | } 69 | 70 | mockSystemResources.EXPECT().GetProcesses().Return(systemProcesses, nil).Times(1) 71 | mockSystemResources.EXPECT().GetPorts().Return(systemPorts).Times(1) 72 | 73 | processes, err := subject.ScanProcesses(scanlog.NewNopLogger()) 74 | 75 | Expect(err).Should(BeNil()) 76 | Expect(processes).Should(HaveLen(1)) 77 | Expect(processes[0]).Should(MatchAllFields(Fields{ 78 | "CommandName": Equal("command"), 79 | "PID": Equal(123), 80 | "User": Equal("user"), 81 | "Cmdline": Equal([]string{"cmd", "arg"}), 82 | "Env": Equal([]string{"foo=bar"}), 83 | "Ports": MatchAllElements(portIdFn, Elements{ 84 | "4567": MatchAllFields(Fields{ 85 | "Protocol": Equal("tcp"), 86 | "Address": Equal("1.2.3.4"), 87 | "Number": Equal(4567), 88 | "ForeignAddress": Equal("2.3.4.5"), 89 | "ForeignNumber": Equal(6789), 90 | "State": Equal("Established"), 91 | "TLSInformation": BeNil(), 92 | }), 93 | }), 94 | })) 95 | }) 96 | 97 | It("Should scan TLS ciphers for listening TCP ports", func() { 98 | 99 | systemProcesses := []scantron.Process{ 100 | { 101 | CommandName: "command", 102 | PID: 123, 103 | User: "user", 104 | Cmdline: []string{"cmd", "arg"}, 105 | Env: []string{"foo=bar"}, 106 | }, 107 | } 108 | 109 | port := scantron.Port{ 110 | Protocol: "tcp", 111 | Address: "1.2.3.4", 112 | Number: 4567, 113 | ForeignAddress: "0.0.0.0", 114 | ForeignNumber: -1, 115 | State: "Listen", 116 | } 117 | systemPorts := []process.ProcessPort{ 118 | { 119 | PID: 123, 120 | Port: port, 121 | }, 122 | } 123 | 124 | portIdFn := func(element interface{}) string { 125 | return fmt.Sprintf("%d", element.(scantron.Port).Number) 126 | } 127 | 128 | mockSystemResources.EXPECT().GetProcesses().Return(systemProcesses, nil).Times(1) 129 | mockSystemResources.EXPECT().GetPorts().Return(systemPorts).Times(1) 130 | 131 | cipherInformation := scantron.CipherInformation{ 132 | "VersionSSL30": []string{"cipher"}, 133 | } 134 | mockTlsScanner.EXPECT().Scan(gomock.Any(), gomock.Eq("localhost"), gomock.Eq("4567")).Return(cipherInformation, nil).Times(1) 135 | 136 | certificate := &scantron.Certificate{ 137 | Expiration: time.Time{}, 138 | Bits: 2048, 139 | Subject: scantron.CertificateSubject{ 140 | Country: "", 141 | Province: "", 142 | Locality: "", 143 | 144 | Organization: "", 145 | CommonName: "", 146 | }, 147 | } 148 | mockTlsScanner.EXPECT().FetchTLSInformation("localhost", "4567").Return( 149 | certificate, false, nil).Times(1) 150 | 151 | processes, err := subject.ScanProcesses(scanlog.NewNopLogger()) 152 | 153 | Expect(err).Should(BeNil()) 154 | Expect(processes).Should(HaveLen(1)) 155 | Expect(processes[0]).Should(MatchAllFields(Fields{ 156 | "CommandName": Equal("command"), 157 | "PID": Equal(123), 158 | "User": Equal("user"), 159 | "Cmdline": Equal([]string{"cmd", "arg"}), 160 | "Env": Equal([]string{"foo=bar"}), 161 | "Ports": MatchAllElements(portIdFn, Elements{ 162 | "4567": MatchAllFields(Fields{ 163 | "Protocol": Equal("tcp"), 164 | "Address": Equal("1.2.3.4"), 165 | "Number": Equal(4567), 166 | "ForeignAddress": Equal("0.0.0.0"), 167 | "ForeignNumber": Equal(-1), 168 | "State": Equal("Listen"), 169 | "TLSInformation": PointTo(MatchAllFields(Fields{ 170 | "Certificate": Equal(certificate), 171 | "CipherInformation": Equal(cipherInformation), 172 | "Mutual": BeFalse(), 173 | "ScanError": BeNil(), 174 | })), 175 | }), 176 | }), 177 | })) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /process/process_suite_test.go: -------------------------------------------------------------------------------- 1 | package process_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "testing" 7 | ) 8 | 9 | func TestUse(t *testing.T) { 10 | RegisterFailHandler(Fail) 11 | RunSpecs(t, "Process Suite") 12 | } 13 | -------------------------------------------------------------------------------- /process/process_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package process 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "github.com/pivotal-cf/scantron" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | type WinProcess struct { 14 | CommandName string `json:"name"` 15 | PID int `json:"pid"` 16 | User string `json:"user"` 17 | Cmdline string `json:"cmdline"` 18 | } 19 | 20 | type WinEnv struct { 21 | Key string `json:"Key"` 22 | Value string `json:"Value"` 23 | } 24 | 25 | type WinPort struct { 26 | LocalAddress string `json:"localaddress"` 27 | LocalPort int `json:"localport"` 28 | RemoteAddress string `json:"remoteaddress"` 29 | RemotePort int `json:"remoteport"` 30 | OwningProcess int `json:"owningprocess"` 31 | State string `json:"state"` 32 | } 33 | type SystemResourceImpl struct { 34 | } 35 | 36 | func (s *SystemResourceImpl) GetProcesses() ([]scantron.Process, error) { 37 | 38 | cmd := exec.Command("powershell", "get-wmiobject win32_process | select @{Name='pid'; Expression={$_.ProcessId}}, @{Name='name'; Expression={$_.Name}}, @{Name='user'; Expression={$_.GetOwner().User }}, @{Name='cmdline'; Expression={$_.Commandline}} | ConvertTo-Json") 39 | 40 | out, e := cmd.Output() 41 | if e != nil { 42 | return nil, e 43 | } 44 | 45 | var rawProcesses = []WinProcess{} 46 | err := json.Unmarshal(out, &rawProcesses) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | processes := []scantron.Process{} 52 | for _, rawProcess := range rawProcesses { 53 | pid := rawProcess.PID 54 | process := scantron.Process{ 55 | CommandName: rawProcess.CommandName, 56 | PID: pid, 57 | User: rawProcess.User, 58 | Cmdline: strings.Split(rawProcess.Cmdline, " "), 59 | Env: getEnv(pid), 60 | } 61 | processes = append(processes, process) 62 | } 63 | 64 | return processes, nil 65 | } 66 | 67 | func (s *SystemResourceImpl) GetPorts() ProcessPorts { 68 | cmd := exec.Command("powershell", "get-netudpendpoint | select localaddress,localport,owningprocess| convertto-json") 69 | out, e := cmd.Output() 70 | if e != nil { 71 | return nil 72 | } 73 | 74 | var rawUdp = []WinPort{} 75 | err := json.Unmarshal(out, &rawUdp) 76 | 77 | if err != nil { 78 | return nil 79 | } 80 | 81 | cmd = exec.Command("powershell", "get-nettcpconnection | select @{Name='state';Expression={$_.State.ToString()}},localaddress,localport,remoteaddress,remoteport,owningprocess | convertto-json") 82 | out, e = cmd.Output() 83 | if e != nil { 84 | return nil 85 | } 86 | 87 | var rawTcp = []WinPort{} 88 | err = json.Unmarshal(out, &rawTcp) 89 | 90 | if err != nil { 91 | return nil 92 | } 93 | 94 | ports := []ProcessPort{} 95 | for _, p := range rawUdp { 96 | ports = append(ports, ProcessPort{ 97 | PID: p.OwningProcess, 98 | Port: scantron.Port{ 99 | Protocol: "udp", 100 | Address: p.LocalAddress, 101 | Number: p.LocalPort, 102 | }, 103 | }) 104 | } 105 | for _, p := range rawTcp { 106 | ports = append(ports, ProcessPort{ 107 | PID: p.OwningProcess, 108 | Port: scantron.Port{ 109 | Protocol: "tcp", 110 | Address: p.LocalAddress, 111 | Number: p.LocalPort, 112 | ForeignAddress: p.RemoteAddress, 113 | ForeignNumber: p.RemotePort, 114 | State: p.State, // FIXME normalization with netstat? 115 | }, 116 | }) 117 | } 118 | 119 | return ports 120 | } 121 | 122 | func getEnv(pid int) []string { 123 | cmd := exec.Command("powershell", fmt.Sprintf("(get-process -id %d).StartInfo.EnvironmentVariables | Convertto-json", pid)) 124 | 125 | out, e := cmd.Output() 126 | if e != nil { 127 | return nil 128 | } 129 | 130 | var rawVars = []WinEnv{} 131 | err := json.Unmarshal(out, &rawVars) 132 | 133 | if err != nil { 134 | return nil 135 | } 136 | 137 | env := []string{} 138 | for _, v := range rawVars { 139 | env = append(env, fmt.Sprintf("%s=%s", v.Key, v.Value)) 140 | } 141 | 142 | return env 143 | } 144 | -------------------------------------------------------------------------------- /process/system_resources.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import "github.com/pivotal-cf/scantron" 4 | 5 | type SystemResources interface { 6 | GetProcesses() ([]scantron.Process, error) 7 | GetPorts() ProcessPorts 8 | } 9 | -------------------------------------------------------------------------------- /remotemachine/mock_remote_machine.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: remotemachine/remote_machine.go 3 | 4 | // Package remotemachine is a generated GoMock package. 5 | package remotemachine 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | io "io" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockRemoteMachine is a mock of RemoteMachine interface 14 | type MockRemoteMachine struct { 15 | ctrl *gomock.Controller 16 | recorder *MockRemoteMachineMockRecorder 17 | } 18 | 19 | // MockRemoteMachineMockRecorder is the mock recorder for MockRemoteMachine 20 | type MockRemoteMachineMockRecorder struct { 21 | mock *MockRemoteMachine 22 | } 23 | 24 | // NewMockRemoteMachine creates a new mock instance 25 | func NewMockRemoteMachine(ctrl *gomock.Controller) *MockRemoteMachine { 26 | mock := &MockRemoteMachine{ctrl: ctrl} 27 | mock.recorder = &MockRemoteMachineMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockRemoteMachine) EXPECT() *MockRemoteMachineMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Address mocks base method 37 | func (m *MockRemoteMachine) Address() string { 38 | ret := m.ctrl.Call(m, "Address") 39 | ret0, _ := ret[0].(string) 40 | return ret0 41 | } 42 | 43 | // Address indicates an expected call of Address 44 | func (mr *MockRemoteMachineMockRecorder) Address() *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockRemoteMachine)(nil).Address)) 46 | } 47 | 48 | // Host mocks base method 49 | func (m *MockRemoteMachine) Host() string { 50 | ret := m.ctrl.Call(m, "Host") 51 | ret0, _ := ret[0].(string) 52 | return ret0 53 | } 54 | 55 | // Host indicates an expected call of Host 56 | func (mr *MockRemoteMachineMockRecorder) Host() *gomock.Call { 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockRemoteMachine)(nil).Host)) 58 | } 59 | 60 | // OSName mocks base method 61 | func (m *MockRemoteMachine) OSName() string { 62 | ret := m.ctrl.Call(m, "OSName") 63 | ret0, _ := ret[0].(string) 64 | return ret0 65 | } 66 | 67 | // OSName indicates an expected call of OSName 68 | func (mr *MockRemoteMachineMockRecorder) OSName() *gomock.Call { 69 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OSName", reflect.TypeOf((*MockRemoteMachine)(nil).OSName)) 70 | } 71 | 72 | // Password mocks base method 73 | func (m *MockRemoteMachine) Password() string { 74 | ret := m.ctrl.Call(m, "Password") 75 | ret0, _ := ret[0].(string) 76 | return ret0 77 | } 78 | 79 | // Password indicates an expected call of Password 80 | func (mr *MockRemoteMachineMockRecorder) Password() *gomock.Call { 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Password", reflect.TypeOf((*MockRemoteMachine)(nil).Password)) 82 | } 83 | 84 | // UploadFile mocks base method 85 | func (m *MockRemoteMachine) UploadFile(localPath, remotePath string) error { 86 | ret := m.ctrl.Call(m, "UploadFile", localPath, remotePath) 87 | ret0, _ := ret[0].(error) 88 | return ret0 89 | } 90 | 91 | // UploadFile indicates an expected call of UploadFile 92 | func (mr *MockRemoteMachineMockRecorder) UploadFile(localPath, remotePath interface{}) *gomock.Call { 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadFile", reflect.TypeOf((*MockRemoteMachine)(nil).UploadFile), localPath, remotePath) 94 | } 95 | 96 | // DeleteFile mocks base method 97 | func (m *MockRemoteMachine) DeleteFile(remotePath string) error { 98 | ret := m.ctrl.Call(m, "DeleteFile", remotePath) 99 | ret0, _ := ret[0].(error) 100 | return ret0 101 | } 102 | 103 | // DeleteFile indicates an expected call of DeleteFile 104 | func (mr *MockRemoteMachineMockRecorder) DeleteFile(remotePath interface{}) *gomock.Call { 105 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFile", reflect.TypeOf((*MockRemoteMachine)(nil).DeleteFile), remotePath) 106 | } 107 | 108 | // RunCommand mocks base method 109 | func (m *MockRemoteMachine) RunCommand(arg0 string) (io.Reader, error) { 110 | ret := m.ctrl.Call(m, "RunCommand", arg0) 111 | ret0, _ := ret[0].(io.Reader) 112 | ret1, _ := ret[1].(error) 113 | return ret0, ret1 114 | } 115 | 116 | // RunCommand indicates an expected call of RunCommand 117 | func (mr *MockRemoteMachineMockRecorder) RunCommand(arg0 interface{}) *gomock.Call { 118 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCommand", reflect.TypeOf((*MockRemoteMachine)(nil).RunCommand), arg0) 119 | } 120 | 121 | // Close mocks base method 122 | func (m *MockRemoteMachine) Close() error { 123 | ret := m.ctrl.Call(m, "Close") 124 | ret0, _ := ret[0].(error) 125 | return ret0 126 | } 127 | 128 | // Close indicates an expected call of Close 129 | func (mr *MockRemoteMachineMockRecorder) Close() *gomock.Call { 130 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRemoteMachine)(nil).Close)) 131 | } 132 | -------------------------------------------------------------------------------- /remotemachine/remote_machine.go: -------------------------------------------------------------------------------- 1 | package remotemachine 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/pivotal-cf/scantron" 10 | "github.com/pkg/sftp" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | type RemoteMachine interface { 15 | Address() string 16 | Host() string 17 | OSName() string 18 | Password() string 19 | 20 | UploadFile(localPath, remotePath string) error 21 | DeleteFile(remotePath string) error 22 | 23 | RunCommand(string) (io.Reader, error) 24 | 25 | Close() error 26 | } 27 | 28 | type remoteMachine struct { 29 | machine scantron.Machine 30 | 31 | conn *ssh.Client 32 | } 33 | 34 | func NewRemoteMachine(machine scantron.Machine) RemoteMachine { 35 | return &remoteMachine{ 36 | machine: machine, 37 | } 38 | } 39 | 40 | func (r *remoteMachine) Address() string { 41 | return fmt.Sprintf("%s:22", r.machine.Address) 42 | } 43 | 44 | func (r *remoteMachine) Host() string { 45 | return r.machine.Address 46 | } 47 | 48 | func (r *remoteMachine) OSName() string { 49 | return r.machine.OSName 50 | } 51 | 52 | func (r *remoteMachine) Password() string { 53 | return r.machine.Password 54 | } 55 | 56 | func (r *remoteMachine) UploadFile(localPath, remotePath string) error { 57 | conn, err := r.sshConn() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | sftp, err := sftp.NewClient(conn) 63 | if err != nil { 64 | return err 65 | } 66 | defer sftp.Close() 67 | 68 | srcFile, err := os.Open(localPath) 69 | if err != nil { 70 | return err 71 | } 72 | defer srcFile.Close() 73 | 74 | dstFile, err := sftp.Create(remotePath) 75 | if err != nil { 76 | return err 77 | } 78 | defer dstFile.Close() 79 | 80 | dstFile.ReadFrom(srcFile) 81 | sftp.Chmod(remotePath, 0700) 82 | 83 | return nil 84 | } 85 | 86 | func (r *remoteMachine) DeleteFile(remotePath string) error { 87 | conn, err := r.sshConn() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | sftp, err := sftp.NewClient(conn) 93 | if err != nil { 94 | return err 95 | } 96 | defer sftp.Close() 97 | 98 | return sftp.Remove(remotePath) 99 | } 100 | 101 | func (r *remoteMachine) RunCommand(command string) (io.Reader, error) { 102 | conn, err := r.sshConn() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | session, err := conn.NewSession() 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | stderr, err := session.StderrPipe() 113 | if err != nil { 114 | return nil, err 115 | } 116 | go io.Copy(os.Stderr, stderr) 117 | 118 | bs, err := session.Output(command) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return bytes.NewBuffer(bs), nil 124 | } 125 | 126 | func (r *remoteMachine) auth() []ssh.AuthMethod { 127 | if r.machine.Key != nil { 128 | return []ssh.AuthMethod{ 129 | ssh.PublicKeys(r.machine.Key), 130 | } 131 | } 132 | 133 | return []ssh.AuthMethod{ 134 | ssh.Password(r.machine.Password), 135 | } 136 | } 137 | 138 | func (r *remoteMachine) sshConn() (*ssh.Client, error) { 139 | if r.conn != nil { 140 | return r.conn, nil 141 | } 142 | 143 | config := &ssh.ClientConfig{ 144 | User: r.machine.Username, 145 | Auth: r.auth(), 146 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 147 | } 148 | 149 | conn, err := ssh.Dial("tcp", r.Address(), config) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | r.conn = conn 155 | 156 | return conn, nil 157 | } 158 | 159 | func (r *remoteMachine) Close() error { 160 | if r.conn != nil { 161 | return r.conn.Close() 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /report/insecure_ssh_key_report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import "github.com/pivotal-cf/scantron/db" 4 | 5 | func BuildInsecureSshKeyReport(database *db.Database) (Report, error) { 6 | rows, err := database.DB().Query(` 7 | SELECT h.name 8 | FROM ssh_keys s1 9 | CROSS JOIN ssh_keys s2 10 | JOIN hosts h 11 | ON s1.host_id = h.id 12 | WHERE s1.key = s2.key 13 | AND s1.id != s2.id 14 | ORDER BY h.name 15 | `) 16 | if err != nil { 17 | return Report{}, err 18 | } 19 | 20 | defer rows.Close() 21 | 22 | report := Report{ 23 | Title: "Duplicate SSH keys:", 24 | Header: []string{"Identity"}, 25 | } 26 | 27 | for rows.Next() { 28 | var hostname string 29 | 30 | err := rows.Scan(&hostname) 31 | if err != nil { 32 | return Report{}, err 33 | } 34 | 35 | report.Rows = append(report.Rows, []string{ 36 | hostname, 37 | }) 38 | } 39 | 40 | return report, nil 41 | } 42 | -------------------------------------------------------------------------------- /report/insecure_ssh_key_report_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/pivotal-cf/scantron/db" 12 | "github.com/pivotal-cf/scantron/report" 13 | ) 14 | 15 | var _ = Describe("BuildInsecureSshKeyReport", func() { 16 | var ( 17 | databasePath, tmpdir string 18 | database *db.Database 19 | ) 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpdir, err = ioutil.TempDir("", "report-test") 24 | Expect(err).NotTo(HaveOccurred()) 25 | databasePath = filepath.Join(tmpdir, "db.db") 26 | 27 | database, err = createTestDatabase(databasePath) 28 | Expect(err).NotTo(HaveOccurred()) 29 | }) 30 | 31 | AfterEach(func() { 32 | err := database.Close() 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = os.RemoveAll(tmpdir) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | It("shows insecure and duplicate ssh keys", func() { 40 | r, err := report.BuildInsecureSshKeyReport(database) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(r.Title).To(Equal("Duplicate SSH keys:")) 44 | Expect(r.Header).To(Equal([]string{"Identity"})) 45 | Expect(r.Rows).To(HaveLen(2)) 46 | Expect(r.Rows).To(Equal([][]string{ 47 | {"host1"}, 48 | {"host3"}, 49 | })) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | ) 9 | 10 | type Report struct { 11 | Header []string 12 | Rows [][]string 13 | Title string 14 | Footnote string 15 | } 16 | 17 | func (r Report) IsEmpty() bool { 18 | return len(r.Rows) == 0 19 | } 20 | 21 | func (r Report) WriteTo(writer io.Writer) { 22 | table := tablewriter.NewWriter(writer) 23 | table.SetHeader(r.Header) 24 | table.AppendBulk(r.Rows) 25 | 26 | fmt.Println(r.Title) 27 | fmt.Println("") 28 | table.Render() 29 | fmt.Println("") 30 | 31 | if r.Footnote != "" { 32 | fmt.Println(r.Footnote) 33 | } 34 | 35 | fmt.Println("") 36 | } 37 | -------------------------------------------------------------------------------- /report/report_suite_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | 9 | "github.com/pivotal-cf/scantron" 10 | "github.com/pivotal-cf/scantron/db" 11 | "github.com/pivotal-cf/scantron/scanner" 12 | ) 13 | 14 | func TestReport(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Report Suite") 17 | } 18 | 19 | func createTestDatabase(databasePath string) (*db.Database, error) { 20 | hosts := scanner.ScanResult{ 21 | JobResults: []scanner.JobResult{ 22 | { 23 | Job: "host3", 24 | Files: []scantron.File{ 25 | { 26 | Path: "/var/vcap/data/jobs/world-readable", 27 | Permissions: 0004, 28 | }, 29 | { 30 | Path: "/var/vcap/data/jobs/world-writable", 31 | Permissions: 0002, 32 | }, 33 | { 34 | Path: "/var/vcap/data/jobs/world-executable", 35 | Permissions: 0001, 36 | }, 37 | }, 38 | SSHKeys: []scantron.SSHKey{ 39 | { 40 | Type: "ssh-rsa", 41 | Key: "SSH KEY 1", 42 | }, 43 | }, 44 | Services: []scantron.Process{ 45 | { 46 | CommandName: "command1", 47 | User: "root", 48 | Ports: []scantron.Port{ 49 | { 50 | State: "LISTEN", 51 | Address: "10.0.5.23", 52 | Number: 7890, 53 | ForeignAddress: "0.0.0.0", 54 | ForeignNumber: -1, 55 | TLSInformation: &scantron.TLSInformation{ 56 | Certificate: &scantron.Certificate{}, 57 | CipherInformation: scantron.CipherInformation{ 58 | "VersionSSL30": []string{"Just the worst"}, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | { 67 | Job: "host1", 68 | Files: []scantron.File{ 69 | { 70 | Path: "/var/vcap/data/jobs/world-everything", 71 | Permissions: 0007, 72 | }, 73 | { 74 | Path: "/root/world-everything", 75 | Permissions: 0007, 76 | }, 77 | }, 78 | SSHKeys: []scantron.SSHKey{ 79 | { 80 | Type: "ssh-rsa", 81 | Key: "SSH KEY 1", 82 | }, 83 | }, 84 | Services: []scantron.Process{ 85 | { 86 | CommandName: "command2", 87 | User: "root", 88 | Ports: []scantron.Port{ 89 | { 90 | State: "LISTEN", 91 | Address: "10.0.5.21", 92 | Number: 19999, 93 | ForeignAddress: "0.0.0.0", 94 | ForeignNumber: -1, 95 | }, 96 | }, 97 | }, 98 | { 99 | CommandName: "command1", 100 | User: "root", 101 | Ports: []scantron.Port{ 102 | { 103 | State: "LISTEN", 104 | Address: "10.0.5.21", 105 | Number: 7890, 106 | ForeignAddress: "0.0.0.0", 107 | ForeignNumber: -1, 108 | TLSInformation: &scantron.TLSInformation{ 109 | Certificate: &scantron.Certificate{}, 110 | CipherInformation: scantron.CipherInformation{ 111 | "VersionSSL30": []string{"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"}, 112 | }, 113 | }, 114 | }, 115 | { 116 | State: "LISTEN", 117 | Address: "44.44.44.44", 118 | Number: 7890, 119 | ForeignAddress: "0.0.0.0", 120 | ForeignNumber: -1, 121 | }, 122 | { 123 | State: "LISTEN", 124 | Address: "127.0.0.1", 125 | Number: 8890, 126 | ForeignAddress: "0.0.0.0", 127 | ForeignNumber: -1, 128 | TLSInformation: &scantron.TLSInformation{ 129 | Certificate: &scantron.Certificate{}, 130 | CipherInformation: scantron.CipherInformation{ 131 | "VersionTLS12": []string{"Bad Cipher"}, 132 | }, 133 | }, 134 | }, 135 | { 136 | State: "ESTABLISHED", 137 | Number: 7891, 138 | ForeignAddress: "10.0.0.1", 139 | ForeignNumber: 2345, 140 | }, 141 | }, 142 | }, 143 | { 144 | CommandName: "sshd", 145 | User: "root", 146 | Ports: []scantron.Port{ 147 | { 148 | State: "LISTEN", 149 | Address: "10.0.5.21", 150 | Number: 22, 151 | ForeignAddress: "0.0.0.0", 152 | ForeignNumber: -1, 153 | }, 154 | }, 155 | }, 156 | { 157 | CommandName: "rpcbind", 158 | User: "root", 159 | Ports: []scantron.Port{ 160 | { 161 | State: "LISTEN", 162 | Address: "10.0.5.21", 163 | Number: 111, 164 | ForeignAddress: "0.0.0.0", 165 | ForeignNumber: -1, 166 | }, 167 | }, 168 | }, 169 | }, 170 | }, 171 | { 172 | Job: "host2", 173 | SSHKeys: []scantron.SSHKey{ 174 | { 175 | Type: "ssh-rsa", 176 | Key: "SSH KEY 2", 177 | }, 178 | }, 179 | Services: []scantron.Process{ 180 | { 181 | CommandName: "command2", 182 | User: "root", 183 | Ports: []scantron.Port{ 184 | { 185 | State: "LISTEN", 186 | Address: "10.0.5.22", 187 | Number: 19999, 188 | ForeignAddress: "0.0.0.0", 189 | ForeignNumber: -1, 190 | TLSInformation: &scantron.TLSInformation{ 191 | Certificate: &scantron.Certificate{}, 192 | CipherInformation: scantron.CipherInformation{ 193 | "VersionTLS12": []string{"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"}, 194 | }, 195 | }, 196 | }, 197 | }, 198 | }, 199 | { 200 | CommandName: "some-non-root-process", 201 | User: "vcap", 202 | Ports: []scantron.Port{ 203 | { 204 | State: "LISTEN", 205 | Address: "10.0.5.22", 206 | Number: 12345, 207 | ForeignAddress: "0.0.0.0", 208 | ForeignNumber: -1, 209 | }, 210 | }, 211 | }, 212 | }, 213 | }, 214 | { 215 | Job: "winhost1", 216 | SSHKeys: []scantron.SSHKey{ 217 | { 218 | Type: "ssh-rsa", 219 | Key: "SSH KEY 3", 220 | }, 221 | }, 222 | Services: []scantron.Process{ 223 | { 224 | CommandName: "command.exe", 225 | User: "SYSTEM", 226 | Ports: []scantron.Port{ 227 | { 228 | State: "Bound", 229 | Address: "10.0.5.25", 230 | Number: 19998, 231 | ForeignAddress: "0.0.0.0", 232 | ForeignNumber: -1, 233 | TLSInformation: &scantron.TLSInformation{ 234 | Certificate: &scantron.Certificate{}, 235 | CipherInformation: scantron.CipherInformation{ 236 | "VersionTLS12": []string{"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"}, 237 | }, 238 | }, 239 | }, 240 | }, 241 | }, 242 | { 243 | CommandName: "command2.exe", 244 | User: "SYSTEM", 245 | Ports: []scantron.Port{ 246 | { 247 | State: "Listen", 248 | Address: "10.0.5.25", 249 | Number: 19999, 250 | ForeignAddress: "0.0.0.0", 251 | ForeignNumber: -1, 252 | TLSInformation: &scantron.TLSInformation{ 253 | Certificate: &scantron.Certificate{}, 254 | CipherInformation: scantron.CipherInformation{ 255 | "VersionTLS12": []string{"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"}, 256 | }, 257 | }, 258 | }, 259 | }, 260 | }, 261 | { 262 | CommandName: "some-non-root-process.exe", 263 | User: "vcap", 264 | Ports: []scantron.Port{ 265 | { 266 | State: "Listen", 267 | Address: "10.0.5.25", 268 | Number: 12345, 269 | ForeignAddress: "0.0.0.0", 270 | ForeignNumber: -1, 271 | }, 272 | }, 273 | }, 274 | }, 275 | }, 276 | }, 277 | } 278 | 279 | database, err := db.CreateDatabase(databasePath) 280 | if err != nil { 281 | return nil, err 282 | } 283 | 284 | err = database.SaveReport("cf1", hosts) 285 | if err != nil { 286 | return nil, err 287 | } 288 | 289 | return database, nil 290 | } 291 | -------------------------------------------------------------------------------- /report/root_processes_report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pivotal-cf/scantron/db" 6 | ) 7 | 8 | func BuildRootProcessesReport(database *db.Database) (Report, error) { 9 | rows, err := database.DB().Query(` 10 | SELECT DISTINCT h.name, po.number, pr.name 11 | FROM hosts h 12 | JOIN processes pr 13 | ON h.id = pr.host_id 14 | JOIN ports po 15 | ON po.process_id = pr.id 16 | WHERE (upper(po.state) = "LISTEN" OR po.state = "Bound") 17 | AND po.address != "127.0.0.1" 18 | AND po.address NOT LIKE "172.%" 19 | AND po.address NOT LIKE "169.%" 20 | AND (pr.user = "root" OR pr.user = "SYSTEM") 21 | AND pr.name NOT IN ('sshd', 'rpcbind') 22 | ORDER BY h.name, po.number 23 | `) 24 | if err != nil { 25 | return Report{}, err 26 | } 27 | 28 | defer rows.Close() 29 | 30 | report := Report{ 31 | Title: "Externally-accessible processes running as root:", 32 | Header: []string{"Identity", "Port", "Process Name"}, 33 | } 34 | 35 | for rows.Next() { 36 | var ( 37 | hostname string 38 | processName string 39 | portNumber int 40 | ) 41 | 42 | err := rows.Scan(&hostname, &portNumber, &processName) 43 | if err != nil { 44 | return Report{}, err 45 | } 46 | 47 | report.Rows = append(report.Rows, []string{ 48 | hostname, 49 | fmt.Sprintf("%d", portNumber), 50 | processName, 51 | }) 52 | } 53 | 54 | return report, nil 55 | } 56 | -------------------------------------------------------------------------------- /report/root_processes_report_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/pivotal-cf/scantron/db" 12 | "github.com/pivotal-cf/scantron/report" 13 | ) 14 | 15 | var _ = Describe("BuildRootProcessesReport", func() { 16 | var ( 17 | databasePath, tmpdir string 18 | database *db.Database 19 | ) 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpdir, err = ioutil.TempDir("", "report-test") 24 | Expect(err).NotTo(HaveOccurred()) 25 | databasePath = filepath.Join(tmpdir, "db.db") 26 | 27 | database, err = createTestDatabase(databasePath) 28 | Expect(err).NotTo(HaveOccurred()) 29 | }) 30 | 31 | AfterEach(func() { 32 | err := database.Close() 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = os.RemoveAll(tmpdir) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | It("shows externally-accessible processes running as root", func() { 40 | r, err := report.BuildRootProcessesReport(database) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(r.Title).To(Equal("Externally-accessible processes running as root:")) 44 | Expect(r.Header).To(Equal([]string{"Identity", "Port", "Process Name"})) 45 | Expect(r.Rows).To(HaveLen(6)) 46 | Expect(r.Rows).To(Equal([][]string{ 47 | {"host1", "7890", "command1"}, 48 | {"host1", "19999", "command2"}, 49 | {"host2", "19999", "command2"}, 50 | {"host3", "7890", "command1"}, 51 | {"winhost1", "19998", "command.exe"}, 52 | {"winhost1", "19999", "command2.exe"}, 53 | })) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /report/tls_violations_report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pivotal-cf/scantron/db" 6 | "github.com/pivotal-cf/scantron/tlsscan" 7 | "strings" 8 | ) 9 | 10 | type stringSlice []string 11 | 12 | func (s stringSlice) contains(str string) bool { 13 | for _, found := range s { 14 | if found == str { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | var goodSuites = stringSlice{ 22 | "VersionTLS12", 23 | } 24 | 25 | func buildGoodCiphers() (stringSlice, error) { 26 | allCiphers, err := tlsscan.BuildCipherSuites() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | goodCiphers := make(stringSlice, 0) 32 | for _, c := range allCiphers { 33 | if c.Recommended { 34 | goodCiphers = append(goodCiphers, c.Name) 35 | } 36 | } 37 | return goodCiphers, nil 38 | } 39 | 40 | func BuildTLSViolationsReport(database *db.Database) (Report, error) { 41 | goodCiphers, err := buildGoodCiphers() 42 | if err != nil { 43 | return Report{}, err 44 | } 45 | query := fmt.Sprintf(`SELECT DISTINCT h.name, po.number, pr.name, s.suite, c.cipher 46 | FROM hosts h 47 | JOIN processes pr 48 | ON h.id = pr.host_id 49 | JOIN ports po 50 | ON po.process_id = pr.id 51 | JOIN tls_certificates t 52 | ON t.port_id = po.id 53 | JOIN certificate_to_ciphersuite ctc 54 | ON t.id = ctc.certificate_id 55 | JOIN tls_suites s 56 | ON ctc.suite_id = s.id 57 | JOIN tls_ciphers c 58 | ON ctc.cipher_id = c.id 59 | WHERE s.suite NOT IN(%s) OR c.cipher NOT IN(%s) 60 | ORDER BY h.name, po.number`, "'"+strings.Join(goodSuites, "','")+"'", "'"+strings.Join(goodCiphers, "','")+"'") // sql binding doesn't play nice with array args 61 | rows, err := database.DB().Query(query) 62 | 63 | if err != nil { 64 | return Report{}, err 65 | } 66 | 67 | defer rows.Close() 68 | 69 | report := Report{ 70 | Title: "Processes using non-approved SSL/TLS settings:", 71 | Header: []string{ 72 | "Identity", 73 | "Port", 74 | "Process Name", 75 | "Non-approved Protocol(s)", 76 | "Non-approved Cipher(s)", 77 | }, 78 | Footnote: "If this is not an internal endpoint then please check with your PM and the security team before applying this change. This change is not backwards compatible.", 79 | } 80 | 81 | type Host struct { 82 | hostname string 83 | portNumber int 84 | processName string 85 | } 86 | 87 | type cipherSuites struct { 88 | suites stringSlice 89 | ciphers stringSlice 90 | } 91 | 92 | var hostMap = map[Host]cipherSuites{} 93 | 94 | for rows.Next() { 95 | var ( 96 | hostname string 97 | processName string 98 | portNumber int 99 | suite string 100 | cipher string 101 | ) 102 | 103 | err := rows.Scan(&hostname, &portNumber, &processName, &suite, &cipher) 104 | if err != nil { 105 | return Report{}, err 106 | } 107 | 108 | host := Host{ 109 | hostname, 110 | portNumber, 111 | processName, 112 | } 113 | 114 | cs := hostMap[host] 115 | if cs.suites == nil { 116 | cs.suites = []string{} 117 | cs.ciphers = []string{} 118 | } 119 | if !goodSuites.contains(suite) && !cs.suites.contains(suite) { 120 | cs.suites = append(cs.suites, suite) 121 | } 122 | if !goodCiphers.contains(cipher) && !cs.ciphers.contains(cipher) { 123 | cs.ciphers = append(cs.ciphers, cipher) 124 | } 125 | hostMap[host] = cs 126 | } 127 | 128 | for host, cs := range hostMap { 129 | report.Rows = append(report.Rows, []string{ 130 | host.hostname, 131 | fmt.Sprintf("%d", host.portNumber), 132 | host.processName, 133 | strings.Join(cs.suites, " "), 134 | strings.Join(cs.ciphers, " "), 135 | }) 136 | } 137 | return report, nil 138 | } 139 | -------------------------------------------------------------------------------- /report/tls_violations_report_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/pivotal-cf/scantron/db" 12 | "github.com/pivotal-cf/scantron/report" 13 | ) 14 | 15 | var _ = Describe("BuildTLSViolationsReport", func() { 16 | var ( 17 | databasePath, tmpdir string 18 | database *db.Database 19 | ) 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpdir, err = ioutil.TempDir("", "report-test") 24 | Expect(err).NotTo(HaveOccurred()) 25 | databasePath = filepath.Join(tmpdir, "db.db") 26 | 27 | database, err = createTestDatabase(databasePath) 28 | Expect(err).NotTo(HaveOccurred()) 29 | }) 30 | 31 | AfterEach(func() { 32 | err := database.Close() 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = os.RemoveAll(tmpdir) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | It("shows processes using non-approved protocols or cipher suites", func() { 40 | r, err := report.BuildTLSViolationsReport(database) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(r.Title).To(Equal("Processes using non-approved SSL/TLS settings:")) 44 | 45 | Expect(r.Header).To(Equal([]string{ 46 | "Identity", 47 | "Port", 48 | "Process Name", 49 | "Non-approved Protocol(s)", 50 | "Non-approved Cipher(s)", 51 | })) 52 | 53 | Expect(r.Rows).To(HaveLen(3)) 54 | Expect(r.Rows).To(ConsistOf( 55 | []string{"host1", "7890", "command1", "VersionSSL30", ""}, 56 | []string{"host1", "8890", "command1", "", "Bad Cipher"}, 57 | []string{"host3", "7890", "command1", "VersionSSL30", "Just the worst"}, 58 | )) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /report/world_readable_files_report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import "github.com/pivotal-cf/scantron/db" 4 | 5 | func BuildWorldReadableFilesReport(database *db.Database) (Report, error) { 6 | rows, err := database.DB().Query(` 7 | SELECT DISTINCT h.name, f.path 8 | FROM hosts h 9 | JOIN files f 10 | ON h.id = f.host_id 11 | WHERE f.path LIKE "/var/vcap/data/jobs/%" 12 | AND f.permissions & 04 != 0 13 | ORDER BY h.name, f.path 14 | `) 15 | if err != nil { 16 | return Report{}, err 17 | } 18 | 19 | defer rows.Close() 20 | 21 | report := Report{ 22 | Title: "World-readable files:", 23 | Header: []string{"Identity", "Path"}, 24 | } 25 | 26 | for rows.Next() { 27 | var ( 28 | hostname string 29 | filepath string 30 | ) 31 | 32 | err := rows.Scan(&hostname, &filepath) 33 | if err != nil { 34 | return Report{}, err 35 | } 36 | 37 | report.Rows = append(report.Rows, []string{ 38 | hostname, 39 | filepath, 40 | }) 41 | } 42 | 43 | return report, nil 44 | } 45 | -------------------------------------------------------------------------------- /report/world_readable_files_report_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/pivotal-cf/scantron/db" 12 | "github.com/pivotal-cf/scantron/report" 13 | ) 14 | 15 | var _ = Describe("BuildWorldReadableFilesReport", func() { 16 | var ( 17 | databasePath, tmpdir string 18 | database *db.Database 19 | ) 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpdir, err = ioutil.TempDir("", "report-test") 24 | Expect(err).NotTo(HaveOccurred()) 25 | databasePath = filepath.Join(tmpdir, "db.db") 26 | 27 | database, err = createTestDatabase(databasePath) 28 | Expect(err).NotTo(HaveOccurred()) 29 | }) 30 | 31 | AfterEach(func() { 32 | err := database.Close() 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = os.RemoveAll(tmpdir) 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | 39 | It("shows world-readable configuration files", func() { 40 | r, err := report.BuildWorldReadableFilesReport(database) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(r.Title).To(Equal("World-readable files:")) 44 | Expect(r.Header).To(Equal([]string{"Identity", "Path"})) 45 | Expect(r.Rows).To(HaveLen(2)) 46 | Expect(r.Rows).To(Equal([][]string{ 47 | {"host1", "/var/vcap/data/jobs/world-everything"}, 48 | {"host3", "/var/vcap/data/jobs/world-readable"}, 49 | })) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /scanlog/log.go: -------------------------------------------------------------------------------- 1 | package scanlog 2 | 3 | import ( 4 | "time" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | type Logger interface { 11 | Debugf(msg string, args ...interface{}) 12 | Infof(msg string, args ...interface{}) 13 | Warnf(msg string, args ...interface{}) 14 | Errorf(msg string, args ...interface{}) 15 | 16 | With(args ...interface{}) Logger 17 | } 18 | 19 | type log struct { 20 | logger *zap.SugaredLogger 21 | } 22 | 23 | func simpleTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 24 | enc.AppendString(t.Format("2006-01-02 15:04:05")) 25 | } 26 | 27 | func NewLogger(debug bool) (Logger, error) { 28 | dyn := zap.NewAtomicLevel() 29 | 30 | if debug { 31 | dyn.SetLevel(zap.DebugLevel) 32 | } else { 33 | dyn.SetLevel(zap.InfoLevel) 34 | } 35 | 36 | config := zap.Config{ 37 | Level: dyn, 38 | Encoding: "console", 39 | DisableCaller: true, 40 | DisableStacktrace: true, 41 | EncoderConfig: zapcore.EncoderConfig{ 42 | // Keys can be anything except the empty string. 43 | TimeKey: "T", 44 | LevelKey: "L", 45 | NameKey: "N", 46 | MessageKey: "M", 47 | EncodeLevel: zapcore.CapitalColorLevelEncoder, 48 | EncodeTime: simpleTimeEncoder, 49 | EncodeDuration: zapcore.StringDurationEncoder, 50 | }, 51 | OutputPaths: []string{"stderr"}, 52 | ErrorOutputPaths: []string{"stderr"}, 53 | } 54 | 55 | logger, err := config.Build() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &log{ 61 | logger: logger.Sugar(), 62 | }, nil 63 | } 64 | 65 | func (l *log) Debugf(msg string, args ...interface{}) { 66 | l.logger.Debugf(msg, args...) 67 | } 68 | 69 | func (l *log) Infof(msg string, args ...interface{}) { 70 | l.logger.Infof(msg, args...) 71 | } 72 | 73 | func (l *log) Warnf(msg string, args ...interface{}) { 74 | l.logger.Warnf(msg, args...) 75 | } 76 | 77 | func (l *log) Errorf(msg string, args ...interface{}) { 78 | l.logger.Errorf(msg, args...) 79 | } 80 | 81 | func (l *log) With(args ...interface{}) Logger { 82 | return &log{ 83 | logger: l.logger.With(args...), 84 | } 85 | } 86 | 87 | func NewNopLogger() Logger { 88 | return &nop{} 89 | } 90 | 91 | type nop struct{} 92 | 93 | func (n *nop) Debugf(msg string, args ...interface{}) { 94 | } 95 | 96 | func (n *nop) Infof(msg string, args ...interface{}) { 97 | } 98 | 99 | func (n *nop) Warnf(msg string, args ...interface{}) { 100 | } 101 | 102 | func (n *nop) Errorf(msg string, args ...interface{}) { 103 | } 104 | 105 | func (n *nop) With(args ...interface{}) Logger { 106 | return n 107 | } 108 | -------------------------------------------------------------------------------- /scanner/bosh.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pivotal-cf/scantron" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/pivotal-cf/scantron/bosh" 10 | "github.com/pivotal-cf/scantron/scanlog" 11 | ) 12 | 13 | type boshScanner struct { 14 | deployment bosh.TargetDeployment 15 | } 16 | 17 | func Bosh(deployment bosh.TargetDeployment) Scanner { 18 | return &boshScanner{ 19 | deployment: deployment, 20 | } 21 | } 22 | 23 | func (s *boshScanner) Scan(fileRegexes *scantron.FileMatch, logger scanlog.Logger) (ScanResult, error) { 24 | vms := s.deployment.VMs() 25 | 26 | wg := &sync.WaitGroup{} 27 | wg.Add(len(vms)) 28 | 29 | hosts := make(chan JobResult) 30 | 31 | err := s.deployment.Setup() 32 | if err != nil { 33 | return ScanResult{}, err 34 | } 35 | defer s.deployment.Cleanup() 36 | 37 | for _, vm := range vms { 38 | vm := vm 39 | 40 | go func() { 41 | defer wg.Done() 42 | 43 | ip := bosh.BestAddress(vm.IPs) 44 | 45 | machineLogger := logger.With( 46 | "job", vm.JobName, 47 | "id", vm.ID, 48 | "index", index(vm.Index), 49 | "address", ip, 50 | ) 51 | 52 | remoteMachine := s.deployment.ConnectTo(vm) 53 | defer remoteMachine.Close() 54 | 55 | systemInfo, err := scanMachine(fileRegexes, machineLogger, remoteMachine) 56 | if err != nil { 57 | machineLogger.Errorf("Failed to scan machine: %s", err) 58 | return 59 | } 60 | 61 | boshName := fmt.Sprintf("%s/%s", vm.JobName, vm.ID) 62 | hosts <- buildJobResult(systemInfo, boshName, ip) 63 | }() 64 | } 65 | 66 | go func() { 67 | wg.Wait() 68 | close(hosts) 69 | }() 70 | 71 | var scannedHosts []JobResult 72 | 73 | for host := range hosts { 74 | scannedHosts = append(scannedHosts, host) 75 | } 76 | 77 | releaseResults := []ReleaseResult{} 78 | for _, release := range s.deployment.Releases() { 79 | releaseResults = append(releaseResults, ReleaseResult{Name: release.Name(), Version: release.Version().String()}) 80 | } 81 | 82 | return ScanResult{ 83 | JobResults: scannedHosts, 84 | ReleaseResults: releaseResults, 85 | }, nil 86 | } 87 | 88 | func index(index *int) string { 89 | if index == nil { 90 | return "?" 91 | } 92 | 93 | return strconv.Itoa(*index) 94 | } 95 | -------------------------------------------------------------------------------- /scanner/bosh_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "github.com/golang/mock/gomock" 8 | "github.com/pivotal-cf/scantron/bosh" 9 | "github.com/pivotal-cf/scantron/remotemachine" 10 | 11 | "github.com/cppforlife/go-semi-semantic/version" 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | 15 | boshdirector "github.com/cloudfoundry/bosh-cli/director" 16 | "github.com/cloudfoundry/bosh-cli/director/directorfakes" 17 | 18 | "github.com/pivotal-cf/scantron" 19 | "github.com/pivotal-cf/scantron/scanlog" 20 | "github.com/pivotal-cf/scantron/scanner" 21 | ) 22 | 23 | var _ = Describe("Bosh Scanning", func() { 24 | var ( 25 | mockCtrl *gomock.Controller 26 | boshScan scanner.Scanner 27 | targetDeployment *bosh.MockTargetDeployment 28 | machine *remotemachine.MockRemoteMachine 29 | 30 | vmInfo []boshdirector.VMInfo 31 | systemInfo scantron.SystemInfo 32 | 33 | release1, release2 *directorfakes.FakeRelease 34 | releaseInfo []boshdirector.Release 35 | 36 | scanResult scanner.ScanResult 37 | scanErr error 38 | logger scanlog.Logger 39 | buffer *bytes.Buffer 40 | 41 | fileMatch *scantron.FileMatch 42 | ) 43 | 44 | AfterEach(func() { 45 | mockCtrl.Finish() 46 | }) 47 | 48 | BeforeEach(func() { 49 | mockCtrl = gomock.NewController(Test) 50 | machine = remotemachine.NewMockRemoteMachine(mockCtrl) 51 | logger = scanlog.NewNopLogger() 52 | 53 | systemInfo = scantron.SystemInfo{ 54 | Processes: []scantron.Process{ 55 | { 56 | CommandName: "java", 57 | PID: 183, 58 | User: "user-name", 59 | }, 60 | }, 61 | Files: []scantron.File{ 62 | {Path: "a/path/to/the/file.txt"}, 63 | }, 64 | } 65 | fileMatch = &scantron.FileMatch{ 66 | MaxRegexFileSize: int64(1000), 67 | } 68 | 69 | buffer = &bytes.Buffer{} 70 | err := json.NewEncoder(buffer).Encode(systemInfo) 71 | Expect(err).NotTo(HaveOccurred()) 72 | 73 | machine.EXPECT().Address().Return("10.0.0.1:22").AnyTimes() 74 | machine.EXPECT().Host().Return("10.0.0.1").AnyTimes() 75 | machine.EXPECT().OSName().Return("trusty").AnyTimes() 76 | machine.EXPECT().Password().Return("password").AnyTimes() 77 | machine.EXPECT().Close().Return(nil).Times(1) 78 | 79 | targetDeployment = bosh.NewMockTargetDeployment(mockCtrl) 80 | 81 | vmInfo = []boshdirector.VMInfo{ 82 | { 83 | JobName: "service", 84 | ID: "id", 85 | IPs: []string{"10.0.0.1"}, 86 | }, 87 | } 88 | targetDeployment.EXPECT().ConnectTo(vmInfo[0]).Return(machine).Times(1) 89 | 90 | release1 = &directorfakes.FakeRelease{} 91 | release1.NameReturns("release-1") 92 | version1, err := version.NewVersionFromString("1.1.1") 93 | Expect(err).NotTo(HaveOccurred()) 94 | release1.VersionReturns(version1) 95 | 96 | release2 = &directorfakes.FakeRelease{} 97 | release2.NameReturns("release-2") 98 | version2, err := version.NewVersionFromString("2.2.2") 99 | Expect(err).NotTo(HaveOccurred()) 100 | release2.VersionReturns(version2) 101 | 102 | releaseInfo = []boshdirector.Release{release1, release2} 103 | 104 | boshScan = scanner.Bosh(targetDeployment) 105 | }) 106 | 107 | JustBeforeEach(func() { 108 | setupCall := targetDeployment.EXPECT().Setup().Times(1) 109 | targetDeployment.EXPECT().Name().Return("vm").AnyTimes() 110 | targetDeployment.EXPECT().VMs().Return(vmInfo).Times(1) 111 | targetDeployment.EXPECT().Releases().Return(releaseInfo).Times(1) 112 | targetDeployment.EXPECT().Cleanup().Times(1).After(setupCall) 113 | 114 | }) 115 | Context("when no regex specified", func() { 116 | It("cleans up the proc_scan binary after the scanning is done", func() { 117 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 118 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(buffer, nil).Times(1) 119 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 120 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 121 | }) 122 | }) 123 | 124 | Context("when regexes specified", func() { 125 | BeforeEach(func() { 126 | fileMatch.PathRegexes = []string{"interesting"} 127 | fileMatch.ContentRegexes = []string{"valuable"} 128 | }) 129 | 130 | It("uploads and cleans the proc_scan binary to the remote machine", func() { 131 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 132 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000 --path \"interesting\" --content \"valuable\"").Return(buffer, nil).Times(1) 133 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 134 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 135 | }) 136 | }) 137 | 138 | It("returns a report from the deployment", func() { 139 | 140 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 141 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(buffer, nil).Times(1) 142 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 143 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 144 | Expect(scanResult).To(Equal(scanner.ScanResult{ 145 | ReleaseResults: []scanner.ReleaseResult{ 146 | { 147 | Name: "release-1", 148 | Version: "1.1.1", 149 | }, 150 | { 151 | Name: "release-2", 152 | Version: "2.2.2", 153 | }, 154 | }, 155 | JobResults: []scanner.JobResult{ 156 | { 157 | IP: "10.0.0.1", 158 | Job: "service/id", 159 | Services: systemInfo.Processes, 160 | Files: systemInfo.Files, 161 | }, 162 | }, 163 | })) 164 | }) 165 | 166 | Context("when the vm index is nil", func() { 167 | BeforeEach(func() { 168 | vmInfo[0].Index = nil 169 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 170 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(buffer, nil).Times(1) 171 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 172 | }) 173 | 174 | It("all still works", func() { 175 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 176 | Expect(scanErr).ShouldNot(HaveOccurred()) 177 | }) 178 | }) 179 | 180 | Context("when uploading the scanning binary fails", func() { 181 | BeforeEach(func() { 182 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(errors.New("disaster")).Times(1) 183 | }) 184 | 185 | It("keeps going", func() { 186 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 187 | Expect(scanErr).NotTo(HaveOccurred()) 188 | }) 189 | }) 190 | 191 | Context("when running the scanning binary fails", func() { 192 | BeforeEach(func() { 193 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 194 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(nil, errors.New("disaster")).Times(1) 195 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 196 | }) 197 | 198 | It("keeps going", func() { 199 | scanResult, scanErr = boshScan.Scan(fileMatch, logger) 200 | Expect(scanErr).NotTo(HaveOccurred()) 201 | }) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /scanner/direct.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/pivotal-cf/scantron" 5 | "net" 6 | 7 | "github.com/pivotal-cf/scantron/remotemachine" 8 | "github.com/pivotal-cf/scantron/scanlog" 9 | ) 10 | 11 | type direct struct { 12 | machine remotemachine.RemoteMachine 13 | } 14 | 15 | func Direct(machine remotemachine.RemoteMachine) Scanner { 16 | return &direct{ 17 | machine: machine, 18 | } 19 | } 20 | 21 | func (d *direct) Scan(match *scantron.FileMatch, logger scanlog.Logger) (ScanResult, error) { 22 | hostLogger := logger.With( 23 | "host", d.machine.Address(), 24 | ) 25 | 26 | systemInfo, err := scanMachine(match, hostLogger, d.machine) 27 | if err != nil { 28 | hostLogger.Errorf("Failed to scan machine: %s", err) 29 | return ScanResult{}, err 30 | } 31 | 32 | hostname, _, err := net.SplitHostPort(d.machine.Address()) 33 | if err != nil { 34 | hostLogger.Errorf("Machine address was malformed: %s", err) 35 | return ScanResult{}, err 36 | } 37 | 38 | scannedHost := buildJobResult(systemInfo, hostname, hostname) 39 | 40 | return ScanResult{JobResults: []JobResult{scannedHost}}, nil 41 | } 42 | -------------------------------------------------------------------------------- /scanner/direct_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "github.com/golang/mock/gomock" 8 | "github.com/pivotal-cf/scantron/remotemachine" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/pivotal-cf/scantron" 14 | "github.com/pivotal-cf/scantron/scanlog" 15 | "github.com/pivotal-cf/scantron/scanner" 16 | ) 17 | 18 | var _ = Describe("Direct Scanning", func() { 19 | var ( 20 | mockCtrl *gomock.Controller 21 | directScan scanner.Scanner 22 | machine *remotemachine.MockRemoteMachine 23 | 24 | systemInfo scantron.SystemInfo 25 | 26 | scanResults scanner.ScanResult 27 | scanErr error 28 | logger scanlog.Logger 29 | buffer *bytes.Buffer 30 | 31 | fileMatch *scantron.FileMatch 32 | ) 33 | 34 | BeforeEach(func() { 35 | mockCtrl = gomock.NewController(Test) 36 | logger = scanlog.NewNopLogger() 37 | machine = remotemachine.NewMockRemoteMachine(mockCtrl) 38 | 39 | systemInfo = scantron.SystemInfo{ 40 | Processes: []scantron.Process{ 41 | { 42 | CommandName: "java", 43 | PID: 183, 44 | User: "user-name", 45 | }, 46 | }, 47 | Files: []scantron.File{ 48 | {Path: "a/path/to/the/file.txt"}, 49 | }, 50 | } 51 | 52 | fileMatch = &scantron.FileMatch{ 53 | MaxRegexFileSize: int64(1000), 54 | } 55 | 56 | buffer = &bytes.Buffer{} 57 | err := json.NewEncoder(buffer).Encode(systemInfo) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | machine.EXPECT().Address().Return("10.0.0.1:22").AnyTimes() 61 | machine.EXPECT().Host().Return("10.0.0.1").AnyTimes() 62 | machine.EXPECT().OSName().Return("trusty").AnyTimes() 63 | machine.EXPECT().Password().Return("password").AnyTimes() 64 | 65 | directScan = scanner.Direct(machine) 66 | }) 67 | 68 | AfterEach(func() { 69 | mockCtrl.Finish() 70 | }) 71 | 72 | Context("when no regex specified", func() { 73 | It("uploads and cleans the proc_scan binary to the remote machine", func() { 74 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 75 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(buffer, nil).Times(1) 76 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 77 | scanResults, scanErr = directScan.Scan(fileMatch, logger) 78 | }) 79 | }) 80 | 81 | Context("when regexes specified", func() { 82 | BeforeEach(func() { 83 | fileMatch.PathRegexes = []string{"interesting"} 84 | fileMatch.ContentRegexes = []string{"valuable"} 85 | }) 86 | 87 | It("uploads and cleans the proc_scan binary to the remote machine", func() { 88 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 89 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000 --path \"interesting\" --content \"valuable\"").Return(buffer, nil).Times(1) 90 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 91 | scanResults, scanErr = directScan.Scan(fileMatch, logger) 92 | }) 93 | }) 94 | 95 | It("returns a report from the machine", func() { 96 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 97 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(buffer, nil).Times(1) 98 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 99 | scanResults, scanErr = directScan.Scan(fileMatch, logger) 100 | Expect(scanResults.JobResults).To(Equal([]scanner.JobResult{ 101 | { 102 | IP: "10.0.0.1", 103 | Job: "10.0.0.1", 104 | Services: systemInfo.Processes, 105 | Files: systemInfo.Files, 106 | }, 107 | })) 108 | }) 109 | 110 | Context("when uploading the scanning binary fails", func() { 111 | BeforeEach(func() { 112 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(errors.New("disaster")).Times(1) 113 | }) 114 | 115 | It("fails to scan", func() { 116 | scanResults, scanErr = directScan.Scan(fileMatch, logger) 117 | Expect(scanErr).To(MatchError("disaster")) 118 | }) 119 | }) 120 | 121 | Context("when running the scanning binary fails", func() { 122 | BeforeEach(func() { 123 | machine.EXPECT().UploadFile(gomock.Any(), "./proc_scan").Return(nil).Times(1) 124 | machine.EXPECT().RunCommand("echo password | sudo -S -- ./proc_scan --context 10.0.0.1 --max 1000").Return(nil, errors.New("disaster")).Times(1) 125 | machine.EXPECT().DeleteFile("./proc_scan").Times(1) 126 | }) 127 | 128 | It("fails to scan", func() { 129 | scanResults, scanErr = directScan.Scan(fileMatch, logger) 130 | Expect(scanErr).To(MatchError("disaster")) 131 | }) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/rakyll/statik/fs" 7 | "io/ioutil" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/pivotal-cf/scantron" 13 | "github.com/pivotal-cf/scantron/remotemachine" 14 | "github.com/pivotal-cf/scantron/scanlog" 15 | _ "github.com/pivotal-cf/scantron/statik" 16 | ) 17 | 18 | type Scanner interface { 19 | Scan(*scantron.FileMatch, scanlog.Logger) (ScanResult, error) 20 | } 21 | 22 | type ScanResult struct { 23 | JobResults []JobResult 24 | ReleaseResults []ReleaseResult 25 | } 26 | 27 | type JobResult struct { 28 | IP string 29 | Job string 30 | 31 | Services []scantron.Process 32 | Files []scantron.File 33 | SSHKeys []scantron.SSHKey 34 | } 35 | 36 | type ReleaseResult struct { 37 | Name string 38 | Version string 39 | } 40 | 41 | func buildJobResult(host scantron.SystemInfo, jobName, address string) JobResult { 42 | return JobResult{ 43 | Job: jobName, 44 | IP: address, 45 | Services: host.Processes, 46 | Files: host.Files, 47 | SSHKeys: host.SSHKeys, 48 | } 49 | } 50 | 51 | func writeProcScanToTempFile(osName string) (string, error) { 52 | data_path := "/proc_scan/proc_scan_linux" 53 | if strings.Contains(osName, "windows") { 54 | data_path = "/proc_scan/proc_scan_windows" 55 | } 56 | statikFS, err := fs.New() 57 | if err != nil { 58 | return "", err 59 | } 60 | 61 | data, err := fs.ReadFile(statikFS, data_path) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | tmpFile, err := ioutil.TempFile("", "proc_scan") 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | err = ioutil.WriteFile(tmpFile.Name(), data, 0644) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | return tmpFile.Name(), nil 77 | } 78 | 79 | func scanMachine(fileRegexes *scantron.FileMatch, logger scanlog.Logger, remoteMachine remotemachine.RemoteMachine) (scantron.SystemInfo, error) { 80 | var systemInfo scantron.SystemInfo 81 | 82 | logger.Infof("Starting VM scan") 83 | defer logger.Infof("VM scan complete") 84 | 85 | osName := remoteMachine.OSName() 86 | logger.Debugf("Deployment stemcell is %s", osName) 87 | 88 | srcFilePath, err := writeProcScanToTempFile(osName) 89 | if err != nil { 90 | return systemInfo, err 91 | } 92 | defer os.Remove(srcFilePath) 93 | 94 | dstFilePath := "./proc_scan" 95 | command := fmt.Sprintf("echo %s | sudo -S -- %s", remoteMachine.Password(), dstFilePath) 96 | if strings.Contains(osName, "windows") { 97 | dstFilePath = ".\\proc_scan.exe" 98 | command = ".\\proc_scan.exe" 99 | } 100 | 101 | if scantron.Debug { 102 | command = strings.Join([]string{command, "--debug"}, " ") 103 | } 104 | command = strings.Join([]string{ 105 | command, 106 | "--context", remoteMachine.Host(), 107 | "--max", strconv.FormatInt(fileRegexes.MaxRegexFileSize, 10), 108 | }, " ") 109 | 110 | // Use escaped " since ' doesn't handle whitespace on windows 111 | for _, r := range fileRegexes.PathRegexes { 112 | command = strings.Join([]string{ 113 | command, 114 | "--path", fmt.Sprintf("\"%s\"", r), 115 | }, " ") 116 | } 117 | for _, r := range fileRegexes.ContentRegexes { 118 | command = strings.Join([]string{ 119 | command, 120 | "--content", fmt.Sprintf("\"%s\"", r), 121 | }, " ") 122 | } 123 | 124 | err = remoteMachine.UploadFile(srcFilePath, dstFilePath) 125 | if err != nil { 126 | logger.Errorf("Failed to upload scanner to remote machine: %s", err) 127 | return systemInfo, err 128 | } 129 | 130 | defer remoteMachine.DeleteFile(dstFilePath) 131 | output, err := remoteMachine.RunCommand(command) 132 | if err != nil { 133 | logger.Errorf("Failed to run scanner on remote machine: %s", err) 134 | return systemInfo, err 135 | } 136 | 137 | err = json.NewDecoder(output).Decode(&systemInfo) 138 | if err != nil { 139 | logger.Errorf("Scanner results were malformed: %s", err) 140 | return systemInfo, err 141 | } 142 | 143 | return systemInfo, nil 144 | } 145 | -------------------------------------------------------------------------------- /scanner/scanner_suite_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var Test *testing.T // GinkgoT() panics if mock expectation fails in goroutine 11 | func TestScanner(t *testing.T) { 12 | Test = t 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Scanner Suite") 15 | } 16 | -------------------------------------------------------------------------------- /scantron.go: -------------------------------------------------------------------------------- 1 | package scantron 2 | 3 | import "golang.org/x/crypto/ssh" 4 | 5 | type Host struct { 6 | Name string `yaml:"name"` 7 | Username string `yaml:"username"` 8 | Password string `yaml:"password"` 9 | Addresses []string `yaml:"addresses"` 10 | } 11 | 12 | type Inventory struct { 13 | Hosts []Host `yaml:"hosts"` 14 | } 15 | 16 | type Machine struct { 17 | Address string 18 | Username string 19 | Password string 20 | Key ssh.Signer 21 | OSName string 22 | } 23 | 24 | var Debug bool 25 | 26 | func SetDebug(debug bool) { 27 | Debug = debug 28 | } 29 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | go get github.com/rakyll/statik 6 | 7 | rm data/proc_scan/* || true 8 | rm scantron || true 9 | rm -rf statik* 10 | 11 | statik -src=data -p statik 12 | GOOS=linux GOARCH=amd64 go build -o data/proc_scan/proc_scan_linux ./cmd/proc_scan 13 | GOOS=windows GOARCH=amd64 go build -o data/proc_scan/proc_scan_windows ./cmd/proc_scan 14 | 15 | statik -src=data -p statik -f # overwrite statik.go to include the two proc_scan binaries in addition to the tls-parameters.csv 16 | GOOS=linux GOARCH=amd64 go build -o scantron ./cmd/scantron 17 | -------------------------------------------------------------------------------- /scripts/build_in_docker: -------------------------------------------------------------------------------- 1 | docker run -it -v $GOPATH/src/github.com/pivotal-cf/scantron:/go/src/github.com/pivotal-cf/scantron golang bash -c 'go get -u github.com/golang/dep/cmd/dep && export PATH=$PATH:/go/bin/ && pushd /go/src/github.com/pivotal-cf/scantron && dep ensure -v && ./scripts/build && popd' 2 | -------------------------------------------------------------------------------- /scripts/low_coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COVER="`ginkgo -r -cover`" 4 | PASS=$? 5 | 6 | if [ $PASS -ne 0 ] 7 | then 8 | echo "There are failing tests" 9 | echo "$COVER" 10 | fi 11 | 12 | LOW="`echo \"$COVER\" | grep -E 'coverage: [1-6]?[0-9]\.[0-9]%' -B1`" 13 | 14 | if [ -n "$LOW" ] 15 | then 16 | echo "Found packages with coverage < 70%:" 17 | echo "$LOW" 18 | exit 1 19 | else 20 | echo "All packages have 70% coverage" 21 | exit $PASS 22 | fi 23 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | go get github.com/onsi/ginkgo/ginkgo 6 | go get github.com/onsi/gomega/... 7 | 8 | my_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | root_dir="$( cd "${my_dir}/.." && pwd )" 10 | 11 | pushd "${root_dir}" > /dev/null 12 | ./scripts/build 13 | 14 | $GOPATH/bin/ginkgo \ 15 | -r \ 16 | -p \ 17 | -race \ 18 | -failOnPending \ 19 | -randomizeAllSpecs \ 20 | -randomizeSuites \ 21 | "$@" 22 | 23 | popd > /dev/null 24 | -------------------------------------------------------------------------------- /ssh/ssh_scanner.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "encoding/base64" 5 | "net" 6 | "strings" 7 | 8 | "github.com/pivotal-cf/scantron" 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | var keyAlgos = []string{ 13 | ssh.KeyAlgoRSA, 14 | ssh.KeyAlgoDSA, 15 | ssh.KeyAlgoECDSA256, 16 | ssh.KeyAlgoECDSA384, 17 | ssh.KeyAlgoECDSA521, 18 | ssh.KeyAlgoED25519, 19 | } 20 | 21 | func ScanSSH(host string) ([]scantron.SSHKey, error) { 22 | sshKeys := []scantron.SSHKey{} 23 | 24 | hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error { 25 | encodedKey := base64.StdEncoding.EncodeToString(key.Marshal()) 26 | sshKeys = append(sshKeys, scantron.SSHKey{ 27 | Type: key.Type(), 28 | Key: encodedKey, 29 | }) 30 | return nil 31 | } 32 | 33 | for _, keyAlgo := range keyAlgos { 34 | config := &ssh.ClientConfig{ 35 | HostKeyCallback: hostKeyCallback, 36 | HostKeyAlgorithms: []string{keyAlgo}, 37 | } 38 | 39 | client, err := ssh.Dial("tcp", host, config) 40 | 41 | if err != nil { 42 | if !strings.HasPrefix(err.Error(), "ssh:") { 43 | return nil, err 44 | } 45 | } else { 46 | client.Close() 47 | } 48 | } 49 | 50 | return sshKeys, nil 51 | } 52 | -------------------------------------------------------------------------------- /ssh/ssh_scanner_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "crypto/dsa" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "errors" 10 | "net" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | 15 | scantronssh "github.com/pivotal-cf/scantron/ssh" 16 | 17 | "github.com/onsi/gomega/gstruct" 18 | "golang.org/x/crypto/ed25519" 19 | "golang.org/x/crypto/ssh" 20 | ) 21 | 22 | var _ = Describe("SshScanner", func() { 23 | Context("with an SSH server", func() { 24 | var listener net.Listener 25 | 26 | BeforeEach(func() { 27 | var err error 28 | 29 | listener, err = net.Listen("tcp", "127.0.0.1:0") //:0 is random port 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | startSshServer(listener) 33 | }) 34 | 35 | AfterEach(func() { 36 | listener.Close() 37 | }) 38 | 39 | It("prints the key type for rsa keys", func() { 40 | address := listener.Addr().String() 41 | 42 | sshKeys, err := scantronssh.ScanSSH(address) 43 | Expect(err).NotTo(HaveOccurred()) 44 | 45 | expectedKeyTypes := []string{ 46 | "ssh-rsa", 47 | "ssh-dss", 48 | "ecdsa-sha2-nistp256", 49 | "ecdsa-sha2-nistp384", 50 | "ecdsa-sha2-nistp521", 51 | "ssh-ed25519", 52 | } 53 | 54 | for _, key := range expectedKeyTypes { 55 | Expect(sshKeys).To(ContainElement(gstruct.MatchAllFields(gstruct.Fields{ 56 | "Type": Equal(key), 57 | "Key": HavePrefix("AAAA"), 58 | }))) 59 | } 60 | }) 61 | }) 62 | }) 63 | 64 | func startSshServer(listener net.Listener) { 65 | config := &ssh.ServerConfig{ 66 | PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { 67 | return nil, errors.New("You shall not pass") 68 | }, 69 | } 70 | 71 | addKey(config, generateRsaKey()) 72 | addKey(config, generateDsaKey()) 73 | addKey(config, generateEcdsaKey(elliptic.P256())) 74 | addKey(config, generateEcdsaKey(elliptic.P384())) 75 | addKey(config, generateEcdsaKey(elliptic.P521())) 76 | addKey(config, generateEd25519Key()) 77 | 78 | go func() { 79 | defer GinkgoRecover() 80 | 81 | for { 82 | conn, err := listener.Accept() 83 | if err != nil { 84 | return 85 | } 86 | 87 | ssh.NewServerConn(conn, config) 88 | } 89 | }() 90 | } 91 | 92 | func addKey(config *ssh.ServerConfig, key interface{}) { 93 | signer, err := ssh.NewSignerFromKey(key) 94 | Expect(err).NotTo(HaveOccurred()) 95 | 96 | config.AddHostKey(signer) 97 | } 98 | 99 | func generateRsaKey() *rsa.PrivateKey { 100 | key, err := rsa.GenerateKey(rand.Reader, 1024) 101 | Expect(err).NotTo(HaveOccurred()) 102 | 103 | return key 104 | } 105 | 106 | func generateDsaKey() *dsa.PrivateKey { 107 | key := &dsa.PrivateKey{} 108 | 109 | dsa.GenerateParameters(&key.Parameters, rand.Reader, dsa.L1024N160) 110 | 111 | err := dsa.GenerateKey(key, rand.Reader) 112 | Expect(err).NotTo(HaveOccurred()) 113 | 114 | return key 115 | } 116 | 117 | func generateEcdsaKey(curve elliptic.Curve) *ecdsa.PrivateKey { 118 | key, err := ecdsa.GenerateKey(curve, rand.Reader) 119 | Expect(err).NotTo(HaveOccurred()) 120 | return key 121 | } 122 | 123 | func generateEd25519Key() ed25519.PrivateKey { 124 | _, key, err := ed25519.GenerateKey(rand.Reader) 125 | Expect(err).NotTo(HaveOccurred()) 126 | return key 127 | } 128 | -------------------------------------------------------------------------------- /ssh/ssh_suite_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestSsh(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "SSH Suite") 13 | } 14 | -------------------------------------------------------------------------------- /tlsscan/ciphers.go: -------------------------------------------------------------------------------- 1 | package tlsscan 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | _ "github.com/pivotal-cf/scantron/statik" 7 | "github.com/rakyll/statik/fs" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | VersionSSL30 = 0x0300 14 | VersionTLS10 = 0x0301 15 | VersionTLS11 = 0x0302 16 | VersionTLS12 = 0x0303 17 | ) 18 | 19 | type ProtocolVersion struct { 20 | ID uint16 21 | Name string 22 | } 23 | 24 | var ProtocolVersions = []ProtocolVersion{ 25 | {ID: VersionSSL30, Name: "VersionSSL30"}, 26 | {ID: VersionTLS10, Name: "VersionTLS10"}, 27 | {ID: VersionTLS11, Name: "VersionTLS11"}, 28 | {ID: VersionTLS12, Name: "VersionTLS12"}, 29 | } 30 | 31 | type CipherSuite struct { 32 | ID uint16 33 | Name string 34 | DTls bool 35 | Recommended bool 36 | } 37 | 38 | func BuildCipherSuites() ([]CipherSuite, error) { 39 | cs := []CipherSuite{} 40 | 41 | statikFS, err := fs.New() 42 | if err != nil { 43 | return cs, err 44 | } 45 | 46 | fileBytes, err := fs.ReadFile(statikFS, "/assets/tls-parameters.csv") 47 | if err != nil { 48 | return cs, err 49 | } 50 | 51 | r := csv.NewReader(bytes.NewReader(fileBytes)) 52 | table, err := r.ReadAll() 53 | if err != nil { 54 | return cs, err 55 | } 56 | 57 | // loop over all rows after header 58 | for i := 1; i < len(table); i++ { 59 | if table[i][2] == "" || table[i][3] == "" { 60 | continue 61 | } 62 | 63 | dtls := table[i][2] == "Y" 64 | rec := table[i][3] == "Y" 65 | cid, err := strconv.ParseUint(strings.Replace(table[i][0], ",0x", "", 1), 0, 16) 66 | if err != nil { 67 | return cs, err 68 | } 69 | 70 | cs = append(cs, CipherSuite{uint16(cid), table[i][1], dtls, rec}) 71 | } 72 | return cs, nil 73 | } 74 | -------------------------------------------------------------------------------- /tlsscan/ciphers_test.go: -------------------------------------------------------------------------------- 1 | package tlsscan_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/pivotal-cf/scantron/tlsscan" 8 | ) 9 | 10 | var _ = Describe("Ciphers", func() { 11 | Context("parsing IANA TLS parameters CSV", func() { 12 | BeforeEach(func() { 13 | 14 | }) 15 | 16 | It("returns a list of ciphers", func() { 17 | ciphers, err := tlsscan.BuildCipherSuites() 18 | 19 | Expect(err).NotTo(HaveOccurred()) 20 | Expect(ciphers).To(HaveLen(339), "have elements") 21 | Expect(ciphers).To(ContainElement(tlsscan.CipherSuite{ 22 | ID: 0x009E, 23 | Name: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 24 | DTls: true, 25 | Recommended: true, 26 | })) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tlsscan/dialer.go: -------------------------------------------------------------------------------- 1 | package tlsscan 2 | 3 | import ( 4 | "crypto/tls" 5 | "github.com/pivotal-cf/scantron/scanlog" 6 | "net" 7 | "reflect" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type timeoutError struct{} 13 | 14 | func (timeoutError) Error() string { return "tls: AttemptHandshake timed out" } 15 | func (timeoutError) Timeout() bool { return true } 16 | func (timeoutError) Temporary() bool { return true } 17 | 18 | var emptyConfig tls.Config 19 | 20 | func defaultConfig() *tls.Config { 21 | return &emptyConfig 22 | } 23 | 24 | // copied from crypto/tls/DialWithDialer, modified to immediately close the connection 25 | // return nil if cipher was negotiated successfully, even if the handshake failed in a later step (e.g. client cert validation) 26 | func AttemptHandshake(logger scanlog.Logger, dialer *net.Dialer, network, addr string, config *tls.Config) error { 27 | 28 | timeout := dialer.Timeout 29 | 30 | if !dialer.Deadline.IsZero() { 31 | deadlineTimeout := time.Until(dialer.Deadline) 32 | if timeout == 0 || deadlineTimeout < timeout { 33 | timeout = deadlineTimeout 34 | } 35 | } 36 | 37 | var errChannel chan error 38 | 39 | if timeout != 0 { 40 | errChannel = make(chan error, 2) 41 | time.AfterFunc(timeout, func() { 42 | errChannel <- timeoutError{} 43 | }) 44 | } 45 | 46 | rawConn, err := dialer.Dial(network, addr) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | colonPos := strings.LastIndex(addr, ":") 52 | if colonPos == -1 { 53 | colonPos = len(addr) 54 | } 55 | hostname := addr[:colonPos] 56 | 57 | if config == nil { 58 | config = defaultConfig() 59 | } 60 | // If no ServerName is set, infer the ServerName 61 | // from the hostname we're connecting to. 62 | if config.ServerName == "" { 63 | // Make a copy to avoid polluting argument or default. 64 | c := config.Clone() 65 | c.ServerName = hostname 66 | config = c 67 | } 68 | 69 | conn := tls.Client(rawConn, config) 70 | 71 | if timeout == 0 { 72 | err = conn.Handshake() 73 | } else { 74 | go func() { 75 | errChannel <- conn.Handshake() 76 | }() 77 | 78 | err = <-errChannel 79 | } 80 | 81 | // use reflection to check connection's ciphersuite (ConnectionStatus isn't set if handshake fails) 82 | 83 | rConn := reflect.ValueOf(conn).Elem() 84 | cipherSuite := rConn.FieldByName("cipherSuite") 85 | clientProtocol := rConn.FieldByName("clientProtocol") 86 | clientProtocolFallback := rConn.FieldByName("clientProtocolFallback") 87 | vers := rConn.FieldByName("vers") 88 | 89 | logger.Debugf("Connection State After Handshake: %d '%s' %s %d", cipherSuite, clientProtocol, clientProtocolFallback, vers) 90 | 91 | if err != nil { 92 | rawConn.Close() 93 | } else { 94 | conn.Close() 95 | } 96 | 97 | if cipherSuite.Uint() == 0 { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /tlsscan/mock_tls_scanner.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: tlsscan/tls_scanner.go 3 | 4 | // Package tlsscan is a generated GoMock package. 5 | package tlsscan 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | scantron "github.com/pivotal-cf/scantron" 10 | scanlog "github.com/pivotal-cf/scantron/scanlog" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockTlsScanner is a mock of TlsScanner interface 15 | type MockTlsScanner struct { 16 | ctrl *gomock.Controller 17 | recorder *MockTlsScannerMockRecorder 18 | } 19 | 20 | // MockTlsScannerMockRecorder is the mock recorder for MockTlsScanner 21 | type MockTlsScannerMockRecorder struct { 22 | mock *MockTlsScanner 23 | } 24 | 25 | // NewMockTlsScanner creates a new mock instance 26 | func NewMockTlsScanner(ctrl *gomock.Controller) *MockTlsScanner { 27 | mock := &MockTlsScanner{ctrl: ctrl} 28 | mock.recorder = &MockTlsScannerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockTlsScanner) EXPECT() *MockTlsScannerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Scan mocks base method 38 | func (m *MockTlsScanner) Scan(logger scanlog.Logger, host, port string) (scantron.CipherInformation, error) { 39 | ret := m.ctrl.Call(m, "Scan", logger, host, port) 40 | ret0, _ := ret[0].(scantron.CipherInformation) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // Scan indicates an expected call of Scan 46 | func (mr *MockTlsScannerMockRecorder) Scan(logger, host, port interface{}) *gomock.Call { 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockTlsScanner)(nil).Scan), logger, host, port) 48 | } 49 | 50 | // FetchTLSInformation mocks base method 51 | func (m *MockTlsScanner) FetchTLSInformation(host, port string) (*scantron.Certificate, bool, error) { 52 | ret := m.ctrl.Call(m, "FetchTLSInformation", host, port) 53 | ret0, _ := ret[0].(*scantron.Certificate) 54 | ret1, _ := ret[1].(bool) 55 | ret2, _ := ret[2].(error) 56 | return ret0, ret1, ret2 57 | } 58 | 59 | // FetchTLSInformation indicates an expected call of FetchTLSInformation 60 | func (mr *MockTlsScannerMockRecorder) FetchTLSInformation(host, port interface{}) *gomock.Call { 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTLSInformation", reflect.TypeOf((*MockTlsScanner)(nil).FetchTLSInformation), host, port) 62 | } 63 | -------------------------------------------------------------------------------- /tlsscan/tls.go: -------------------------------------------------------------------------------- 1 | package tlsscan 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "net" 11 | 12 | "github.com/pivotal-cf/scantron" 13 | ) 14 | 15 | var ErrExpectedAbort = errors.New("tls: aborting handshake") 16 | 17 | func (s *TlsScannerImpl) FetchTLSInformation(host, port string) (*scantron.Certificate, bool, error) { 18 | certs := []x509.Certificate{} 19 | mutual := false 20 | 21 | config := &tls.Config{ 22 | // We never send secret information over this TLS connection. We're just 23 | // probing it. 24 | InsecureSkipVerify: true, 25 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 26 | for _, rawCert := range rawCerts { 27 | cert, err := x509.ParseCertificate(rawCert) 28 | if err != nil { 29 | return errors.New("tls: failed to parse certificate from server: " + err.Error()) 30 | } 31 | 32 | certs = append(certs, *cert) 33 | } 34 | 35 | return nil 36 | }, 37 | GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 38 | mutual = true 39 | return nil, ErrExpectedAbort 40 | }, 41 | } 42 | 43 | hostport := net.JoinHostPort(host, port) 44 | conn, err := tls.Dial("tcp", hostport, config) 45 | if err != nil && err != ErrExpectedAbort { 46 | return nil, false, err 47 | } 48 | 49 | if conn != nil { 50 | _ = conn.Close() 51 | } 52 | 53 | // XXX: We only get the first certificate given to us. 54 | cert := certs[0] 55 | var bits int 56 | 57 | switch key := cert.PublicKey.(type) { 58 | case *rsa.PublicKey: 59 | bits = key.N.BitLen() 60 | case *ecdsa.PublicKey: 61 | bits = key.Params().BitSize 62 | default: 63 | msg := fmt.Sprintf("did not know how to convert type: %T", key) 64 | panic(msg) 65 | } 66 | 67 | certificate := &scantron.Certificate{ 68 | Bits: bits, 69 | Expiration: cert.NotAfter, 70 | Subject: scantron.CertificateSubject{ 71 | Country: singleton(cert.Subject.Country), 72 | Province: singleton(cert.Subject.Province), 73 | Locality: singleton(cert.Subject.Locality), 74 | 75 | Organization: singleton(cert.Subject.Organization), 76 | CommonName: cert.Subject.CommonName, 77 | }, 78 | } 79 | 80 | return certificate, mutual, nil 81 | } 82 | 83 | func singleton(array []string) string { 84 | if len(array) > 0 { 85 | return array[0] 86 | } 87 | 88 | return "" 89 | } 90 | -------------------------------------------------------------------------------- /tlsscan/tls_scan.go: -------------------------------------------------------------------------------- 1 | package tlsscan 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "fmt" 7 | "net" 8 | "sync" 9 | 10 | "crypto/tls" 11 | "strings" 12 | 13 | "time" 14 | 15 | "github.com/pivotal-cf/scantron" 16 | "github.com/pivotal-cf/scantron/scanlog" 17 | "golang.org/x/sync/semaphore" 18 | ) 19 | 20 | const ( 21 | maxInFlight = 20 22 | ) 23 | 24 | type result struct { 25 | version string 26 | suite string 27 | } 28 | 29 | func release(logger scanlog.Logger, sem *semaphore.Weighted, wg *sync.WaitGroup) { 30 | logger.Debugf("Releasing locks") 31 | sem.Release(1) 32 | wg.Done() 33 | } 34 | 35 | type TlsScannerImpl struct{} 36 | 37 | func (s *TlsScannerImpl) Scan(logger scanlog.Logger, host string, port string) (scantron.CipherInformation, error) { 38 | results := scantron.CipherInformation{} 39 | for _, version := range ProtocolVersions { 40 | results[version.Name] = []string{} 41 | } 42 | 43 | supportedProtocols := getSupportedProtocols(logger, host, port) 44 | if len(supportedProtocols) == 0 { 45 | logger.Debugf("Skipping cipher scan for %s:%s (no supported protocols)", host, port) 46 | return results, nil 47 | } 48 | 49 | logger.Debugf("Starting cipher scan for %s:%s", host, port) 50 | 51 | sem := semaphore.NewWeighted(maxInFlight) 52 | cipherSuites, err := BuildCipherSuites() 53 | if err != nil { 54 | return results, err 55 | } 56 | numCiphersuites := len(supportedProtocols) * len(cipherSuites) 57 | resultChan := make(chan result, maxInFlight) 58 | 59 | wg := &sync.WaitGroup{} 60 | wg.Add(numCiphersuites) 61 | 62 | go func(logger scanlog.Logger) { 63 | for _, version := range supportedProtocols { 64 | logger.Debugf("Starting TLS version %s", version.Name) 65 | for _, cipherSuite := range cipherSuites { 66 | logger.Debugf("Starting ciphersuite %s", cipherSuite.Name) 67 | scanLogger := logger.With( 68 | "host", host, 69 | "port", port, 70 | "version", version.Name, 71 | "suite", cipherSuite.Name, 72 | ) 73 | 74 | if err := sem.Acquire(context.Background(), 1); err != nil { 75 | scanLogger.Errorf("Failed to acquire lock: %q", err) 76 | } 77 | scanLogger.Debugf("Acquired lock") 78 | 79 | go testCipher(scanLogger, version, cipherSuite, sem, wg, host, port, resultChan) 80 | } 81 | } 82 | }(logger) 83 | 84 | go func(logger scanlog.Logger) { 85 | wg.Wait() 86 | logger.Debugf("Wait group done") 87 | close(resultChan) 88 | }(logger) 89 | 90 | logger.Debugf("About to start reading from channel") 91 | for res := range resultChan { 92 | logger.Debugf("Read %s %s from channel", res.version, res.suite) 93 | results[res.version] = append(results[res.version], res.suite) 94 | } 95 | 96 | logger.Debugf("Finished cipher scan for %s:%s", host, port) 97 | return results, nil 98 | } 99 | 100 | func testCipher( 101 | logger scanlog.Logger, 102 | version ProtocolVersion, 103 | cipherSuite CipherSuite, 104 | sem *semaphore.Weighted, 105 | wg *sync.WaitGroup, 106 | host string, 107 | port string, 108 | resultChan chan result) { 109 | defer release(logger, sem, wg) 110 | found, err := tryHandshakeWithCipher(logger, host, port, version, cipherSuite) 111 | if err != nil { 112 | logger.Debugf("Remote server did not respond affirmatively to request: %s", err) 113 | return 114 | } 115 | 116 | if found { 117 | logger.Debugf("Sending result") 118 | resultChan <- result{ 119 | version: version.Name, 120 | suite: cipherSuite.Name, 121 | } 122 | logger.Debugf("Result sent") 123 | } 124 | logger.Debugf("Finished ciphersuite %s", cipherSuite.Name) 125 | } 126 | 127 | func getSupportedProtocols(logger scanlog.Logger, host string, port string) []ProtocolVersion { 128 | supportedVersions := []ProtocolVersion{} 129 | for _, version := range ProtocolVersions { 130 | providesCert := false 131 | wantsCert := false 132 | config := tls.Config{ 133 | MinVersion: version.ID, 134 | MaxVersion: version.ID, 135 | InsecureSkipVerify: true, 136 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 137 | providesCert = true 138 | return nil 139 | }, 140 | GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 141 | wantsCert = true 142 | return nil, ErrExpectedAbort 143 | }, 144 | } 145 | err := AttemptHandshake(logger, &net.Dialer{Timeout: 1 * time.Second}, "tcp", fmt.Sprintf("%s:%s", host, port), &config) 146 | 147 | if providesCert { 148 | logger.Debugf("%s:%s accepts TLS (%s mutual=%t)", host, port, version.Name, wantsCert) 149 | supportedVersions = append(supportedVersions, version) 150 | } else { 151 | logger.Debugf("%s:%s refuses TLS (%s %s)", host, port, version.Name, err) 152 | } 153 | } 154 | return supportedVersions 155 | } 156 | 157 | func tryHandshakeWithCipher(logger scanlog.Logger, host string, port string, version ProtocolVersion, cipherSuite CipherSuite) (bool, error) { 158 | config := tls.Config{ 159 | MinVersion: version.ID, 160 | MaxVersion: version.ID, 161 | CipherSuites: []uint16{cipherSuite.ID}, 162 | InsecureSkipVerify: true, 163 | VerifyPeerCertificate: nil, 164 | } 165 | 166 | address := fmt.Sprintf("%s:%s", host, port) 167 | logger.Debugf("Dialing %s %s %s", address, version.Name, cipherSuite.Name) 168 | err := AttemptHandshake(logger, &net.Dialer{Timeout: 10 * time.Second}, "tcp", address, &config) 169 | 170 | if err != nil { 171 | if strings.Contains(err.Error(), "remote error") { 172 | logger.Debugf("Dialed: no tls for %s %s %s (%s)", address, version.Name, cipherSuite.Name, err) 173 | return false, nil 174 | } 175 | 176 | // TODO are these meant to be recorded in tls_scan_errors? 177 | logger.Debugf("Dialed: error for %s %s %s: %s", address, version.Name, cipherSuite.Name, err) 178 | return false, err 179 | } 180 | 181 | logger.Debugf("Dialed: tls available for %s %s %s", address, version.Name, cipherSuite.Name) 182 | return true, nil 183 | } 184 | -------------------------------------------------------------------------------- /tlsscan/tls_scan_test.go: -------------------------------------------------------------------------------- 1 | package tlsscan_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | 15 | "github.com/pivotal-cf/paraphernalia/test/certtest" 16 | "github.com/pivotal-cf/scantron/scanlog" 17 | "github.com/pivotal-cf/scantron/tlsscan" 18 | ) 19 | 20 | var _ = Describe("TLS Scan", func() { 21 | var ( 22 | server *httptest.Server 23 | logger scanlog.Logger 24 | subject *tlsscan.TlsScannerImpl 25 | ) 26 | 27 | BeforeEach(func() { 28 | // crypto/tls uses the stdlib log package 29 | log.SetOutput(GinkgoWriter) 30 | 31 | logger = scanlog.NewNopLogger() 32 | server = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | fmt.Fprintln(w, "hello?") 34 | })) 35 | 36 | subject = &tlsscan.TlsScannerImpl{} 37 | }) 38 | 39 | AfterEach(func() { 40 | server.Close() 41 | }) 42 | 43 | Context("scanning a server that supports TLS", func() { 44 | BeforeEach(func() { 45 | config := &tls.Config{ 46 | MinVersion: tls.VersionTLS10, 47 | MaxVersion: tls.VersionTLS11, // no tls 1.2 48 | CipherSuites: []uint16{ 49 | // Note: this is an old cipher that should not be used but 50 | // it it widely supported and therefore useful for this test. 51 | tls.TLS_RSA_WITH_AES_128_CBC_SHA, 52 | }, 53 | } 54 | 55 | server.TLS = config 56 | server.StartTLS() 57 | }) 58 | 59 | It("performs a scan", func() { 60 | host, port := hostport(server.URL) 61 | 62 | result, err := subject.Scan(logger, host, port) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | Expect(result.HasTLS()).To(BeTrue()) 66 | 67 | Expect(result).To(HaveKeyWithValue("VersionTLS10", []string{"TLS_RSA_WITH_AES_128_CBC_SHA"})) 68 | Expect(result).To(HaveKeyWithValue("VersionTLS11", []string{"TLS_RSA_WITH_AES_128_CBC_SHA"})) 69 | Expect(result).To(HaveKeyWithValue("VersionTLS12", []string{})) 70 | }) 71 | }) 72 | 73 | Context("scanning a server that does not support TLS", func() { 74 | BeforeEach(func() { 75 | server.Start() 76 | }) 77 | 78 | It("performs a scan", func() { 79 | host, port := hostport(server.URL) 80 | result, err := subject.Scan(logger, host, port) 81 | Expect(err).NotTo(HaveOccurred()) 82 | 83 | Expect(result.HasTLS()).To(BeFalse()) 84 | 85 | Expect(result).To(HaveKeyWithValue("VersionTLS10", []string{})) 86 | Expect(result).To(HaveKeyWithValue("VersionTLS11", []string{})) 87 | Expect(result).To(HaveKeyWithValue("VersionTLS12", []string{})) 88 | }) 89 | }) 90 | 91 | Context("scanning a server that supports mutual TLS", func() { 92 | BeforeEach(func() { 93 | ca, err := certtest.BuildCA("tlsscan") 94 | Expect(err).NotTo(HaveOccurred()) 95 | 96 | pool, err := ca.CertPool() 97 | Expect(err).NotTo(HaveOccurred()) 98 | 99 | cert, err := ca.BuildSignedCertificate("server") 100 | Expect(err).NotTo(HaveOccurred()) 101 | 102 | tlsCert, err := cert.TLSCertificate() 103 | Expect(err).NotTo(HaveOccurred()) 104 | 105 | config := &tls.Config{ 106 | MinVersion: tls.VersionTLS12, 107 | ClientAuth: tls.RequireAndVerifyClientCert, 108 | ClientCAs: pool, 109 | PreferServerCipherSuites: true, 110 | CipherSuites: []uint16{ 111 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 112 | }, 113 | CurvePreferences: []tls.CurveID{ 114 | tls.CurveP384, 115 | }, 116 | Certificates: []tls.Certificate{tlsCert}, 117 | } 118 | 119 | server.TLS = config 120 | server.StartTLS() 121 | }) 122 | 123 | It("performs a scan", func() { 124 | host, port := hostport(server.URL) 125 | 126 | result, err := subject.Scan(logger, host, port) 127 | Expect(err).NotTo(HaveOccurred()) 128 | 129 | Expect(result.HasTLS()).To(BeTrue()) 130 | 131 | Expect(result).To(HaveKeyWithValue("VersionTLS10", []string{})) 132 | Expect(result).To(HaveKeyWithValue("VersionTLS11", []string{})) 133 | Expect(result).To(HaveKeyWithValue("VersionTLS12", []string{ 134 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 135 | })) 136 | }) 137 | }) 138 | 139 | Context("when the server accepts the connection but doesn't respond to anything", func() { 140 | var ( 141 | listener net.Listener 142 | ) 143 | 144 | BeforeEach(func() { 145 | ca, err := certtest.BuildCA("tlsscan") 146 | Expect(err).NotTo(HaveOccurred()) 147 | 148 | cert, err := ca.BuildSignedCertificate("server") 149 | Expect(err).NotTo(HaveOccurred()) 150 | 151 | tlsCert, err := cert.TLSCertificate() 152 | Expect(err).NotTo(HaveOccurred()) 153 | 154 | listener, err = tls.Listen("tcp", "127.0.0.1:0", &tls.Config{ 155 | Certificates: []tls.Certificate{tlsCert}, 156 | }) 157 | Expect(err).NotTo(HaveOccurred()) 158 | }) 159 | 160 | AfterEach(func() { 161 | listener.Close() 162 | }) 163 | 164 | It("gives up but does not hang forever", func() { 165 | host, port, err := net.SplitHostPort(listener.Addr().String()) 166 | Expect(err).NotTo(HaveOccurred()) 167 | 168 | result, err := subject.Scan(logger, host, port) 169 | Expect(err).NotTo(HaveOccurred()) 170 | 171 | Expect(result.HasTLS()).To(BeFalse()) 172 | 173 | Expect(result).To(HaveKeyWithValue("VersionTLS10", []string{})) 174 | Expect(result).To(HaveKeyWithValue("VersionTLS11", []string{})) 175 | Expect(result).To(HaveKeyWithValue("VersionTLS12", []string{})) 176 | }) 177 | }) 178 | }) 179 | 180 | func hostport(uri string) (string, string) { 181 | pu, err := url.Parse(uri) 182 | Expect(err).ShouldNot(HaveOccurred()) 183 | 184 | host, port, err := net.SplitHostPort(pu.Host) 185 | Expect(err).ShouldNot(HaveOccurred()) 186 | 187 | return host, port 188 | } 189 | -------------------------------------------------------------------------------- /tlsscan/tls_scanner.go: -------------------------------------------------------------------------------- 1 | package tlsscan 2 | 3 | import ( 4 | "github.com/pivotal-cf/scantron" 5 | "github.com/pivotal-cf/scantron/scanlog" 6 | ) 7 | 8 | type TlsScanner interface { 9 | Scan(logger scanlog.Logger, host string, port string) (scantron.CipherInformation, error) 10 | FetchTLSInformation(host, port string) (*scantron.Certificate, bool, error) 11 | } 12 | -------------------------------------------------------------------------------- /tlsscan/tls_test.go: -------------------------------------------------------------------------------- 1 | package tlsscan_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "time" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/pivotal-cf/paraphernalia/secure/tlsconfig" 14 | "github.com/pivotal-cf/paraphernalia/test/certtest" 15 | "github.com/pivotal-cf/scantron" 16 | "github.com/pivotal-cf/scantron/tlsscan" 17 | ) 18 | 19 | var _ = Describe("TLS", func() { 20 | var subject *tlsscan.TlsScannerImpl 21 | BeforeEach(func() { 22 | log.SetOutput(GinkgoWriter) 23 | subject = &tlsscan.TlsScannerImpl{} 24 | }) 25 | 26 | Describe("Certificate Report", func() { 27 | var ( 28 | tlsConfig *tls.Config 29 | server *httptest.Server 30 | ) 31 | 32 | JustBeforeEach(func() { 33 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | fmt.Fprintln(w, "Hello, client") 35 | }) 36 | 37 | server = httptest.NewUnstartedServer(handler) 38 | server.TLS = tlsConfig 39 | server.StartTLS() 40 | }) 41 | 42 | AfterEach(func() { 43 | server.Close() 44 | }) 45 | 46 | Context("with standard non-mutual TLS", func() { 47 | BeforeEach(func() { 48 | ca, err := certtest.BuildCA("scantron") 49 | Expect(err).NotTo(HaveOccurred()) 50 | 51 | cert, err := ca.BuildSignedCertificate("server") 52 | Expect(err).NotTo(HaveOccurred()) 53 | 54 | tlsCert, err := cert.TLSCertificate() 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | tlsConfig = tlsconfig.Build(tlsconfig.WithIdentity(tlsCert)).Server() 58 | }) 59 | 60 | It("should show TLS certificate details", func() { 61 | host, port := hostport(server.URL) 62 | 63 | cert, mutual, err := subject.FetchTLSInformation(host, port) 64 | Expect(err).ShouldNot(HaveOccurred()) 65 | Expect(mutual).To(BeFalse()) 66 | Expect(cert).ShouldNot(BeNil()) 67 | 68 | Expect(cert.Bits).To(Equal(1024)) 69 | 70 | expectedExpiration := time.Now().AddDate(1, 0, 0) 71 | Expect(cert.Expiration).To(BeTemporally("~", expectedExpiration, time.Minute)) 72 | Expect(cert.Subject.Country).To(Equal("AQ")) 73 | Expect(cert.Subject.Province).To(Equal("Ross Island")) 74 | Expect(cert.Subject.Locality).To(Equal("McMurdo Station")) 75 | Expect(cert.Subject.Organization).To(Equal("certtest Organization")) 76 | Expect(cert.Subject.CommonName).To(Equal("server")) 77 | }) 78 | }) 79 | 80 | Context("with mutual TLS", func() { 81 | BeforeEach(func() { 82 | ca, err := certtest.BuildCA("scantron") 83 | Expect(err).NotTo(HaveOccurred()) 84 | 85 | pool, err := ca.CertPool() 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | cert, err := ca.BuildSignedCertificate("server") 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | tlsCert, err := cert.TLSCertificate() 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | tlsConfig = tlsconfig.Build( 95 | tlsconfig.WithIdentity(tlsCert), 96 | ).Server( 97 | tlsconfig.WithClientAuthentication(pool), 98 | ) 99 | }) 100 | 101 | It("should show TLS certificate details", func() { 102 | host, port := hostport(server.URL) 103 | 104 | cert, mutual, err := subject.FetchTLSInformation(host, port) 105 | Expect(err).ShouldNot(HaveOccurred()) 106 | Expect(mutual).To(BeTrue()) 107 | Expect(cert).ShouldNot(BeNil()) 108 | 109 | Expect(cert.Bits).To(Equal(1024)) 110 | 111 | expectedExpiration := time.Now().AddDate(1, 0, 0) 112 | Expect(cert.Expiration).To(BeTemporally("~", expectedExpiration, time.Minute)) 113 | Expect(cert.Subject.Country).To(Equal("AQ")) 114 | Expect(cert.Subject.Province).To(Equal("Ross Island")) 115 | Expect(cert.Subject.Locality).To(Equal("McMurdo Station")) 116 | Expect(cert.Subject.Organization).To(Equal("certtest Organization")) 117 | Expect(cert.Subject.CommonName).To(Equal("server")) 118 | }) 119 | }) 120 | }) 121 | 122 | Describe("Certificate Subject", func() { 123 | It("displays in a familiar format", func() { 124 | subject := scantron.CertificateSubject{ 125 | Country: "US", 126 | Province: "California", 127 | Locality: "San Francisco", 128 | 129 | Organization: "Pivotal", 130 | CommonName: "*.not-real.example.com", 131 | } 132 | 133 | Expect(subject.String()).To(Equal("/C=US/ST=California/L=San Francisco/O=Pivotal/CN=*.not-real.example.com")) 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /tlsscan/tlsscan_suite_test.go: -------------------------------------------------------------------------------- 1 | package tlsscan_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestTlsscan(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "TLS Scan Suite") 13 | } 14 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package scantron 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type File struct { 10 | Path string `json:"path"` 11 | Permissions os.FileMode `json:"permissions"` 12 | User string `json:"user"` 13 | Group string `json:"group"` 14 | ModifiedTime time.Time `json:"modified_time"` 15 | Size int64 `json:"size"` 16 | RegexMatches []RegexMatch `json:"regex_matches"` 17 | } 18 | 19 | type RegexMatch struct { 20 | PathRegex string `json:"path_regex"` 21 | ContentRegex string `json:"content_regex"` 22 | } 23 | 24 | type Port struct { 25 | Protocol string `json:"protocol"` 26 | Address string `json:"address"` 27 | Number int `json:"number"` 28 | ForeignAddress string `json:"foreignAddress"` 29 | ForeignNumber int `json:"foreignNumber"` 30 | State string `json:"state"` 31 | 32 | TLSInformation *TLSInformation `json:"tls_information"` 33 | } 34 | 35 | type TLSInformation struct { 36 | Certificate *Certificate `json:"certificate"` 37 | CipherInformation CipherInformation `json:"cipher_information"` 38 | Mutual bool `json:"mutual_tls"` 39 | 40 | ScanError error `json:"scan_error,omitempty"` 41 | } 42 | 43 | type FileMatch struct { 44 | PathRegexes []string `long:"path" description:"Regexes for file paths"` 45 | ContentRegexes []string `long:"content" description:"Regexes for file content"` 46 | MaxRegexFileSize int64 `long:"max" description:"Max file size to check content against regexes" default:"1048576"` // default 1 MB 47 | } 48 | 49 | type CipherInformation map[string][]string 50 | 51 | func (c CipherInformation) HasTLS() bool { 52 | for _, suites := range c { 53 | if len(suites) != 0 { 54 | return true 55 | } 56 | } 57 | 58 | return false 59 | } 60 | 61 | type Certificate struct { 62 | Expiration time.Time `json:"expiration"` 63 | Bits int `json:"bits"` 64 | Subject CertificateSubject `json:"subject"` 65 | } 66 | 67 | type CertificateSubject struct { 68 | Country string `json:"country"` 69 | Province string `json:"province"` 70 | Locality string `json:"locality"` 71 | 72 | Organization string `json:"organization"` 73 | CommonName string `json:"common_name"` 74 | } 75 | 76 | func (cs CertificateSubject) String() string { 77 | return fmt.Sprintf("/C=%s/ST=%s/L=%s/O=%s/CN=%s", cs.Country, cs.Province, cs.Locality, cs.Organization, cs.CommonName) 78 | } 79 | 80 | type Process struct { 81 | CommandName string `json:"name"` 82 | PID int `json:"pid"` 83 | User string `json:"user"` 84 | Cmdline []string `json:"cmdline"` 85 | Env []string `json:"env"` 86 | 87 | Ports []Port `json:"ports"` 88 | } 89 | 90 | type SSHKey struct { 91 | Type string `json:"type"` 92 | Key string `json:"key"` 93 | } 94 | 95 | type SystemInfo struct { 96 | Processes []Process `json:"processes"` 97 | Files []File `json:"files"` 98 | SSHKeys []SSHKey `json:"ssh_keys"` 99 | } 100 | 101 | func (p Process) HasFileWithPort(number int) bool { 102 | for _, port := range p.Ports { 103 | if number == port.Number { 104 | return true 105 | } 106 | } 107 | 108 | return false 109 | } 110 | --------------------------------------------------------------------------------