diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff4e4b2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile ~/.gitignore_global
+
+# Ignore bundler config
+/.bundle
+
+# Ignore the default SQLite database.
+/db/*.sqlite3
+
+# Ignore all logfiles and tempfiles.
+/log/*.log
+/tmp
+.DS_Store
+
+# Ignore generated files.
+/Gemfile.lock
+
+# Ignore private configuration files.
+/config/auth.yml
+/config/database.yml
+/config/initializers/secret_token.rb
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..397ebfd
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,45 @@
+source 'https://rubygems.org'
+gem 'rails', '4.0.1'
+
+# Rails 4.0 dropped this, we'll put it back for now.
+gem 'protected_attributes'
+
+# Remote workers
+gem 'sidekiq'
+gem 'slim'
+# if you require 'sinatra' you get the DSL extended to Object
+gem 'sinatra', '>= 1.3.0', :require => nil
+
+# Tags, necessary in both environments because of the helpers.
+gem 'acts-as-taggable-on'
+
+# XML parsing
+gem 'nokogiri'
+
+group :remote do
+ # Needed for the in-memory SQLite stub. Pointless, but easier than patching
+ # ActiveRecord out of everything, or Sidekiq to not load models.
+ gem 'sqlite3'
+end
+
+group :local do
+ # We'll just standardize on MySQL.
+ gem 'mysql2'
+
+ # Bootstrap
+ gem 'therubyracer'
+ gem 'less-rails' #Sprockets (what Rails 3.1 uses for its asset pipeline) supports LESS
+ gem 'twitter-bootstrap-rails', '~> 2.2.6'
+
+ # Network gems
+ gem 'netaddr'
+
+ # Pagination
+ gem 'kaminari'
+
+ gem 'jquery-rails'
+ gem 'jquery-ui-rails'
+ gem 'sass-rails'
+ gem 'coffee-rails'
+ gem 'uglifier'
+end
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000..51ae6c0
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,29 @@
+# Hacking Nepenthes
+
+# So you want to write a worker
+
+Writing another worker is designed to be easy. (It may or may not fall short, but that's the goal.) If you want to write a new worker, you'll probably want to split it into two parts: the part that actually needs to reach a server, and a part for processing the data and storing results into the database. (This separation is good because occasionally scans need to be run on a client site across a VPN, or on several remote, partially-trusted machines without access to your database.)
+
+## The setup
+
+I recommend copying `ssl_worker.rb` and `ssl_results.rb` to files for your own worker. For simplicity, name one `[type]_worker.rb` and `[type]_results.rb`, so people know where to look.
+
+## foo_worker.rb
+
+The worker you use shouldn't require any access to the database or any ActiveRecord models. Pass in any address you want to use as an actual parameter, rather than trying to synthesize it in the worker. Perform your work/scan/etc., and then pass the results back to your results worker. Be sure to send any relevant ID to your results worker, so it can update the database.
+
+## foo_results.rb
+
+Your results worker should be run on the results queue, and can interact with the database. It should process the data from the worker (if necessary), update the database, and quit. Results workers tend to be fairly quick to execute, compared to the normal worker. (Keep in mind that large netpens may have dozens or hundreds of normal worker threads running, but only one or two result worker threads running.)
+
+# Extending models
+
+Feel free to extend models as necessary, but note that both IpAddress and Port have JSON-serialized `.settings` properties, where you can stash results if necessary. This may be appropriate for extra data that will not need to be queried directly, rather than adding another column to the database. On the other hand, never underestimate the usefulness of being able to query based on the results of your worker.
+
+Most workers will likely not need their own model for results, but some may. Anything that can be assumed to always be the same for a given object (such as the SSL details on a given port, or the reverse DNS for an IP address) should be set on that object. Anything that may have multiple results for its parent should consider adding a model. For example, scans have their own model, as it is reasonable to run multiple scans on a given IP address, and screenshots have their own model, as it's possible that screenshotting subdirectories would be desirable.
+
+# Displaying results
+
+You should expose the results of your worker to the user in the user interface. For scans that operate on ports, this should be relatively straightforward: modify the `/app/views/ports/_port.html.erb` template to display your worker's results if they exist and are relevant.
+
+If your result display is likely to be large and/or not frequently accessed, you may consider hiding it in some fashion (for example, under a toggle display). You may also consider making a new page in the interface (either under an existing controller if it fits, or as a new controller), particularly if your results are likely to be large or otherwise unwieldy, or if you want to browse them in a different fashion.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f4e3f9c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+# Nepenthes
+
+# This README is actually informative. Please read it before starting to use Nepenthes.
+
+## Install
+* `brew install phantomjs coreutils` or `sudo apt-get install phantomjs`, depending on your OS.
+* `bundle install`
+* `cp config/database.yml.example config/database.yml`
+* `cp config/auth.yml.example config/auth.yml`
+* Edit config/database.yml. Set it up with MySQL or MariaDB, please. You'll be happy you did. SQLite, in particular, is problematic.
+* Edit config/auth.yml. Pick a username and password. You'll be using HTTP Basic auth, with the same username/password for everybody.
+* `echo "Nepenthes::Application.config.secret_token = \"``rake secret``\"" > config/initializers/secret_token.rb` (Note that you need backticks around "rake secret", but our GH markdown doesn't seem to tolerate double backticks well.)
+* Make sure that your database is running (MySQL - `mysql.server start` - or SQLite)
+* Edit the `config/database.yml` to have the correct connection info for your database
+* `rake db:create` to create the netpen database.
+* `rake db:migrate`
+* If you're not already running Redis, run it. (`redis-server` in a new terminal window is fine, as is running it as a daemon.) Be warned that Redis listens on all interfaces by default. Nepenthes only needs to access it via localhost, so feel free to lock down Redis' configuration.
+* `rails s`
+* In another terminal window on your local computer, run `sidekiq -c 1 -r . -q results -v`. (If you are using SQLite, you *must not* use more than one thread here. Other databases can use more, but it won't help much: this isn't a very slow step.)
+* Visit http://localhost:3000/regions
+
+If you want to run Nepenthes workers locally:
+* First, reconsider. You *should not* do this from inside a NAT if you're scanning anything public. If you're fully inside a VPN, it *might* work, but may also crash the VPN. The reasoning here is that you're running a lot of nmap scans and will quickly fill up NAT tables and related resources. Doing this will make your sysadmin sad.
+* Second, if you've decided to go for it anyway and not blame any Nepenthes authors, make sure you have local copies of whatever tools you might use. This includes nmap for scans, phantomjs for screenshots, and nikto for Nikto scans.
+* Finally, you will want to run the two `sidekiq` commands from the second below on your local machine. You may need to adjust the `~/nepenthes` directory to suit your environment. You do not need to play with editing the database.yml file, installing the bundle without the local group, or anything else. Your machine likely has more than 0.5 GB of RAM, so consider increasing the thread count if you have sufficient bandwidth to your target.
+
+For remote workers (highly recommended, VPSes are cheap and many allow scanning if you only scan ranges you have permission to scan):
+
+* Get something running *Ubuntu 12.10 or newer* (older versions have severely outdated copies of some packages, and errors will result).
+* From your local Nepenthes directory, run `rsync -a --exclude log --exclude log --exclude config/database.yml --exclude .bundle --exclude Gemfile.lock . tendril:~/nepenthes` (where `tendril` is your host, consider adding it to your `.ssh/config`)
+* `ssh -R 127.0.0.1:6379:127.0.0.1:6379 tendril` (log in to your remote VM, and forward your local Redis connection)
+* The rest is on the remote VM:
+* `sudo apt-get install ruby1.9.1 ruby1.9.1-dev libsqlite3-dev libxslt1-dev nmap phantomjs nikto`
+* `sudo gem install --no-rdoc --no-ri bundler`
+* `cd ~/nepenthes`
+* `cp config/database.yml.example config/database.yml` (you don't need to edit it this time, the in-memory SQLite3 is fine for remote workers: they don't store data.)
+* `bundle install --without local`
+* You'll probably want to run these in two `screen` windows (or at the very least, two terminal windows, as they should be run concurrently):
+* `sidekiq -e sidekiq -c 2 -r ~/nepenthes -q himem_fast -q himem_slow -v` and `sidekiq -e sidekiq -c 20 -r ~/nepenthes -q lomem_fast -q lomem_slow -v`. These settings work well for 512 MB of RAM. You can add more threads (under the -c parameter) as desired, and you will want way more of the "lomem" workers for large sets of scans. Consider getting more RAM or more VMs if this is the case, or the OS will OOM kill your nmap processes, and nothing will get done. One user noted that 150 lomem threads worked for him with 1 GB of RAM.
+
+Feel free to repeat the "for remote workers" section on as many VMs as you want. You will get more mileage out of additional RAM before you get help from multiple VMs, but multiple VMs isn't a bad thing.
+
+## Usage
+* Add a region via http://localhost:3000/regions . The start and end test times must be numbers, but don't actually matter at the moment.
+* Go to http://localhost:3000/ip_addresses and add IP addresses. You can use single IP addresses (one per line, don't comma-separate them) or ranges (192.168.1.0 - 192.168.5.255). CIDR support doesn't currently work, but I think that's a matter of changing a regex. If you want to tag all of the ranges you're entering at a time in some way (hosting facility, country, whatever), you can add tags for all of them (space-separated) in the appropriate field. To tag just addresses in a specific range, you can put them space-separated after the range, on the same line. Adding thousands of IP addresses will be a bit slow.
+
+## Scanning
+
+There is now a web interface for this. It isn't quite as configurable for some scans, but *check it out first*. That's just at http://localhost:3000/ . The instructions below assume you aren't using the web interface, so pick and choose as you go if you want more power than you can extract from the web interface.
+
+### Again, use the web interface home page first. If you need configurability that it doesn't give you, here's a way to run them manually.
+
+* `rails c`
+* Specify some nmap options, such as `opts = ['-Pn', '-p', '80,443,22,25,21,8080,23,3306,143,53', '-sV', '--version-light']`
+** Note: Spaces in the options aren't treated they way you might expect; if the commandline would be `--scan-delay 250ms`, you need to add it as `['--scan-delay', '250ms']`
+* `IpAddress.includes(:scans).where(:scans => {:ip_address_id => nil}).each {|ip| ip.queue_scan!(opts) }`
+* Wait for a bit while every IP address has a scan queued.
+* You can follow progress on http://localhost:3000/sidekiq/ .
+
+For full scans:
+
+* After you've done your lighter scans (and ideally after they've actually returned results - full scans are queued first for hosts with open ports), schedule full scans in the console: `IpAddress.queue_full_scans!`
+
+Once your scans are done:
+
+* To check whether ports are using *SSL* or not: `Port.check_all_ssl!`.
+* To get *screenshots* of applicable webpages (including on all ports), do `Port.take_all_screenshots!`. This is on the `screenshot` queue, and requires PhantomJS on the worker. (Packages exist in most OS's package managers, any recent version is fine.) `sidekiq -c [number of threads] -r . -q screenshot -v` will get it running.
+* To process the results, you'll want to keep a results processor going. `sidekiq -c 1 -r . -q results -v` if you didn't still have it running.
+
+## Results
+
+* You can view results while scans are still going on. http://localhost:3000/ports will give a listing of ports found and the number of each, you can click on a port to get a list of hosts, click on a host to get a list of ports for that host, etc.
+* There are a bunch of features lurking around that need better documentation. You can add .csv to the end of the URL for any(?) /ports page and get a .csv of the applicable hosts, ports, versions (if applicable), and such.
+* http://localhost:3000/ip_addresses.xml will give a combined XML output as if all of the scans were done in one nmap run with XML output. This is handy for importing to Nessus.
+
+## Extending
+
+You can add your own workers to Nepenthes to gather additional information, scan other things, or do whatever you need. Check out `HACKING.md` for information on writing your own worker.
+
+If you have any problems, feel free to submit an issue. Pull requests welcome.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..df88501
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,7 @@
+#!/usr/bin/env rake
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require File.expand_path('../config/application', __FILE__)
+
+Nepenthes::Application.load_tasks
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..1f9ac6c
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,3 @@
+* We should have a facility for detecting HTTP servers like we do for SSL servers. This would be useful for Nikto, as well as potentially for other services.
+
+* Full support for domain names would also be useful. For example, Nikto scans could be run on domains rather than ports. SSL scan results could be used to add domain names. We could look up IP addresses for given domains, and add those. We could brute-force subdomains (admin.client.com) and see which ones exist.
\ No newline at end of file
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 0000000..096e848
Binary files /dev/null and b/app/assets/images/favicon.png differ
diff --git a/app/assets/images/lock.png b/app/assets/images/lock.png
new file mode 100644
index 0000000..e536687
Binary files /dev/null and b/app/assets/images/lock.png differ
diff --git a/app/assets/images/rails.png b/app/assets/images/rails.png
new file mode 100644
index 0000000..d5edc04
Binary files /dev/null and b/app/assets/images/rails.png differ
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000..fae6b28
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -0,0 +1,43 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
+// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
+// GO AFTER THE REQUIRES BELOW.
+//
+//= require jquery
+//= require jquery_ujs
+//= require jquery.ui.all
+//= require twitter/bootstrap
+//= require_tree .
+
+$(function() {
+ $( "#dialog-form" ).dialog({
+ autoOpen: false,
+ height: 350,
+ width: 350,
+ modal: true,
+ buttons: {
+ "Save": function() {
+ $( "#dialog-form-tag").submit();
+ $( this ).dialog( "close" );
+},
+Cancel: function() {
+ $( this ).dialog( "close" );
+ }
+},
+close: function() {
+ }
+});
+});
+
+function show_notes_dialog(id) {
+ $( "#dialog-form-tag").attr('action', "/ports/" + id);
+ $( "#dialog-form" ).dialog( "open" );
+}
+
diff --git a/app/assets/javascripts/bootstrap.js.coffee b/app/assets/javascripts/bootstrap.js.coffee
new file mode 100644
index 0000000..c9404a8
--- /dev/null
+++ b/app/assets/javascripts/bootstrap.js.coffee
@@ -0,0 +1,4 @@
+jQuery ->
+ $("a[rel=popover]").popover()
+ $(".tooltip").tooltip()
+ $("a[rel=tooltip]").tooltip()
\ No newline at end of file
diff --git a/app/assets/javascripts/domains.js.coffee b/app/assets/javascripts/domains.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/domains.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/dynamic.js.coffee b/app/assets/javascripts/dynamic.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/dynamic.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/home.js.coffee b/app/assets/javascripts/home.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/home.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/ip_addresses.js.coffee b/app/assets/javascripts/ip_addresses.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/ip_addresses.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/ports.js.coffee b/app/assets/javascripts/ports.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/ports.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/regions.js.coffee b/app/assets/javascripts/regions.js.coffee
new file mode 100644
index 0000000..7615679
--- /dev/null
+++ b/app/assets/javascripts/regions.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 0000000..290b7aa
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -0,0 +1,14 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
+ * compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require_self
+ *= require jquery.ui.all
+ *= require_tree .
+ */
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
new file mode 100644
index 0000000..fe06a5d
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -0,0 +1,44 @@
+@import "twitter/bootstrap/bootstrap";
+@import "twitter/bootstrap/responsive";
+
+// Set the correct sprite paths
+@iconSpritePath: asset-path("twitter/bootstrap/glyphicons-halflings.png");
+@iconWhiteSpritePath: asset-path("twitter/bootstrap/glyphicons-halflings-white.png");
+
+// Set the Font Awesome (Font Awesome is default. You can disable by commenting below lines)
+// Note: If you use asset_path() here, your compiled bootstrap_and_overrides.css will not
+// have the proper paths. So for now we use the absolute path.
+@fontAwesomeEotPath: asset-path("fontawesome-webfont.eot?v=3.0.2");
+@fontAwesomeEotPath_iefix: asset-path("fontawesome-webfont.eot?#iefix&v=3.0.2");
+@fontAwesomeWoffPath: asset-path("fontawesome-webfont.woff?v=3.0.2");
+@fontAwesomeTtfPath: asset-path("fontawesome-webfont.ttf?v=3.0.2");
+
+// Font Awesome
+//@import "fontawesome";
+
+// Glyphicons
+//@import "twitter/bootstrap/sprites.less";
+
+// Your custom LESS stylesheets goes here
+//
+// Since bootstrap was imported above you have access to its mixins which
+// you may use and inherit here
+//
+// If you'd like to override bootstrap's own variables, you can do so here as well
+// See http://twitter.github.com/bootstrap/customize.html#variables for their names and documentation
+//
+// Example:
+// @linkColor: #ff0000;
+
+textarea {
+ width: 400px;
+}
+
+form {
+ margin: 0;
+}
+
+// http://stackoverflow.com/a/10688485
+.table-nonfluid {
+ width: auto;
+}
diff --git a/app/assets/stylesheets/domains.css.scss b/app/assets/stylesheets/domains.css.scss
new file mode 100644
index 0000000..11af334
--- /dev/null
+++ b/app/assets/stylesheets/domains.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Domains controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/dynamic.css.scss b/app/assets/stylesheets/dynamic.css.scss
new file mode 100644
index 0000000..8d1e775
--- /dev/null
+++ b/app/assets/stylesheets/dynamic.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the dynamic controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.scss
new file mode 100644
index 0000000..7131aac
--- /dev/null
+++ b/app/assets/stylesheets/home.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Home controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/ip_addresses.css.scss b/app/assets/stylesheets/ip_addresses.css.scss
new file mode 100644
index 0000000..cb18d51
--- /dev/null
+++ b/app/assets/stylesheets/ip_addresses.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the IpAddresses controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/ports.css.scss b/app/assets/stylesheets/ports.css.scss
new file mode 100644
index 0000000..6a2750f
--- /dev/null
+++ b/app/assets/stylesheets/ports.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Ports controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/regions.css.scss b/app/assets/stylesheets/regions.css.scss
new file mode 100644
index 0000000..2999a0a
--- /dev/null
+++ b/app/assets/stylesheets/regions.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Regions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss
new file mode 100644
index 0000000..6ec6a8f
--- /dev/null
+++ b/app/assets/stylesheets/scaffolds.css.scss
@@ -0,0 +1,69 @@
+body {
+ background-color: #fff;
+ color: #333;
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+
+p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a {
+ color: #000;
+ &:visited {
+ color: #666;
+ }
+ &:hover {
+ color: #fff;
+ background-color: #000;
+ }
+}
+
+div {
+ &.field, &.actions {
+ margin-bottom: 10px;
+ }
+}
+
+#notice {
+ color: green;
+}
+
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#error_explanation {
+ width: 450px;
+ border: 2px solid red;
+ padding: 7px;
+ padding-bottom: 0;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+ h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ margin-bottom: 0px;
+ background-color: #c00;
+ color: #fff;
+ }
+ ul li {
+ font-size: 12px;
+ list-style: square;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..0de8b54
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,24 @@
+class ApplicationController < ActionController::Base
+ protect_from_forgery
+ before_filter :require_auth
+ http_basic_authenticate_with(
+ name: AUTH_CONFIG['username'],
+ password: AUTH_CONFIG['password'])
+
+ # Using pluralize inside controllers.
+ # From http://www.dzone.com/snippets/using-helpers-inside
+ def help
+ Helper.instance
+ end
+
+ def require_auth
+ unless AUTH_CONFIG and AUTH_CONFIG['changed']
+ render text: 'You must set up config/auth.yml first.'
+ end
+ end
+
+ class Helper
+ include Singleton
+ include ActionView::Helpers::TextHelper
+ end
+end
diff --git a/app/controllers/domains_controller.rb b/app/controllers/domains_controller.rb
new file mode 100644
index 0000000..26f41e2
--- /dev/null
+++ b/app/controllers/domains_controller.rb
@@ -0,0 +1,83 @@
+class DomainsController < ApplicationController
+ # GET /domains
+ # GET /domains.json
+ def index
+ @domains = Domain.all
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.json { render json: @domains }
+ end
+ end
+
+ # GET /domains/1
+ # GET /domains/1.json
+ def show
+ @domain = Domain.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.json { render json: @domain }
+ end
+ end
+
+ # GET /domains/new
+ # GET /domains/new.json
+ def new
+ @domain = Domain.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.json { render json: @domain }
+ end
+ end
+
+ # GET /domains/1/edit
+ def edit
+ @domain = Domain.find(params[:id])
+ end
+
+ # POST /domains
+ # POST /domains.json
+ def create
+ @domain = Domain.new(params[:domain])
+
+ respond_to do |format|
+ if @domain.save
+ format.html { redirect_to @domain, notice: 'Domain was successfully created.' }
+ format.json { render json: @domain, status: :created, location: @domain }
+ else
+ format.html { render action: "new" }
+ format.json { render json: @domain.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /domains/1
+ # PUT /domains/1.json
+ def update
+ @domain = Domain.find(params[:id])
+
+ respond_to do |format|
+ if @domain.update_attributes(params[:domain])
+ format.html { redirect_to @domain, notice: 'Domain was successfully updated.' }
+ format.json { head :no_content }
+ else
+ format.html { render action: "edit" }
+ format.json { render json: @domain.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /domains/1
+ # DELETE /domains/1.json
+ def destroy
+ @domain = Domain.find(params[:id])
+ @domain.destroy
+
+ respond_to do |format|
+ format.html { redirect_to domains_url }
+ format.json { head :no_content }
+ end
+ end
+end
diff --git a/app/controllers/dynamic_controller.rb b/app/controllers/dynamic_controller.rb
new file mode 100644
index 0000000..ca216e0
--- /dev/null
+++ b/app/controllers/dynamic_controller.rb
@@ -0,0 +1,6 @@
+class DynamicController < ApplicationController
+ def screenshot
+ screenshot = Screenshot.find_by_id(params[:id])
+ send_data screenshot.data, type: 'image/png', disposition: 'inline'
+ end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
new file mode 100644
index 0000000..5c9c68e
--- /dev/null
+++ b/app/controllers/home_controller.rb
@@ -0,0 +1,36 @@
+class HomeController < ApplicationController
+ def index
+ end
+
+ def action
+ case params[:id]
+ when 'ipaddress_quick'
+ count = IpAddress.queue_quick_scans!
+ flash[:success] = "Queued quick scans for #{count} hosts."
+ when 'ipaddress_full'
+ count = IpAddress.queue_full_scans!
+ flash[:success] = "Queued full scans for #{count} hosts."
+ when 'ipaddress_hostname'
+ count = IpAddress.queue_hostname_checks!
+ flash[:success] = "Queued hostname checks for #{count} hosts."
+ when 'port_ssl'
+ count = Port.check_all_ssl!
+ flash[:success] = "Queued SSL checks for #{count} ports."
+ when 'port_screenshot'
+ count = Port.take_all_screenshots!
+ flash[:success] = "Queued screenshots for #{count} ports."
+ when 'port_nikto'
+ count = Port.queue_nikto_scans!
+ flash[:success] = "Queued Nikto scans for #{count} ports."
+ else
+ flash[:error] = 'Unknown action.'
+ end
+
+ redirect_to root_path
+ end
+
+ def screenshots
+ @screenshots = Screenshot
+ @screenshots = @screenshots.page(params[:page]).per(100)
+ end
+end
diff --git a/app/controllers/ip_addresses_controller.rb b/app/controllers/ip_addresses_controller.rb
new file mode 100644
index 0000000..776ff5b
--- /dev/null
+++ b/app/controllers/ip_addresses_controller.rb
@@ -0,0 +1,188 @@
+class IpAddressesController < ApplicationController
+ IP_REGEX = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:\/\d+)?/
+
+ # GET /ip_addresses
+ # GET /ip_addresses.json
+ def index
+ if params[:tag].present?
+ @ip_addresses = IpAddress.tagged_with(params[:tag])
+ else
+ @ip_addresses = IpAddress
+ end
+
+ @ip_addresses = @ip_addresses.order('address').page(params[:page]).per(50)
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.json { render json: @ip_addresses }
+ format.xml {
+ send_data Scan.get_all_scanned_xml,
+ :type => 'text/xml; charset=UTF-8;',
+ :disposition => 'attachment; filename=nmap.xml'
+ }
+ end
+ end
+
+ def search
+ if @ip_address = IpAddress.find_by_address(NetAddr::CIDR.create(params[:q]).to_i(:ip))
+ redirect_to ip_address_url(@ip_address)
+ else
+ flash[:error] = 'Could not find '+params[:q]
+ redirect_to ip_addresses_url
+ end
+ end
+
+ # GET /ip_addresses/1
+ # GET /ip_addresses/1.json
+ def show
+ @ip_address = IpAddress.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.json { render json: @ip_address }
+ end
+ end
+
+ # GET /ip_addresses/new
+ # GET /ip_addresses/new.json
+ def new
+ @ip_address = IpAddress.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.json { render json: @ip_address }
+ end
+ end
+
+ # GET /ip_addresses/1/edit
+ def edit
+ @ip_address = IpAddress.find(params[:id])
+ end
+
+ # POST /ip_addresses
+ # POST /ip_addresses.json
+ def create
+ @ip_address = IpAddress.new(params[:ip_address])
+
+ respond_to do |format|
+ if @ip_address.save
+ format.html { redirect_to @ip_address, notice: 'Ip address was successfully created.' }
+ format.json { render json: @ip_address, status: :created, location: @ip_address }
+ else
+ format.html { render action: "new" }
+ format.json { render json: @ip_address.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /ip_addresses/1
+ # PUT /ip_addresses/1.json
+ def update
+ @ip_address = IpAddress.find(params[:id])
+
+ respond_to do |format|
+ if @ip_address.update_attributes(params[:ip_address])
+ format.html { redirect_to @ip_address, notice: 'Ip address was successfully updated.' }
+ format.json { head :no_content }
+ else
+ format.html { render action: "edit" }
+ format.json { render json: @ip_address.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /ip_addresses/1
+ # DELETE /ip_addresses/1.json
+ def destroy
+ @ip_address = IpAddress.find(params[:id])
+ @ip_address.destroy
+
+ respond_to do |format|
+ format.html { redirect_to ip_addresses_url }
+ format.json { head :no_content }
+ end
+ end
+
+ def batch
+ @regions = Region.all
+ @region_id = params[:region_id]
+ case params[:type] || 'create'
+ when 'create'
+ @action = 'batch_create'
+ @title = 'Add IP Addresses'
+ when 'delete'
+ @action = 'batch_delete'
+ @title = 'Delete IP Addresses'
+ else
+ render :text => 'Unknown batch action.'
+ end
+ end
+
+ def batch_create
+ base_tags = params[:tags].gsub(/\s+,\s*/m, ' ').strip.split(" ")
+ base_tags.push(Region.find_by_id(params[:region_id]).name)
+ total_addresses = 0
+
+ ActiveRecord::Base.transaction do
+ text_to_ips(params[:addresses]).each do |addressAndTags|
+ newAddress = IpAddress.find_or_create_by(address: addressAndTags[0].to_i(:ip))
+ newAddress.region_id = params[:region_id]
+ newAddress.tag_list = base_tags.concat(addressAndTags[1]).join(', ')
+ newAddress.save
+ total_addresses += 1
+ end
+ end
+
+ flash[:success] = "Added/updated #{help.pluralize(total_addresses, 'address')}"
+ redirect_to batch_ip_addresses_path(:region_id => params[:region_id])
+ end
+
+ def batch_delete
+ total_addresses = 0
+
+ text_to_ips(params[:addresses]).each do |addressAndTags|
+ addr = IpAddress.find_by_address(addressAndTags[0].to_i(:ip))
+ if addr
+ addr.destroy
+ total_addresses += 1
+ end
+ end
+
+ flash[:success] = "Deleted #{help.pluralize(total_addresses, 'address')}"
+ redirect_to batch_ip_addresses_path(:type => 'delete')
+ end
+
+private
+ def text_to_ips(addresses)
+ allAddresses = []
+ addresses.each_line do |line|
+ next if line.strip == ''
+
+ parts = line.gsub(/\s+/m, ' ').strip.split(" ")
+ address_begin = parts.shift
+ unless IP_REGEX.match(address_begin)
+ flash[:error] = address_begin+' is not an IP address'
+ end
+
+ addresses = address_begin.scan(IP_REGEX)
+ address_begin = NetAddr::CIDR.create(addresses[0])
+ if addresses.length > 1
+ address_end = NetAddr::CIDR.create(addresses[1])
+ else
+ address_end = address_begin
+ end
+
+ parts.shift if parts[0] == '-'
+
+ if IP_REGEX.match(parts[0])
+ address_end = NetAddr::CIDR.create(parts.shift.scan(IP_REGEX)[0])
+ end
+
+ (address_begin..address_end).each do |address|
+ allAddresses << [address, parts]
+ end
+ end
+
+ allAddresses
+ end
+end
diff --git a/app/controllers/ports_controller.rb b/app/controllers/ports_controller.rb
new file mode 100644
index 0000000..19ed94c
--- /dev/null
+++ b/app/controllers/ports_controller.rb
@@ -0,0 +1,80 @@
+class PortsController < ApplicationController
+ def index
+ if params[:tag].present?
+ @ports = Port.tagged_with(params[:tag])
+ else
+ @ports = Port
+ end
+
+ @ports = @ports.order('number')
+ @unique_ports = @ports.group('number')
+
+ @output_array = Array.new
+ @unique_ports.each { |p|
+ data = Hash.new
+ data[:number] = p.number
+ data[:count] = @ports.where("number = ?",p.number).count
+ data[:done] = @ports.where("number = ?",p.number).count(:done)
+ @output_array << data
+ }
+
+ respond_to do |format|
+ format.html { @output_array }
+ format.json { render json: @ports }
+ format.csv { render text: Port.order('number').includes(:ip_address).to_csv }
+ end
+ end
+
+ # GET /ip_addresses/1
+ # GET /ip_addresses/1.json
+ def show
+ if params[:tag].present?
+ @ports = Port.tagged_with(params[:tag])
+ else
+ @ports = Port
+ end
+
+ if params[:todo].present?
+ @ports = @ports.where(:number => params[:id]).where("ports.done IS NULL or ports.done = 0").includes(:ip_address).order('ip_addresses.address').page(params[:page]).per(50)
+ else
+ @ports = @ports.where(:number => params[:id]).includes(:ip_address).order('ip_addresses.address').page(params[:page]).per(50)
+ end
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.json { render json: @ports }
+ format.csv { render text: @ports.to_csv }
+ end
+ end
+
+ def update
+ @port = Port.find(params[:id])
+
+ respond_to do |format|
+ if @port.update_attributes(params[:port])
+ if params[:todo].present?
+ format.html { redirect_to :back, notice: 'Port was updated sucessfully.' }
+ else
+ format.html { redirect_to :back, notice: 'Port was updated sucessfully.' }
+ end
+ else
+ format.html { render action: "edit" }
+ end
+ end
+ end
+
+ def edit
+ @port = Port.find(params[:id])
+ end
+
+ def mark_as_done
+ @port = Port.find(params[:id])
+ @port.done = @port.done ? nil : true
+ @port.save!
+ if params[:todo].present?
+ redirect_to :back, notice: 'Port marked as done.'
+ else
+ redirect_to :back, notice: 'Port marked as done.'
+ end
+ end
+end
diff --git a/app/controllers/regions_controller.rb b/app/controllers/regions_controller.rb
new file mode 100644
index 0000000..b279dd0
--- /dev/null
+++ b/app/controllers/regions_controller.rb
@@ -0,0 +1,83 @@
+class RegionsController < ApplicationController
+ # GET /regions
+ # GET /regions.json
+ def index
+ @regions = Region.all
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.json { render json: @regions }
+ end
+ end
+
+ # GET /regions/1
+ # GET /regions/1.json
+ def show
+ @region = Region.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.json { render json: @region }
+ end
+ end
+
+ # GET /regions/new
+ # GET /regions/new.json
+ def new
+ @region = Region.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.json { render json: @region }
+ end
+ end
+
+ # GET /regions/1/edit
+ def edit
+ @region = Region.find(params[:id])
+ end
+
+ # POST /regions
+ # POST /regions.json
+ def create
+ @region = Region.new(params[:region])
+
+ respond_to do |format|
+ if @region.save
+ format.html { redirect_to @region, notice: 'Region was successfully created.' }
+ format.json { render json: @region, status: :created, location: @region }
+ else
+ format.html { render action: "new" }
+ format.json { render json: @region.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /regions/1
+ # PUT /regions/1.json
+ def update
+ @region = Region.find(params[:id])
+
+ respond_to do |format|
+ if @region.update_attributes(params[:region])
+ format.html { redirect_to @region, notice: 'Region was successfully updated.' }
+ format.json { head :no_content }
+ else
+ format.html { render action: "edit" }
+ format.json { render json: @region.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /regions/1
+ # DELETE /regions/1.json
+ def destroy
+ @region = Region.find(params[:id])
+ @region.destroy
+
+ respond_to do |format|
+ format.html { redirect_to regions_url }
+ format.json { head :no_content }
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
new file mode 100644
index 0000000..860af40
--- /dev/null
+++ b/app/helpers/application_helper.rb
@@ -0,0 +1,6 @@
+module ApplicationHelper
+ def tag_escape(input)
+ # % / ? &
+ input.gsub('%', '%25').gsub('/', '%2F').gsub('?', '%3F').gsub('&', '%26').gsub('.', '%2E')
+ end
+end
diff --git a/app/helpers/domains_helper.rb b/app/helpers/domains_helper.rb
new file mode 100644
index 0000000..04b2f61
--- /dev/null
+++ b/app/helpers/domains_helper.rb
@@ -0,0 +1,2 @@
+module DomainsHelper
+end
diff --git a/app/helpers/dynamic_helper.rb b/app/helpers/dynamic_helper.rb
new file mode 100644
index 0000000..d4666ab
--- /dev/null
+++ b/app/helpers/dynamic_helper.rb
@@ -0,0 +1,2 @@
+module DynamicHelper
+end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
new file mode 100644
index 0000000..23de56a
--- /dev/null
+++ b/app/helpers/home_helper.rb
@@ -0,0 +1,2 @@
+module HomeHelper
+end
diff --git a/app/helpers/ip_addresses_helper.rb b/app/helpers/ip_addresses_helper.rb
new file mode 100644
index 0000000..0df31d0
--- /dev/null
+++ b/app/helpers/ip_addresses_helper.rb
@@ -0,0 +1,3 @@
+module IpAddressesHelper
+ include ActsAsTaggableOn::TagsHelper
+end
diff --git a/app/helpers/ports_helper.rb b/app/helpers/ports_helper.rb
new file mode 100644
index 0000000..526ed2f
--- /dev/null
+++ b/app/helpers/ports_helper.rb
@@ -0,0 +1,2 @@
+module PortsHelper
+end
diff --git a/app/helpers/regions_helper.rb b/app/helpers/regions_helper.rb
new file mode 100644
index 0000000..2a11e5d
--- /dev/null
+++ b/app/helpers/regions_helper.rb
@@ -0,0 +1,2 @@
+module RegionsHelper
+end
diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/.gitkeep b/app/models/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/domain.rb b/app/models/domain.rb
new file mode 100644
index 0000000..5f801c0
--- /dev/null
+++ b/app/models/domain.rb
@@ -0,0 +1,4 @@
+class Domain < ActiveRecord::Base
+ belongs_to :parent
+ attr_accessible :name
+end
diff --git a/app/models/ip_address.rb b/app/models/ip_address.rb
new file mode 100644
index 0000000..39223bb
--- /dev/null
+++ b/app/models/ip_address.rb
@@ -0,0 +1,95 @@
+class IpAddress < ActiveRecord::Base
+ belongs_to :region
+ belongs_to :tag
+ has_many :scans, :dependent => :destroy
+ has_many :ports, :dependent => :destroy
+ attr_accessible :address, :tags
+ acts_as_taggable
+ store :settings, coder: JSON
+
+ default_scope { order(:address) }
+
+ def address_and_hostname
+ if self.hostname.empty?
+ self.to_s
+ else
+ "#{self.to_s} (#{self.hostname})"
+ end
+ end
+
+ def address_or_hostname
+ self.hostname.empty? ? self.to_s : self.hostname
+ end
+
+ def to_s
+ NetAddr::CIDR.create(self.address.to_i).ip
+ end
+
+ def self.find_by_dotted(dotted)
+ self.find_by_address(NetAddr::CIDR.create(dotted).to_i(:ip))
+ end
+
+ def self.queue_full_scans!
+ queued = 0
+ scanlater = []
+ # We'd like this to really only queue full scans where we haven't before,
+ # but we don't store that information (yet).
+ self.where(has_full_scan: false).each do |ip|
+ if ip.ports.count > 0
+ scan = ip.scans.create
+ Sidekiq::Client.enqueue(FullScannerWorker, scan.id, ip.to_s)
+ queued += 1
+ else
+ scanlater << ip
+ end
+ end
+
+ scanlater.each do |ip|
+ scan = ip.scans.create
+ Sidekiq::Client.enqueue(FullScannerWorker, scan.id, ip.to_s)
+ queued += 1
+ end
+
+ queued
+ end
+
+ def self.queue_quick_scans!
+ queued = 0
+ self.includes(:scans).where(:scans => {:ip_address_id => nil}).each do |ip|
+ ip.queue_scan!
+ queued += 1
+ end
+
+ queued
+ end
+
+ def queue_scan!(opts = ['-Pn', '-p', '80,443,22,25,21,8080,23,3306,143,53',
+ '-sV', '--version-light'])
+ # Recommend something like the above.
+ unless opts.kind_of?(Array)
+ throw 'opts must be an array.'
+ end
+
+ scan = self.scans.new(:options => opts)
+ scan.save!
+ Sidekiq::Client.enqueue(ScannerWorker, scan.id, self.to_s, opts)
+ end
+
+ def self.not_hostname_checked
+ self.where(hostname: nil)
+ end
+
+ def self.queue_hostname_checks!
+ queued = 0
+ self.not_hostname_checked.each do |ip|
+ ip.queue_check_hostname!
+ queued += 1
+ end
+
+ queued
+ end
+
+ def queue_check_hostname!
+ Sidekiq::Client.enqueue(HostnameWorker, self.id, self.to_s)
+ end
+end
diff --git a/app/models/port.rb b/app/models/port.rb
new file mode 100644
index 0000000..27dae90
--- /dev/null
+++ b/app/models/port.rb
@@ -0,0 +1,154 @@
+class Port < ActiveRecord::Base
+ belongs_to :ip_address
+ belongs_to :scan
+ has_many :screenshots, as: :screenshotable, dependent: :destroy
+ attr_accessible :number, :product, :version, :extra, :notes, :done
+ acts_as_taggable
+ store :settings, coder: JSON
+
+ # We try to avoid sending things to these ports (screenshots, SSL handshakes,
+ # etc.) because it may cause problems. For example, printers printing
+ # data of some sort.
+ AVOID_PORTS = [9100]
+
+ SCREENSHOT_PRIORITY_PORTS = []
+ (0..65535).step(1000).each do |base|
+ SCREENSHOT_PRIORITY_PORTS.concat [base + 80, base + 443]
+ end
+ SCREENSHOT_PRIORITY_PORTS.concat [
+ 8081, 8082,
+ 3000, 3001, 3002,
+ ]
+
+ def queue_http_title_scan!
+ Sidekiq::Client.enqueue(HttpTitleWorker, self.id, self.ip_address.to_s, self.number)
+ end
+
+ def product_str
+ if self.product
+ if self.version
+ "#{self.product} (#{self.version})"
+ else
+ self.product
+ end
+ else
+ ''
+ end
+ end
+
+ def probably_ssl?
+ self.ssl or (self.ssl == nil and self.number % 1000 == 443)
+ end
+
+ def take_screenshot!(path = '/')
+ protocol = 'http'
+ if self.probably_ssl?
+ protocol = 'https'
+ end
+ url = "#{protocol}://#{self.ip_address.to_s}:#{self.number}#{path}"
+
+ existing = self.screenshots.where(url: url)
+ if existing.count > 0
+ return existing.first
+ end
+
+ screenshot = self.screenshots.new(url: url)
+ screenshot.save!
+ screenshot.take!
+ screenshot
+ end
+
+ def check_ssl!
+ Sidekiq::Client.enqueue(SslWorker, self.id, self.ip_address.to_s,
+ self.number)
+ end
+
+ def check_nikto!
+ Sidekiq::Client.enqueue(NiktoWorker, self.id, self.ip_address.to_s,
+ self.number, self.probably_ssl?)
+ end
+
+ def self.not_screenshotted(force_avoided_ports = false)
+ return self.where(screenshotted: false) if force_avoided_ports
+ self.where(screenshotted: false).where('number NOT IN (?)', AVOID_PORTS)
+ end
+
+ def self.not_ssl_checked(force_avoided_ports = false)
+ return self.where(ssl: nil) if force_avoided_ports
+ self.where(ssl: nil).where('number NOT IN (?)', AVOID_PORTS)
+ end
+
+ def self.not_nikto_scanned(force_avoided_ports = false)
+ return self.where(nikto_results: nil).where(
+ 'number IN (631,8000,8008,8009,8888) OR MOD(number,1000) IN (80,81,82,88,443)') if force_avoided_ports
+ self.where(nikto_results: nil).where(
+ 'number IN (631,8000,8008,8009,8888) OR MOD(number,1000) IN (80,81,82,88,443)').where('number NOT IN (?)', AVOID_PORTS)
+ end
+
+ def self.take_all_screenshots!(force_avoided_ports = false)
+ queued = 0
+ # Do the priority ones first
+ self.not_screenshotted(force_avoided_ports).
+ where('number IN (?)', SCREENSHOT_PRIORITY_PORTS).each do |port|
+ port.take_screenshot!
+ queued += 1
+ end
+
+ # And the rest later
+ self.not_screenshotted(force_avoided_ports).
+ where('number NOT IN (?)', SCREENSHOT_PRIORITY_PORTS).each do |port|
+ port.take_screenshot!
+ queued += 1
+ end
+
+ queued
+ end
+
+ def self.check_all_ssl!(force_avoided_ports = false)
+ queued = 0
+ self.not_ssl_checked(force_avoided_ports).each do |port|
+ port.check_ssl!
+ queued += 1
+ end
+
+ queued
+ end
+
+ def self.queue_nikto_scans!(force_avoided_ports = false)
+ queued = 0
+ self.not_nikto_scanned(force_avoided_ports).each do |port|
+ port.check_nikto!
+ queued += 1
+ end
+
+ queued
+ end
+
+ def self.to_csv(grouped = false)
+ CSV.generate do |csv|
+ if grouped
+ csv << ['port', 'count']
+ all.each do |port|
+ csv << [
+ port.number,
+ port.count
+ ]
+ end
+ else
+ csv << ['host', 'port', 'region', 'product', 'version', 'extra']
+ all.each do |port|
+ if port.ip_address
+ csv << [
+ port.ip_address.to_s,
+ port.number,
+ port.ip_address.region.name,
+ port.product,
+ port.version,
+ port.extra,
+ ]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/region.rb b/app/models/region.rb
new file mode 100644
index 0000000..134ac4c
--- /dev/null
+++ b/app/models/region.rb
@@ -0,0 +1,3 @@
+class Region < ActiveRecord::Base
+ attr_accessible :name, :utc_end_test, :utc_start_test
+end
diff --git a/app/models/scan.rb b/app/models/scan.rb
new file mode 100644
index 0000000..a11d706
--- /dev/null
+++ b/app/models/scan.rb
@@ -0,0 +1,65 @@
+class Scan < ActiveRecord::Base
+ belongs_to :ip_address
+ attr_accessible :results, :options
+ serialize :options
+ has_many :ports
+
+ def process!
+ return if self.processed
+
+ doc = Nokogiri::XML(self.results)
+ unless doc.at('host/address/@addr')
+ Sidekiq::Client.enqueue(FullScannerWorker, self.id, self.ip_address.to_s)
+ return
+ end
+
+ ip_address = IpAddress.find_by_dotted(doc.at('host/address/@addr').value)
+ empty_ports = []
+ have_ports = false
+
+ doc.xpath('//port').each do |port|
+ if port.at('state/@state').value == 'open'
+ portRow = ip_address.ports.find_or_create_by_number(port['portid'])
+ portRow.scan = self
+ if port.at('service/@product')
+ portRow.product = port.at('service/@product').value
+ portRow.tag_list << portRow.product
+ end
+ if port.at('service/@version')
+ portRow.version = port.at('service/@version').value
+ portRow.tag_list << portRow.version
+ end
+ if port.at('service/@extrainfo')
+ portRow.extra = port.at('service/@extrainfo').value
+ end
+ portRow.save
+ else
+ empty_ports << port['portid']
+ end
+ end
+
+ ip_address.ports.where(:number => empty_ports).destroy_all
+ self.processed = true
+ self.save
+ end
+
+ def get_host_xml
+ return unless self.processed
+
+ doc = Nokogiri::XML(self.results)
+ doc.xpath('//host')[0]
+ end
+
+ def self.get_all_scanned_xml
+ template = Nokogiri::XML(Scan.where(:processed => true).first.results)
+
+ scans = Scan.where(:processed => true)
+ template.xpath('//hosts').remove
+ hosts = template.xpath('//nmaprun')[0]
+ scans.where(:processed => true).find_each do |scan|
+ hosts << scan.get_host_xml
+ end
+
+ template.to_xml
+ end
+end
diff --git a/app/models/screenshot.rb b/app/models/screenshot.rb
new file mode 100644
index 0000000..ca957aa
--- /dev/null
+++ b/app/models/screenshot.rb
@@ -0,0 +1,10 @@
+class Screenshot < ActiveRecord::Base
+ attr_accessible :url, :data
+ belongs_to :screenshotable, polymorphic: true
+
+ def take!
+ return if self.data
+
+ Sidekiq::Client.enqueue(ScreenshotWorker, self.id, self.url)
+ end
+end
diff --git a/app/views/domains/_form.html.erb b/app/views/domains/_form.html.erb
new file mode 100644
index 0000000..6c00bb9
--- /dev/null
+++ b/app/views/domains/_form.html.erb
@@ -0,0 +1,20 @@
+<%= form_for @domain, :html => { :class => 'form-horizontal' } do |f| %>
+
+ <%= f.label :name, :class => 'control-label' %>
+
+ <%= f.text_field :name, :class => 'text_field' %>
+
+
+
+ <%= f.label :parent_id, :class => 'control-label' %>
+
+ <%= f.number_field :parent_id, :class => 'number_field' %>
+
+
+
+
+ <%= f.submit nil, :class => 'btn btn-primary' %>
+ <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
+ domains_path, :class => 'btn' %>
+
+<% end %>
diff --git a/app/views/domains/edit.html.erb b/app/views/domains/edit.html.erb
new file mode 100644
index 0000000..6bc4385
--- /dev/null
+++ b/app/views/domains/edit.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = Domain -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/domains/index.html.erb b/app/views/domains/index.html.erb
new file mode 100644
index 0000000..9fef7cb
--- /dev/null
+++ b/app/views/domains/index.html.erb
@@ -0,0 +1,38 @@
+<%- model_class = Domain -%>
+
+
+
+
+ <%= model_class.human_attribute_name(:id) %> |
+ <%= model_class.human_attribute_name(:name) %> |
+ <%= model_class.human_attribute_name(:parent_id) %> |
+ <%= model_class.human_attribute_name(:created_at) %> |
+ <%=t '.actions', :default => t("helpers.actions") %> |
+
+
+
+ <% @domains.each do |domain| %>
+
+ <%= link_to domain.id, domain_path(domain) %> |
+ <%= domain.name %> |
+ <%= domain.parent_id %> |
+ <%=l domain.created_at %> |
+
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_domain_path(domain), :class => 'btn btn-mini' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ domain_path(domain),
+ :method => :delete,
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-mini btn-danger' %>
+ |
+
+ <% end %>
+
+
+
+<%= link_to t('.new', :default => t("helpers.links.new")),
+ new_domain_path,
+ :class => 'btn btn-primary' %>
diff --git a/app/views/domains/new.html.erb b/app/views/domains/new.html.erb
new file mode 100644
index 0000000..480fc94
--- /dev/null
+++ b/app/views/domains/new.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = Domain -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/domains/show.html.erb b/app/views/domains/show.html.erb
new file mode 100644
index 0000000..eac6345
--- /dev/null
+++ b/app/views/domains/show.html.erb
@@ -0,0 +1,23 @@
+<%- model_class = Domain -%>
+
+
+
+ - <%= model_class.human_attribute_name(:name) %>:
+ - <%= @domain.name %>
+ - <%= model_class.human_attribute_name(:parent_id) %>:
+ - <%= @domain.parent_id %>
+
+
+
+ <%= link_to t('.back', :default => t("helpers.links.back")),
+ domains_path, :class => 'btn' %>
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_domain_path(@domain), :class => 'btn' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ domain_path(@domain),
+ :method => 'delete',
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-danger' %>
+
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb
new file mode 100644
index 0000000..4813eea
--- /dev/null
+++ b/app/views/home/index.html.erb
@@ -0,0 +1,157 @@
+
+
+
+ Nepenthes is currently tracking <%= pluralize IpAddress.count, 'host' %>
+ with <%= pluralize Port.count, 'open port' %> known.
+ There are currently <%= pluralize (Sidekiq::Workers.new.size), 'busy worker' %>.
+
+
+
+
+ Queue |
+ Details |
+
+ <%
+ queue_length = {}
+ [['lomem_slow', 'Low-memory, slow'],
+ ['lomem_fast', 'Low-memory, fast'],
+ ['himem_slow', 'High-memory, slow'],
+ ['himem_fast', 'High-memory, fast'],
+ ['results', 'Results']].each do |queue_name, desc|
+ queue = Sidekiq::Queue.new(queue_name)
+ queue_length[queue_name] = queue.size %>
+
+ <%= desc %> |
+
+ <% if queue_length[queue_name].zero? %>
+ Empty
+ <% else %>
+ <%= pluralize queue_length[queue_name], 'queued job' %>,
+ dating back to <%= time_ago_in_words (queue.latency).ago %> ago.
+ <% end %>
+ |
+
+ <% end %>
+
+
+Actions
+The actions listed below are not all that can be done in Nepenthes. Check out the readme or look at the models to see what else can be done. You may also benefit from pulling up a Rails console to perform your own queries.
+
+Actions listed here are disabled if they make little sense. They are also colored by their impact on worker resources and length of time to complete.
+Uncolored actions take you elsewhere and don't perform an action immediately.
+Green actions should complete quickly.
+Yellow actions may take a few minutes (or longer for large numbers of hosts).
+Red actions may take hours, depending on the number of hosts or ports you scan.
+
+IP Addresses
+<%
+total_addresses = IpAddress.count
+missing_scan = IpAddress.includes(:scans).where(:scans => {:ip_address_id => nil}).count
+missing_full = IpAddress.where(has_full_scan: false).count
+%>
+
+
+ Status |
+ Actions |
+
+
+
+ <%= pluralize total_addresses, 'IP address' %> total
+ |
+
+ <%= link_to 'Add More',
+ batch_ip_addresses_path,
+ class: 'btn btn-mini' %>
+ <%= link_to_unless total_addresses.zero?,
+ content_tag(:span,
+ 'Remove Some',
+ class: 'btn btn-mini',
+ disabled: total_addresses.zero?),
+ batch_ip_addresses_path(:type => 'delete') %>
+ |
+
+
+
+ <%= missing_scan %> without scheduled scans
+ |
+
+ <%= button_to 'Queue Quick Scans',
+ {:action => 'action', :id => 'ipaddress_quick'},
+ class: 'btn btn-mini btn-warning',
+ disabled: missing_scan.zero? %>
+ |
+
+
+
+ <%= missing_full %> without full scan results
+ |
+
+ <%= button_to 'Queue Full Scans',
+ {:action => 'action', :id => 'ipaddress_full'},
+ class: 'btn btn-mini btn-danger',
+ disabled: missing_full.zero?,
+ confirm: queue_length['lomem_slow'].zero? ? false : "Are you sure? There are #{pluralize queue_length['lomem_slow'], 'job'} in the queue, which may include full scans." %>
+ |
+
+
+
+Ports
+<%
+missing_hostname = IpAddress.not_hostname_checked.count
+missing_ssl = Port.not_ssl_checked.count
+missing_screenshots = Port.not_screenshotted.count
+missing_nikto = Port.not_nikto_scanned.count
+%>
+
+
+ Status |
+ Actions |
+
+
+
+ <%= missing_hostname %> without hostnames checked
+ |
+
+ <%= button_to 'Queue Hostname Checks',
+ {:action => 'action', :id => 'ipaddress_hostname'},
+ class: 'btn btn-mini btn-success',
+ disabled: missing_hostname.zero? %>
+ |
+
+
+
+ <%= missing_ssl %> without SSL checked
+ |
+
+ <%= button_to 'Queue SSL Checks',
+ {:action => 'action', :id => 'port_ssl'},
+ class: 'btn btn-mini btn-success',
+ disabled: missing_ssl.zero? %>
+ |
+
+
+
+ <%= missing_screenshots %> without <%= link_to 'screenshot', screenshots_path %> attempts
+ |
+
+ <%= button_to 'Queue Screenshots',
+ {:action => 'action', :id => 'port_screenshot'},
+ class: 'btn btn-mini btn-warning',
+ disabled: missing_screenshots.zero?,
+ confirm: (missing_ssl.zero? ? false : 'Are you sure? You might want to wait until the SSL checks are done for best results.') %>
+ |
+
+
+
+ <%= missing_nikto %> without Nikto scans
+ |
+
+ <%= button_to 'Queue Nikto Scans',
+ {:action => 'action', :id => 'port_nikto'},
+ class: 'btn btn-mini btn-danger',
+ disabled: missing_nikto.zero? %>
+ |
+
+
diff --git a/app/views/home/screenshots.html.erb b/app/views/home/screenshots.html.erb
new file mode 100644
index 0000000..29b368a
--- /dev/null
+++ b/app/views/home/screenshots.html.erb
@@ -0,0 +1,6 @@
+
+<%= paginate @screenshots %>
+<%= render partial: 'ports/ports', object: @screenshots.map(&:screenshotable), locals: {show: :both} %>
+<%= paginate @screenshots %>
diff --git a/app/views/ip_addresses/_form.html.erb b/app/views/ip_addresses/_form.html.erb
new file mode 100644
index 0000000..d344886
--- /dev/null
+++ b/app/views/ip_addresses/_form.html.erb
@@ -0,0 +1,32 @@
+<%= form_for @ip_address, :html => { :class => 'form-horizontal' } do |f| %>
+
+ <%= f.label :address, :class => 'control-label' %>
+
+ <%= f.text_field :address, :class => 'text_field' %>
+
+
+
+ <%= f.label :tags, :class => 'control-label' %>
+
+ <%= f.text_field :tags, :class => 'text_field' %>
+
+
+
+ <%= f.label :region_id, :class => 'control-label' %>
+
+ <%= f.number_field :region_id, :class => 'number_field' %>
+
+
+
+ <%= f.label :tag_id, :class => 'control-label' %>
+
+ <%= f.number_field :tag_id, :class => 'number_field' %>
+
+
+
+
+ <%= f.submit nil, :class => 'btn btn-primary' %>
+ <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
+ ip_addresses_path, :class => 'btn' %>
+
+<% end %>
diff --git a/app/views/ip_addresses/batch.html.erb b/app/views/ip_addresses/batch.html.erb
new file mode 100644
index 0000000..c63fd84
--- /dev/null
+++ b/app/views/ip_addresses/batch.html.erb
@@ -0,0 +1,27 @@
+<%= @title %>
+<%= form_tag @action, :class => 'form-horizontal' do %>
+
+ <%= label_tag :addresses, 'IP Addresses', :class => 'control-label' %>
+
+ <%= text_area_tag :addresses, '', :class => 'text_area', :rows => 7 %>
+
+
+
+ <%= label_tag :tags, 'Tags', :class => 'control-label' %>
+
+ <%= text_field_tag :tags, '', :class => 'text_field' %>
+
+
+
+ <%= label_tag :region_id, 'Region', :class => 'control-label' %>
+
+ <%= select_tag :region_id, options_from_collection_for_select(@regions, "id", "name", @region_id), :class => 'number_field' %>
+
+
+
+
+ <%= submit_tag nil, :class => 'btn btn-primary' %>
+ <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
+ ip_addresses_path, :class => 'btn' %>
+
+<% end %>
diff --git a/app/views/ip_addresses/edit.html.erb b/app/views/ip_addresses/edit.html.erb
new file mode 100644
index 0000000..1565629
--- /dev/null
+++ b/app/views/ip_addresses/edit.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = IpAddress -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/ip_addresses/index.html.erb b/app/views/ip_addresses/index.html.erb
new file mode 100644
index 0000000..40ea038
--- /dev/null
+++ b/app/views/ip_addresses/index.html.erb
@@ -0,0 +1,62 @@
+<%- model_class = IpAddress -%>
+
+
+<%= form_tag search_ip_addresses_path, :class => 'form-horizontal' do %>
+
+ <%= label_tag :q, 'Search for IP Address', :class => 'control-label' %>
+
+ <%= text_field_tag :q, '', :class => 'text_field' %>
+ <%= submit_tag nil, :class => 'btn btn-primary' %>
+
+
+<% end %>
+
+<%= paginate @ip_addresses %>
+
+
+
+ <%= model_class.human_attribute_name(:address) %> |
+ Tags |
+ Open Ports |
+ <%=t '.actions', :default => t("helpers.actions") %> |
+
+
+
+ <% @ip_addresses.each do |ip_address| %>
+
+ <%= link_to ip_address.address_and_hostname, ip_address_path(ip_address) %> |
+
+ <% if ip_address.tags.any? %>
+ <% ip_address.tags.each do |tag| %>
+ <%= link_to tag.name, tagged_ip_addresses_url(:tag => tag_escape(tag.name)) %>
+ <% end %>
+ <% end %>
+ |
+
+ <%= ip_address.ports.count %>
+ |
+
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_ip_address_path(ip_address), :class => 'btn btn-mini' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ ip_address_path(ip_address),
+ :method => :delete,
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-mini btn-danger' %>
+ |
+
+ <% end %>
+
+
+
+<%= paginate @ip_addresses %>
+
+<%= link_to 'Bulk Add',
+ batch_ip_addresses_path,
+ :class => 'btn' %>
+
+<%= link_to 'Bulk Delete',
+ batch_ip_addresses_path(:type => 'delete'),
+ :class => 'btn btn-danger' %>
diff --git a/app/views/ip_addresses/new.html.erb b/app/views/ip_addresses/new.html.erb
new file mode 100644
index 0000000..0668437
--- /dev/null
+++ b/app/views/ip_addresses/new.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = IpAddress -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/ip_addresses/show.html.erb b/app/views/ip_addresses/show.html.erb
new file mode 100644
index 0000000..4e58b8b
--- /dev/null
+++ b/app/views/ip_addresses/show.html.erb
@@ -0,0 +1,36 @@
+<%- model_class = IpAddress -%>
+
+
+
+ - <%= model_class.human_attribute_name(:tags) %>:
+ -
+ <% if @ip_address.tags.any? %>
+ <% @ip_address.tags.each do |tag| %>
+ <%= link_to tag.name, tagged_ip_addresses_url(:tag => tag.name) %>
+ <% end %>
+ <% end %>
+
+ - <%= model_class.human_attribute_name(:region_id) %>:
+ - <%= @ip_address.region.name %>
+
+
+Open ports
+<% if @ip_address.ports.any? %>
+ <%= render partial: 'ports/ports', object: @ip_address.ports, locals: {show: :port} %>
+<% else %>
+ No open ports found (yet)!
+<% end %>
+
+
+ <%= link_to t('.back', :default => t("helpers.links.back")),
+ ip_addresses_path, :class => 'btn' %>
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_ip_address_path(@ip_address), :class => 'btn' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ ip_address_path(@ip_address),
+ :method => 'delete',
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-danger' %>
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
new file mode 100644
index 0000000..0ac062e
--- /dev/null
+++ b/app/views/layouts/application.html.erb
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ <%= content_for?(:title) ? yield(:title) : "Nepenthes" %>
+ <%= csrf_meta_tags %>
+
+
+
+
+ <%= stylesheet_link_tag "application", :media => "all" %>
+ <%= favicon_link_tag 'favicon.png', :rel => 'shortcut icon' %>
+
+
+
+
+
+
+
+
+
+
+
+
Nepenthes
+
+
+ - <%= link_to 'IP Addresses', ip_addresses_path %>
+ - <%= link_to 'Ports', ports_path %>
+ - <%= link_to 'Regions', regions_path %>
+ - <%= link_to 'Screenshots', screenshots_path %>
+
+
+
+
+
+
+
+
+
+ <%= bootstrap_flash %>
+ <%= yield %>
+
+
+
+
+
+
+
+
+
+ <%= javascript_include_tag "application" %>
+
+
+
diff --git a/app/views/ports/_form.html.erb b/app/views/ports/_form.html.erb
new file mode 100644
index 0000000..9776af8
--- /dev/null
+++ b/app/views/ports/_form.html.erb
@@ -0,0 +1,32 @@
+<%= form_for @port, :html => { :class => 'form-horizontal' } do |f| %>
+
+ <%= f.label :number, :class => 'control-label' %>
+
+ <%= f.text_field :number, :class => 'text_field', :disabled => "disabled" %>
+
+
+
+ <%= f.label :ip_address, :class => 'control-label' %>
+
+ <%= f.text_field :ip_address, :class => 'text_field', :disabled => "disabled" %>
+
+
+
+ <%= f.label :notes, :class => 'control-label' %>
+
+ <%= f.text_area :notes, :class => 'text_area' %>
+
+
+
+ <%= f.label :done, :class => 'control-label' %>
+
+ <%= f.check_box :done, :class => 'check_box' %>
+
+
+
+
+ <%= f.submit nil, :class => 'btn btn-primary' %>
+ <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
+ port_path, :class => 'btn' %>
+
+<% end %>
diff --git a/app/views/ports/_port.html.erb b/app/views/ports/_port.html.erb
new file mode 100644
index 0000000..89953ec
--- /dev/null
+++ b/app/views/ports/_port.html.erb
@@ -0,0 +1,51 @@
+
+
+ <% if show == :port %>
+ <%= link_to port.number, port_path(port.number) %>
+ <% elsif show == :address %>
+ <%= link_to port.ip_address.address_or_hostname, ip_address_path(port.ip_address) %>
+ <% else %>
+ <%= link_to port.ip_address.address_or_hostname, ip_address_path(port.ip_address) %>:<%= link_to port.number, port_path(port.number) %>
+ <% end %>
+ |
+
+ <% if port.ssl == nil %>
+ " target="_blank">http
+ " target="_blank">https
+ <% elsif port.ssl == false %>
+ " target="_blank">http
+ <% else %>
+ " target="_blank">https
+ <% end %>
+ |
+
+ <% if port.ip_address.tags.any? %>
+ <% port.ip_address.tags.each do |tag| %>
+ <%= link_to tag.name, tagged_ip_addresses_url(:tag => tag_escape(tag.name)) %>
+ <% end %>
+ <% end %>
+ |
+
+ <% if port.product %>
+ <%= link_to port.product_str, tagged_ports_url(tag_escape(port.product)) %>
+ <% end %>
+ |
+
+ <%= port.extra %>
+ <% unless port.screenshots.where('length(data) > 0').empty?
+ ss = port.screenshots.where('length(data) > 0').first %>
+ <%= link_to (image_tag screenshot_path(ss.id), width: 200, height: 150), ss.url, target: '_blank' %>
+ <% end %>
+ |
+ <%= simple_format(h(port.notes)) %> |
+ <%= simple_format(h(port.nikto_results)) %> |
+ <%= port.done %> |
+
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_port_path(port), :class => 'btn btn-mini' %>
+ <%= link_to t('.note', :default => t("helpers.links.done")),
+ mark_as_done_port_path(port, :todo => params[:todo] ), :class => 'btn btn-mini', :method => :post %>
+ Quick Update
+
+ |
+
diff --git a/app/views/ports/_ports.html.erb b/app/views/ports/_ports.html.erb
new file mode 100644
index 0000000..a8f1506
--- /dev/null
+++ b/app/views/ports/_ports.html.erb
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ <%= show == :port ? 'Port' : 'IP Address' %> |
+ Links |
+ Host Tags |
+ Product |
+ Extra |
+ Notes |
+ Nikto Results |
+ Done |
+
+
+
+ <%= render partial: 'ports/port', collection: ports, locals: {show: show} %>
+
+
diff --git a/app/views/ports/edit.html.erb b/app/views/ports/edit.html.erb
new file mode 100644
index 0000000..61bac3a
--- /dev/null
+++ b/app/views/ports/edit.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = Port -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/ports/index.html.erb b/app/views/ports/index.html.erb
new file mode 100644
index 0000000..10c34de
--- /dev/null
+++ b/app/views/ports/index.html.erb
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Port |
+ # of Hosts |
+ # inspected |
+
+
+
+ <% @output_array.each do |port| %>
+
+ <%= link_to port[:number],
+ (params[:tag].present? ? tagged_port_path(:id => port[:number], :tag => tag_escape(params[:tag])) : port_path(port[:number])) %> |
+ <%= port[:count] %> |
+ <%= port[:done] %> |
+
+ <% end %>
+
+
diff --git a/app/views/ports/show.html.erb b/app/views/ports/show.html.erb
new file mode 100644
index 0000000..3716592
--- /dev/null
+++ b/app/views/ports/show.html.erb
@@ -0,0 +1,16 @@
+
+
+
+<%= paginate @ports %>
+<%= render partial: 'ports', object: @ports, locals: {show: :address} %>
+<%= paginate @ports %>
diff --git a/app/views/regions/_form.html.erb b/app/views/regions/_form.html.erb
new file mode 100644
index 0000000..8598e01
--- /dev/null
+++ b/app/views/regions/_form.html.erb
@@ -0,0 +1,26 @@
+<%= form_for @region, :html => { :class => 'form-horizontal' } do |f| %>
+
+ <%= f.label :name, :class => 'control-label' %>
+
+ <%= f.text_field :name, :class => 'text_field' %>
+
+
+
+ <%= f.label :utc_start_test, :class => 'control-label' %>
+
+ <%= f.text_field :utc_start_test, :class => 'text_field' %>
+
+
+
+ <%= f.label :utc_end_test, :class => 'control-label' %>
+
+ <%= f.text_field :utc_end_test, :class => 'text_field' %>
+
+
+
+
+ <%= f.submit nil, :class => 'btn btn-primary' %>
+ <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
+ regions_path, :class => 'btn' %>
+
+<% end %>
diff --git a/app/views/regions/edit.html.erb b/app/views/regions/edit.html.erb
new file mode 100644
index 0000000..e59e348
--- /dev/null
+++ b/app/views/regions/edit.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = Region -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/regions/index.html.erb b/app/views/regions/index.html.erb
new file mode 100644
index 0000000..9de1112
--- /dev/null
+++ b/app/views/regions/index.html.erb
@@ -0,0 +1,40 @@
+<%- model_class = Region -%>
+
+
+
+
+ <%= model_class.human_attribute_name(:id) %> |
+ <%= model_class.human_attribute_name(:name) %> |
+ <%= model_class.human_attribute_name(:utc_start_test) %> |
+ <%= model_class.human_attribute_name(:utc_end_test) %> |
+ <%= model_class.human_attribute_name(:created_at) %> |
+ <%=t '.actions', :default => t("helpers.actions") %> |
+
+
+
+ <% @regions.each do |region| %>
+
+ <%= link_to region.id, region_path(region) %> |
+ <%= region.name %> |
+ <%= region.utc_start_test %> |
+ <%= region.utc_end_test %> |
+ <%=l region.created_at %> |
+
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_region_path(region), :class => 'btn btn-mini' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ region_path(region),
+ :method => :delete,
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-mini btn-danger' %>
+ |
+
+ <% end %>
+
+
+
+<%= link_to t('.new', :default => t("helpers.links.new")),
+ new_region_path,
+ :class => 'btn btn-primary' %>
diff --git a/app/views/regions/new.html.erb b/app/views/regions/new.html.erb
new file mode 100644
index 0000000..adb232c
--- /dev/null
+++ b/app/views/regions/new.html.erb
@@ -0,0 +1,5 @@
+<%- model_class = Region -%>
+
+<%= render :partial => 'form' %>
diff --git a/app/views/regions/show.html.erb b/app/views/regions/show.html.erb
new file mode 100644
index 0000000..201d6cc
--- /dev/null
+++ b/app/views/regions/show.html.erb
@@ -0,0 +1,25 @@
+<%- model_class = Region -%>
+
+
+
+ - <%= model_class.human_attribute_name(:name) %>:
+ - <%= @region.name %>
+ - <%= model_class.human_attribute_name(:utc_start_test) %>:
+ - <%= @region.utc_start_test %>
+ - <%= model_class.human_attribute_name(:utc_end_test) %>:
+ - <%= @region.utc_end_test %>
+
+
+
+ <%= link_to t('.back', :default => t("helpers.links.back")),
+ regions_path, :class => 'btn' %>
+ <%= link_to t('.edit', :default => t("helpers.links.edit")),
+ edit_region_path(@region), :class => 'btn' %>
+ <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
+ region_path(@region),
+ :method => 'delete',
+ :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
+ :class => 'btn btn-danger' %>
+
diff --git a/app/workers/full_scanner_worker.rb b/app/workers/full_scanner_worker.rb
new file mode 100644
index 0000000..6436c85
--- /dev/null
+++ b/app/workers/full_scanner_worker.rb
@@ -0,0 +1,18 @@
+require 'open3'
+
+class FullScannerWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :lomem_slow
+
+ def perform(id, host)
+ full_options = ['nmap', '-Pn', '-oX', '-', '-p-', '--max-rtt-timeout=500ms',
+ '--max-retries=2', '--host-timeout=30m', '-v', '-sV', '--version-light', host]
+ stdout_str, status = Open3.send(:capture2, *full_options)
+ if status == 0
+ Sidekiq::Client.enqueue(ScannerResults, id, stdout_str, true)
+ else
+ # nmap didn't finish properly (probably killed), try again later.
+ Sidekiq::Client.enqueue(FullScannerWorker, id, host)
+ end
+ end
+end
diff --git a/app/workers/hostname_results.rb b/app/workers/hostname_results.rb
new file mode 100644
index 0000000..6013d4c
--- /dev/null
+++ b/app/workers/hostname_results.rb
@@ -0,0 +1,11 @@
+class HostnameResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, hostname_data)
+ ip_address = IpAddress.find_by_id(id)
+ return unless ip_address
+ ip_address.hostname = hostname_data
+ ip_address.save!
+ end
+end
diff --git a/app/workers/hostname_worker.rb b/app/workers/hostname_worker.rb
new file mode 100644
index 0000000..4c46e19
--- /dev/null
+++ b/app/workers/hostname_worker.rb
@@ -0,0 +1,15 @@
+require 'resolv'
+
+class HostnameWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :lomem_fast
+
+ def perform(id, host)
+ begin
+ hostname_data = Resolv.getname(host)
+ rescue Resolv::ResolvError
+ hostname_data = ''
+ end
+ Sidekiq::Client.enqueue(HostnameResults, id, hostname_data)
+ end
+end
diff --git a/app/workers/http_title_results.rb b/app/workers/http_title_results.rb
new file mode 100644
index 0000000..c195af8
--- /dev/null
+++ b/app/workers/http_title_results.rb
@@ -0,0 +1,12 @@
+class HttpTitleResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, title)
+ puts id
+ puts title
+ port = Port.find_by_id(id)
+ port.extra = "#{port.extra} \n #{title}"
+ port.save!
+ end
+end
diff --git a/app/workers/http_title_worker.rb b/app/workers/http_title_worker.rb
new file mode 100644
index 0000000..19f758f
--- /dev/null
+++ b/app/workers/http_title_worker.rb
@@ -0,0 +1,19 @@
+require 'nokogiri'
+require 'net/http'
+
+class HttpTitleWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :lomem_fast
+
+ def perform(id, ip, port)
+ title = ""
+ protocol = "http://"
+ if port == 443
+ protocol = "https://"
+ end
+
+ html_doc = Nokogiri::HTML(open("#{protocol}#{ip}:#{port}"))
+ title = html_doc.title
+ Sidekiq::Client.enqueue(HttpTitleResults, id, title)
+ end
+end
diff --git a/app/workers/nikto_results.rb b/app/workers/nikto_results.rb
new file mode 100644
index 0000000..cc98c1c
--- /dev/null
+++ b/app/workers/nikto_results.rb
@@ -0,0 +1,12 @@
+require 'base64'
+
+class NiktoResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, nikto_data)
+ port = Port.find_by_id(id)
+ port.nikto_results = nikto_data
+ port.save!
+ end
+end
diff --git a/app/workers/nikto_worker.rb b/app/workers/nikto_worker.rb
new file mode 100644
index 0000000..de45c7a
--- /dev/null
+++ b/app/workers/nikto_worker.rb
@@ -0,0 +1,13 @@
+require 'open3'
+
+class NiktoWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :himem_slow
+
+ def perform(id, host, port, ssl)
+ nikto_data, status = Open3.capture2($NIKTO_PATH,
+ '-host', "#{host}", '-port', "#{port}", '-C', 'all', ssl ? '-ssl' : '-nossl')
+
+ Sidekiq::Client.enqueue(NiktoResults, id, nikto_data)
+ end
+end
diff --git a/app/workers/scanner_results.rb b/app/workers/scanner_results.rb
new file mode 100644
index 0000000..86ec7ff
--- /dev/null
+++ b/app/workers/scanner_results.rb
@@ -0,0 +1,16 @@
+class ScannerResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, results, full)
+ scan = Scan.find_by_id(id)
+ scan.results = results
+ scan.save!
+ scan.process!
+ if full
+ ip = scan.ip_address
+ ip.has_full_scan = true
+ ip.save!
+ end
+ end
+end
diff --git a/app/workers/scanner_worker.rb b/app/workers/scanner_worker.rb
new file mode 100644
index 0000000..917da58
--- /dev/null
+++ b/app/workers/scanner_worker.rb
@@ -0,0 +1,17 @@
+require 'open3'
+
+class ScannerWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :lomem_slow
+
+ def perform(id, host, opts)
+ full_options = ['nmap', '-oX', '-', opts, host].flatten
+ stdout_str, status = Open3.send(:capture2, *full_options)
+ if status == 0
+ Sidekiq::Client.enqueue(ScannerResults, id, stdout_str, false)
+ else
+ # nmap didn't finish properly (probably killed), try again later.
+ Sidekiq::Client.enqueue(ScannerWorker, id, host, opts)
+ end
+ end
+end
diff --git a/app/workers/screenshot/get_screenshot.js b/app/workers/screenshot/get_screenshot.js
new file mode 100644
index 0000000..6b8edda
--- /dev/null
+++ b/app/workers/screenshot/get_screenshot.js
@@ -0,0 +1,33 @@
+var page = require('webpage').create(),
+ system = require('system'),
+ address, output, size;
+
+if (system.args.length != 2) {
+ console.log('Usage: get_screenshot.js URL');
+ phantom.exit(1);
+} else {
+ address = system.args[1];
+ page.viewportSize = { width: 800, height: 600 };
+
+ // Squelch these so they don't interefere with our image output.
+ page.onError = function (msg) {};
+
+ page.open(address, function (status) {
+ if (status !== 'success') {
+ console.log('failed');
+ phantom.exit();
+ }
+
+ page.evaluate(function() {
+ if (document.body.bgColor == '' &&
+ document.body.style.backgroundColor == '') {
+ document.body.bgColor = 'white';
+ }
+ });
+
+ window.setTimeout(function () {
+ console.log(page.renderBase64('PNG'));
+ phantom.exit();
+ }, 2000);
+ });
+}
diff --git a/app/workers/screenshot_results.rb b/app/workers/screenshot_results.rb
new file mode 100644
index 0000000..f1efe85
--- /dev/null
+++ b/app/workers/screenshot_results.rb
@@ -0,0 +1,27 @@
+require 'base64'
+
+class ScreenshotResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, encoded_image)
+ screenshot = Screenshot.find_by_id(id)
+ return unless screenshot
+
+ if screenshot.screenshotable.is_a?(Port)
+ port = screenshot.screenshotable
+ port.screenshotted = true
+ port.save
+ end
+
+ if encoded_image.strip == 'failed' or encoded_image == ''
+ # If we failed to take the screenshot, we'll just delete it to avoid
+ # having it show up in other pages.
+ screenshot.destroy
+ else
+ image = Base64.decode64(encoded_image)
+ screenshot.data = image
+ screenshot.save!
+ end
+ end
+end
diff --git a/app/workers/screenshot_worker.rb b/app/workers/screenshot_worker.rb
new file mode 100644
index 0000000..1e48408
--- /dev/null
+++ b/app/workers/screenshot_worker.rb
@@ -0,0 +1,16 @@
+require 'open3'
+
+$SCREENSHOT_SCRIPT_PATH = File.dirname(__FILE__)+'/screenshot/get_screenshot.js'
+
+class ScreenshotWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :himem_fast
+
+ def perform(id, url)
+ encoded_image, status = Open3.capture2($TIMEOUT_PATH, '20',
+ $PHANTOMJS_PATH, '--ignore-ssl-errors=yes', $SCREENSHOT_SCRIPT_PATH,
+ url)
+
+ Sidekiq::Client.enqueue(ScreenshotResults, id, encoded_image)
+ end
+end
diff --git a/app/workers/ssl_results.rb b/app/workers/ssl_results.rb
new file mode 100644
index 0000000..5557a49
--- /dev/null
+++ b/app/workers/ssl_results.rb
@@ -0,0 +1,28 @@
+require 'openssl'
+
+CERT_REGEX = /-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----/
+
+class SslResults
+ include Sidekiq::Worker
+ sidekiq_options :queue => :results
+
+ def perform(id, ssl_data)
+ port = Port.find_by_id(id)
+ return unless port
+ port.settings['ssl_details'] = ssl_data
+ port.ssl = ssl_data.include?('SSL-Session')
+
+ if cert_match = ssl_data.match(CERT_REGEX)
+ cert = OpenSSL::X509::Certificate.new(cert_match[0])
+ port.notes ||= ''
+ # We add a space after slashes to wrap long lines.
+ port.notes += "SSL For: #{cert.subject.to_s.gsub('/', '/ ')}\n"
+ cert.extensions.each do |extension|
+ if extension.oid == 'subjectAltName'
+ port.notes += "#{extension.value.gsub(', ', "\n")}\n"
+ end
+ end
+ end
+ port.save!
+ end
+end
diff --git a/app/workers/ssl_worker.rb b/app/workers/ssl_worker.rb
new file mode 100644
index 0000000..8aa6bf3
--- /dev/null
+++ b/app/workers/ssl_worker.rb
@@ -0,0 +1,15 @@
+require 'open3'
+
+class SslWorker
+ include Sidekiq::Worker
+ sidekiq_options :queue => :lomem_fast
+
+ def perform(id, host, port)
+ if $TIMEOUT_PATH == nil
+ raise 'Please install coreutils.'
+ end
+ ssl_data, status = Open3.capture2($TIMEOUT_PATH, '2', $OPENSSL_PATH,
+ 's_client', '-connect', "#{host}:#{port}", '-showcerts')
+ Sidekiq::Client.enqueue(SslResults, id, ssl_data)
+ end
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..d6a7b98
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Nepenthes::Application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..966e07d
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,65 @@
+require File.expand_path('../boot', __FILE__)
+
+require 'csv'
+require 'rails/all'
+
+if defined?(Bundler)
+ # If you precompile assets before deploying to production, use this line
+ Bundler.require(*Rails.groups(:assets => %w(development test)))
+ # If you want your assets lazily compiled in production, use this line
+ # Bundler.require(:default, :assets, Rails.env)
+ Bundler.require(:default)
+ Bundler.require(:local) unless Rails.env.sidekiq?
+end
+
+module Nepenthes
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+
+ # Custom directories with classes and modules you want to be autoloadable.
+ # config.autoload_paths += %W(#{config.root}/extras)
+
+ # Only load the plugins named here, in the order given (default is alphabetical).
+ # :all can be used as a placeholder for all plugins not explicitly named.
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+
+ # Activate observers that should always be running.
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+ # config.time_zone = 'Central Time (US & Canada)'
+
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
+
+ # Configure the default encoding used in templates for Ruby 1.9.
+ config.encoding = "utf-8"
+
+ # Configure sensitive parameters which will be filtered from the log file.
+ config.filter_parameters += [:password]
+
+ # Enable escaping HTML in JSON.
+ config.active_support.escape_html_entities_in_json = true
+
+ # Use SQL instead of Active Record's schema dumper when creating the database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Enforce whitelist mode for mass assignment.
+ # This will create an empty whitelist of attributes available for mass-assignment for all models
+ # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
+ # parameters by using an attr_accessible or attr_protected declaration.
+ config.active_record.whitelist_attributes = true
+
+ # Enable the asset pipeline
+ config.assets.enabled = true
+
+ # Version of your assets, change this if you want to expire all your assets
+ config.assets.version = '1.0'
+ end
+end
diff --git a/config/auth.yml.example b/config/auth.yml.example
new file mode 100644
index 0000000..59decf3
--- /dev/null
+++ b/config/auth.yml.example
@@ -0,0 +1,3 @@
+username: changeme
+password: changeme
+changed: false # Change this to true when you set a password.
\ No newline at end of file
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..4489e58
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,6 @@
+require 'rubygems'
+
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
diff --git a/config/database.yml.example b/config/database.yml.example
new file mode 100644
index 0000000..002f0a4
--- /dev/null
+++ b/config/database.yml.example
@@ -0,0 +1,45 @@
+# SQLite version 3.x
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+# development:
+# adapter: sqlite3
+# database: db/development.sqlite3
+# pool: 5
+# timeout: 5000
+development:
+ adapter: mysql2
+ encoding: utf8
+ database: netpen
+ username: root
+ password:
+
+sidekiq:
+ adapter: sqlite3
+ database: ":memory:"
+ verbosity: quiet
+
+# If you prefer:
+# development:
+# adapter: mysql2
+# encoding: utf8
+# database: netpen
+# username: netpen
+# password: somepassword
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: mysql2
+ encoding: utf8
+ database: netpen
+ username: root
+ password:
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..e73f0c6
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the rails application
+require File.expand_path('../application', __FILE__)
+
+# Initialize the rails application
+Nepenthes::Application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..96bb559
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,33 @@
+Nepenthes::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send
+ config.action_mailer.raise_delivery_errors = false
+
+ # Print deprecation notices to the Rails logger
+ config.active_support.deprecation = :log
+
+ # Only use best-standards-support built into browsers
+ config.action_dispatch.best_standards_support = :builtin
+
+ # Raise exception on mass assignment protection for Active Record models
+ config.active_record.mass_assignment_sanitizer = :strict
+
+ # Do not compress assets
+ config.assets.compress = false
+
+ # Expands the lines which load the assets
+ config.assets.debug = true
+
+ # Rails 4.0, yay
+ config.eager_load = false
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..42d242a
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,70 @@
+Nepenthes::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # Code is not reloaded between requests
+ config.cache_classes = true
+
+ # Full error reports are disabled and caching is turned on
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Disable Rails's static asset server (Apache or nginx will already do this)
+ config.serve_static_assets = false
+
+ # Compress JavaScripts and CSS
+ config.assets.compress = true
+
+ # Don't fallback to assets pipeline if a precompiled asset is missed
+ config.assets.compile = false
+
+ # Generate digests for assets URLs
+ config.assets.digest = true
+
+ # Defaults to nil and saved in location specified by config.assets.prefix
+ # config.assets.manifest = YOUR_PATH
+
+ # Specifies the header that your server uses for sending files
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # See everything in the log (default is :info)
+ # config.log_level = :debug
+
+ # Prepend all log lines with the following tags
+ # config.log_tags = [ :subdomain, :uuid ]
+
+ # Use a different logger for distributed setups
+ # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
+
+ # Use a different cache store in production
+ # config.cache_store = :mem_cache_store
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server
+ # config.action_controller.asset_host = "http://assets.example.com"
+
+ # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
+ # config.assets.precompile += %w( search.js )
+
+ # Disable delivery errors, bad email addresses will be ignored
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable threaded mode
+ # config.threadsafe!
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation can not be found)
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners
+ config.active_support.deprecation = :notify
+
+ # Log the query plan for queries taking more than this (works
+ # with SQLite, MySQL, and PostgreSQL)
+ # config.active_record.auto_explain_threshold_in_seconds = 0.5
+
+ # Rails 4.0, yay
+ config.eager_load = true
+end
diff --git a/config/environments/sidekiq.rb b/config/environments/sidekiq.rb
new file mode 100644
index 0000000..444dce9
--- /dev/null
+++ b/config/environments/sidekiq.rb
@@ -0,0 +1,28 @@
+Nepenthes::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # Code is not reloaded between requests
+ config.cache_classes = true
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send
+ config.action_mailer.raise_delivery_errors = false
+
+ # Print deprecation notices to the Rails logger
+ config.active_support.deprecation = :log
+
+ # Only use best-standards-support built into browsers
+ config.action_dispatch.best_standards_support = :builtin
+
+ # Do not compress assets
+ config.assets.compress = false
+
+ # Expands the lines which load the assets
+ config.assets.debug = true
+
+ # Rails 4.0, yay
+ config.eager_load = false
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..9ef9836
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,37 @@
+Nepenthes::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Configure static asset server for tests with Cache-Control for performance
+ config.serve_static_assets = true
+ config.static_cache_control = "public, max-age=3600"
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Raise exception on mass assignment protection for Active Record models
+ config.active_record.mass_assignment_sanitizer = :strict
+
+ # Print deprecation notices to the stderr
+ config.active_support.deprecation = :stderr
+
+ # Rails 4.0, yay
+ config.eager_load = false
+end
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000..59385cd
--- /dev/null
+++ b/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/config/initializers/commands.rb b/config/initializers/commands.rb
new file mode 100644
index 0000000..d8ec20a
--- /dev/null
+++ b/config/initializers/commands.rb
@@ -0,0 +1,19 @@
+# http://stackoverflow.com/a/5471032
+def which(cmd)
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
+ exts.each { |ext|
+ exe = File.join(path, "#{cmd}#{ext}")
+ return exe if File.executable? exe
+ }
+ end
+ return nil
+end
+
+$TIMEOUT_PATH = which 'timeout'
+if $TIMEOUT_PATH == nil
+ $TIMEOUT_PATH = which 'gtimeout'
+end
+$OPENSSL_PATH = which 'openssl'
+$PHANTOMJS_PATH = which 'phantomjs'
+$NIKTO_PATH = which 'nikto'
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..5d8d9be
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,15 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+#
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/config/initializers/load_auth.rb b/config/initializers/load_auth.rb
new file mode 100644
index 0000000..d113bd5
--- /dev/null
+++ b/config/initializers/load_auth.rb
@@ -0,0 +1,8 @@
+config_path = "#{Rails.root.to_s}/config/auth.yml"
+
+config = false
+if File.exists?(config_path)
+ config = YAML.load_file(config_path)
+end
+
+AUTH_CONFIG = config
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
new file mode 100644
index 0000000..72aca7e
--- /dev/null
+++ b/config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
+# Mime::Type.register_alias "text/html", :iphone
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
new file mode 100644
index 0000000..d82f5cd
--- /dev/null
+++ b/config/initializers/session_store.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+Nepenthes::Application.config.session_store :cookie_store, key: '_nepenthes_session'
+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rails generate session_migration")
+# Nepenthes::Application.config.session_store :active_record_store
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
new file mode 100644
index 0000000..f833698
--- /dev/null
+++ b/config/initializers/sidekiq.rb
@@ -0,0 +1,6 @@
+Sidekiq.configure_server do |config|
+ config.redis = { namespace: 'resque', timeout: 30 }
+end
+Sidekiq.configure_client do |config|
+ config.redis = { namespace: 'resque' }
+end
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000..999df20
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# Disable root element in JSON by default.
+ActiveSupport.on_load(:active_record) do
+ self.include_root_in_json = false
+end
diff --git a/config/locales/en.bootstrap.yml b/config/locales/en.bootstrap.yml
new file mode 100644
index 0000000..271b49c
--- /dev/null
+++ b/config/locales/en.bootstrap.yml
@@ -0,0 +1,17 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ helpers:
+ actions: "Actions"
+ links:
+ back: "Back"
+ cancel: "Cancel"
+ confirm: "Are you sure?"
+ destroy: "Delete"
+ new: "New"
+ titles:
+ edit: "Edit"
+ save: "Save"
+ new: "New"
+ delete: "Delete"
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..781e456
--- /dev/null
+++ b/config/locales/en.yml
@@ -0,0 +1,8 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ hello: "Hello world"
+ activerecord:
+ models:
+ ip_address: 'IP Addresses'
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..37ef01e
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,93 @@
+require 'sidekiq/web'
+
+Nepenthes::Application.routes.draw do
+ mount Sidekiq::Web => '/sidekiq'
+
+ root to: 'home#index'
+ post 'action', controller: 'home', action: 'action'
+ get 'screenshots', controller: 'home', action: 'screenshots'
+
+ resources :domains
+
+ resources :ip_addresses do
+ collection do
+ get 'batch'
+ post 'batch_create'
+ post 'batch_delete'
+ post 'search'
+ get 'tagged/:tag', :action => 'index', :as => 'tagged'
+ end
+ end
+
+ resources :ports do
+ member do
+ get 'tagged/:tag', :action => 'show', :as => 'tagged'
+ post 'mark_as_done'
+ end
+
+ collection do
+ get 'tagged/:tag', :action => 'index', :as => 'tagged'
+ end
+ end
+
+ resources :regions
+
+ get 'dynamic/screenshot/:id', controller: 'dynamic', action: 'screenshot', as: 'screenshot'
+
+ # The priority is based upon order of creation:
+ # first created -> highest priority.
+
+ # Sample of regular route:
+ # match 'products/:id' => 'catalog#view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # Sample of named route:
+ # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
+ # This route can be invoked with purchase_url(:id => product.id)
+
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
+ # resources :products
+
+ # Sample resource route with options:
+ # resources :products do
+ # member do
+ # get 'short'
+ # post 'toggle'
+ # end
+ #
+ # collection do
+ # get 'sold'
+ # end
+ # end
+
+ # Sample resource route with sub-resources:
+ # resources :products do
+ # resources :comments, :sales
+ # resource :seller
+ # end
+
+ # Sample resource route with more complex sub-resources
+ # resources :products do
+ # resources :comments
+ # resources :sales do
+ # get 'recent', :on => :collection
+ # end
+ # end
+
+ # Sample resource route within a namespace:
+ # namespace :admin do
+ # # Directs /admin/products/* to Admin::ProductsController
+ # # (app/controllers/admin/products_controller.rb)
+ # resources :products
+ # end
+
+ # You can have the root of your site routed with "root"
+ # just remember to delete public/index.html.
+ # root :to => 'welcome#index'
+
+ # See how all your routes lay out with "rake routes"
+
+ # This is a legacy wild controller route that's not recommended for RESTful applications.
+ # Note: This route will make all actions in every controller accessible via GET requests.
+ # match ':controller(/:action(/:id))(.:format)'
+end
diff --git a/db/migrate/20130408185627_create_regions.rb b/db/migrate/20130408185627_create_regions.rb
new file mode 100644
index 0000000..9ab9ecb
--- /dev/null
+++ b/db/migrate/20130408185627_create_regions.rb
@@ -0,0 +1,11 @@
+class CreateRegions < ActiveRecord::Migration
+ def change
+ create_table :regions do |t|
+ t.string :name
+ t.float :utc_start_test
+ t.float :utc_end_test
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20130408192148_acts_as_taggable_on_migration.rb b/db/migrate/20130408192148_acts_as_taggable_on_migration.rb
new file mode 100644
index 0000000..e8b978d
--- /dev/null
+++ b/db/migrate/20130408192148_acts_as_taggable_on_migration.rb
@@ -0,0 +1,30 @@
+class ActsAsTaggableOnMigration < ActiveRecord::Migration
+ def self.up
+ create_table :tags do |t|
+ t.string :name
+ end
+
+ create_table :taggings do |t|
+ t.references :tag
+
+ # You should make sure that the column created is
+ # long enough to store the required class names.
+ t.references :taggable, :polymorphic => true
+ t.references :tagger, :polymorphic => true
+
+ # Limit is created to prevent MySQL error on index
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
+ t.string :context, :limit => 128
+
+ t.datetime :created_at
+ end
+
+ add_index :taggings, :tag_id
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+
+ def self.down
+ drop_table :taggings
+ drop_table :tags
+ end
+end
diff --git a/db/migrate/20130408192951_create_ip_addresses.rb b/db/migrate/20130408192951_create_ip_addresses.rb
new file mode 100644
index 0000000..ee0589f
--- /dev/null
+++ b/db/migrate/20130408192951_create_ip_addresses.rb
@@ -0,0 +1,13 @@
+class CreateIpAddresses < ActiveRecord::Migration
+ def change
+ create_table :ip_addresses do |t|
+ t.string :tags
+ t.belongs_to :region
+ t.belongs_to :tag
+
+ t.timestamps
+ end
+ add_index :ip_addresses, :region_id
+ add_index :ip_addresses, :tag_id
+ end
+end
diff --git a/db/migrate/20130408210432_remove_ip_addresses_tags.rb b/db/migrate/20130408210432_remove_ip_addresses_tags.rb
new file mode 100644
index 0000000..18893c3
--- /dev/null
+++ b/db/migrate/20130408210432_remove_ip_addresses_tags.rb
@@ -0,0 +1,11 @@
+class RemoveIpAddressesTags < ActiveRecord::Migration
+ def up
+ remove_column :ip_addresses, :tag_id
+ remove_column :ip_addresses, :tags
+ end
+
+ def down
+ add_column :ip_addresses, :tag_id, :integer
+ add_column :ip_addresses, :tags, :string
+ end
+end
diff --git a/db/migrate/20130409144019_change_ipaddress_to_binary.rb b/db/migrate/20130409144019_change_ipaddress_to_binary.rb
new file mode 100644
index 0000000..44ff0f9
--- /dev/null
+++ b/db/migrate/20130409144019_change_ipaddress_to_binary.rb
@@ -0,0 +1,18 @@
+class ChangeIpaddressToBinary < ActiveRecord::Migration
+ class IpAddress < ActiveRecord::Base
+ end
+
+ def up
+ add_column :ip_addresses, :address, :binary, :limit => 16
+ IpAddress.reset_column_information
+ IpAddress.all.each do |ipaddr|
+ ipaddr.address_bin = NetAddr::CIDR.create(ipaddr.address).to_i(:ip)
+ ipaddr.save!
+ end
+ add_index :ip_addresses, :address, :unique => true, :length => 16
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration, 'Too lazy.'
+ end
+end
diff --git a/db/migrate/20130409151131_add_taggings_index.rb b/db/migrate/20130409151131_add_taggings_index.rb
new file mode 100644
index 0000000..818c483
--- /dev/null
+++ b/db/migrate/20130409151131_add_taggings_index.rb
@@ -0,0 +1,5 @@
+class AddTaggingsIndex < ActiveRecord::Migration
+ def change
+ add_index :taggings, [:tag_id, :taggable_id]
+ end
+end
diff --git a/db/migrate/20130409160523_create_domains.rb b/db/migrate/20130409160523_create_domains.rb
new file mode 100644
index 0000000..1adcb80
--- /dev/null
+++ b/db/migrate/20130409160523_create_domains.rb
@@ -0,0 +1,11 @@
+class CreateDomains < ActiveRecord::Migration
+ def change
+ create_table :domains do |t|
+ t.string :name
+ t.belongs_to :parent
+
+ t.timestamps
+ end
+ add_index :domains, :parent_id
+ end
+end
diff --git a/db/migrate/20130409171633_remove_parent_from_domains.rb b/db/migrate/20130409171633_remove_parent_from_domains.rb
new file mode 100644
index 0000000..257de83
--- /dev/null
+++ b/db/migrate/20130409171633_remove_parent_from_domains.rb
@@ -0,0 +1,9 @@
+class RemoveParentFromDomains < ActiveRecord::Migration
+ def up
+ remove_column :domains, :parent_id
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration, 'Too lazy.'
+ end
+end
diff --git a/db/migrate/20130409172852_create_scans.rb b/db/migrate/20130409172852_create_scans.rb
new file mode 100644
index 0000000..e3d413b
--- /dev/null
+++ b/db/migrate/20130409172852_create_scans.rb
@@ -0,0 +1,11 @@
+class CreateScans < ActiveRecord::Migration
+ def change
+ create_table :scans do |t|
+ t.belongs_to :ip_address
+ t.text :results
+
+ t.timestamps
+ end
+ add_index :scans, :ip_address_id
+ end
+end
diff --git a/db/migrate/20130410185506_add_options_to_scans.rb b/db/migrate/20130410185506_add_options_to_scans.rb
new file mode 100644
index 0000000..c787ba2
--- /dev/null
+++ b/db/migrate/20130410185506_add_options_to_scans.rb
@@ -0,0 +1,5 @@
+class AddOptionsToScans < ActiveRecord::Migration
+ def change
+ add_column :scans, :options, :text
+ end
+end
diff --git a/db/migrate/20130410205445_create_ports.rb b/db/migrate/20130410205445_create_ports.rb
new file mode 100644
index 0000000..19a0603
--- /dev/null
+++ b/db/migrate/20130410205445_create_ports.rb
@@ -0,0 +1,16 @@
+class CreatePorts < ActiveRecord::Migration
+ def change
+ create_table :ports do |t|
+ t.integer :number
+ t.belongs_to :ip_address
+ t.belongs_to :scan
+ t.string :product
+ t.string :version
+ t.text :extra
+ t.timestamps
+ end
+ # From http://stackoverflow.com/a/12514828
+ # This doesn't work in SQLite3, as its integer type is just INTEGER.
+ # change_column :ports, :id , 'bigint NOT NULL AUTO_INCREMENT'
+ end
+end
diff --git a/db/migrate/20130410211849_add_scan_processed.rb b/db/migrate/20130410211849_add_scan_processed.rb
new file mode 100644
index 0000000..2908fae
--- /dev/null
+++ b/db/migrate/20130410211849_add_scan_processed.rb
@@ -0,0 +1,5 @@
+class AddScanProcessed < ActiveRecord::Migration
+ def change
+ add_column :scans, :processed, :boolean, :default => false, :null => false
+ end
+end
diff --git a/db/migrate/20130416161929_scan_results_to_longtext.rb b/db/migrate/20130416161929_scan_results_to_longtext.rb
new file mode 100644
index 0000000..763d284
--- /dev/null
+++ b/db/migrate/20130416161929_scan_results_to_longtext.rb
@@ -0,0 +1,5 @@
+class ScanResultsToLongtext < ActiveRecord::Migration
+ def change
+ change_column :scans, :results, :text, :limit => 4294967295
+ end
+end
diff --git a/db/migrate/20130617192110_add_notes_to_port.rb b/db/migrate/20130617192110_add_notes_to_port.rb
new file mode 100644
index 0000000..3c04d81
--- /dev/null
+++ b/db/migrate/20130617192110_add_notes_to_port.rb
@@ -0,0 +1,5 @@
+class AddNotesToPort < ActiveRecord::Migration
+ def change
+ add_column :ports, :notes, :text
+ end
+end
diff --git a/db/migrate/20130617192125_add_done_to_port.rb b/db/migrate/20130617192125_add_done_to_port.rb
new file mode 100644
index 0000000..f09edc3
--- /dev/null
+++ b/db/migrate/20130617192125_add_done_to_port.rb
@@ -0,0 +1,5 @@
+class AddDoneToPort < ActiveRecord::Migration
+ def change
+ add_column :ports, :done, :boolean
+ end
+end
diff --git a/db/migrate/20130618211646_add_indexes_to_tables.rb b/db/migrate/20130618211646_add_indexes_to_tables.rb
new file mode 100644
index 0000000..dee45f7
--- /dev/null
+++ b/db/migrate/20130618211646_add_indexes_to_tables.rb
@@ -0,0 +1,7 @@
+class AddIndexesToTables < ActiveRecord::Migration
+ def change
+ add_index :ports, :ip_address_id
+ add_index :ports, :number
+ add_index :ports, :done
+ end
+end
diff --git a/db/migrate/20130711204541_create_screenshots.rb b/db/migrate/20130711204541_create_screenshots.rb
new file mode 100644
index 0000000..3ff72c3
--- /dev/null
+++ b/db/migrate/20130711204541_create_screenshots.rb
@@ -0,0 +1,11 @@
+class CreateScreenshots < ActiveRecord::Migration
+ def change
+ create_table :screenshots do |t|
+ t.string :url
+ t.binary :data, limit: 5.megabytes
+ t.integer :screenshotable_id
+ t.string :screenshotable_type
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20130715183413_add_settings_to_port_and_ip_address.rb b/db/migrate/20130715183413_add_settings_to_port_and_ip_address.rb
new file mode 100644
index 0000000..cde8076
--- /dev/null
+++ b/db/migrate/20130715183413_add_settings_to_port_and_ip_address.rb
@@ -0,0 +1,6 @@
+class AddSettingsToPortAndIpAddress < ActiveRecord::Migration
+ def change
+ add_column :ports, :settings, :text, limit: 50.megabytes
+ add_column :ip_addresses, :settings, :text, limit: 50.megabytes
+ end
+end
diff --git a/db/migrate/20130808144034_add_ssl_to_port.rb b/db/migrate/20130808144034_add_ssl_to_port.rb
new file mode 100644
index 0000000..07ce38c
--- /dev/null
+++ b/db/migrate/20130808144034_add_ssl_to_port.rb
@@ -0,0 +1,20 @@
+class AddSslToPort < ActiveRecord::Migration
+ class Port < ActiveRecord::Base
+ end
+
+ def up
+ add_column :ports, :ssl, :boolean
+ add_index :ports, :ssl
+ Port.reset_column_information
+ Port.all.each do |port|
+ port.ssl = port.settings.delete('ssl')
+ end
+ end
+ def down
+ Port.all.each do |port|
+ port.settings['ssl'] = port.ssl
+ end
+ remove_index :ports, :ssl
+ remove_column :ports, :ssl
+ end
+end
diff --git a/db/migrate/20130808163901_add_has_full_scan_to_ip_addresses.rb b/db/migrate/20130808163901_add_has_full_scan_to_ip_addresses.rb
new file mode 100644
index 0000000..689113b
--- /dev/null
+++ b/db/migrate/20130808163901_add_has_full_scan_to_ip_addresses.rb
@@ -0,0 +1,5 @@
+class AddHasFullScanToIpAddresses < ActiveRecord::Migration
+ def change
+ add_column :ip_addresses, :has_full_scan, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20130808213023_add_screenshotted_to_ports.rb b/db/migrate/20130808213023_add_screenshotted_to_ports.rb
new file mode 100644
index 0000000..7466463
--- /dev/null
+++ b/db/migrate/20130808213023_add_screenshotted_to_ports.rb
@@ -0,0 +1,6 @@
+class AddScreenshottedToPorts < ActiveRecord::Migration
+ def change
+ add_column :ports, :screenshotted, :boolean, default: false
+ add_index :ports, :screenshotted
+ end
+end
diff --git a/db/migrate/20131008194814_add_nikto_results_to_ports.rb b/db/migrate/20131008194814_add_nikto_results_to_ports.rb
new file mode 100644
index 0000000..7602c1a
--- /dev/null
+++ b/db/migrate/20131008194814_add_nikto_results_to_ports.rb
@@ -0,0 +1,5 @@
+class AddNiktoResultsToPorts < ActiveRecord::Migration
+ def change
+ add_column :ports, :nikto_results, :text
+ end
+end
diff --git a/db/migrate/20131125165826_change_ip_address_address_to_integer.rb b/db/migrate/20131125165826_change_ip_address_address_to_integer.rb
new file mode 100644
index 0000000..10aba3f
--- /dev/null
+++ b/db/migrate/20131125165826_change_ip_address_address_to_integer.rb
@@ -0,0 +1,10 @@
+class ChangeIpAddressAddressToInteger < ActiveRecord::Migration
+ def up
+ # Most databases aren't happy with 128-bit integers for some reason.
+ change_column :ip_addresses, :address, :integer, :limit => 4, :default => 0
+ end
+
+ def down
+ change_column :ip_addresses, :address, :binary, :limit => 255
+ end
+end
diff --git a/db/migrate/20131125221449_add_hostname_to_ip_addresses.rb b/db/migrate/20131125221449_add_hostname_to_ip_addresses.rb
new file mode 100644
index 0000000..8d141f9
--- /dev/null
+++ b/db/migrate/20131125221449_add_hostname_to_ip_addresses.rb
@@ -0,0 +1,5 @@
+class AddHostnameToIpAddresses < ActiveRecord::Migration
+ def change
+ add_column :ip_addresses, :hostname, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..a51a437
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,104 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20131125221449) do
+
+ create_table "domains", force: true do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "ip_addresses", force: true do |t|
+ t.integer "region_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "address", default: 0, null: false
+ t.text "settings", limit: 2147483647
+ t.boolean "has_full_scan", default: false
+ t.string "hostname"
+ end
+
+ add_index "ip_addresses", ["address"], name: "index_ip_addresses_on_address", unique: true, using: :btree
+ add_index "ip_addresses", ["region_id"], name: "index_ip_addresses_on_region_id", using: :btree
+
+ create_table "ports", force: true do |t|
+ t.integer "number"
+ t.integer "ip_address_id"
+ t.integer "scan_id"
+ t.string "product"
+ t.string "version"
+ t.text "extra"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "notes"
+ t.boolean "done"
+ t.text "settings", limit: 2147483647
+ t.boolean "ssl"
+ t.boolean "screenshotted", default: false
+ t.text "nikto_results"
+ end
+
+ add_index "ports", ["done"], name: "index_ports_on_done", using: :btree
+ add_index "ports", ["ip_address_id"], name: "index_ports_on_ip_address_id", using: :btree
+ add_index "ports", ["number"], name: "index_ports_on_number", using: :btree
+ add_index "ports", ["screenshotted"], name: "index_ports_on_screenshotted", using: :btree
+ add_index "ports", ["ssl"], name: "index_ports_on_ssl", using: :btree
+
+ create_table "regions", force: true do |t|
+ t.string "name"
+ t.float "utc_start_test"
+ t.float "utc_end_test"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "scans", force: true do |t|
+ t.integer "ip_address_id"
+ t.text "results", limit: 2147483647
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "options"
+ t.boolean "processed", default: false, null: false
+ end
+
+ add_index "scans", ["ip_address_id"], name: "index_scans_on_ip_address_id", using: :btree
+
+ create_table "screenshots", force: true do |t|
+ t.string "url"
+ t.binary "data", limit: 16777215
+ t.integer "screenshotable_id"
+ t.string "screenshotable_type"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "taggings", ["tag_id", "taggable_id"], name: "index_taggings_on_tag_id_and_taggable_id", using: :btree
+ add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
+ add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+
+ create_table "tags", force: true do |t|
+ t.string "name"
+ end
+
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..4edb1e8
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
+# Mayor.create(name: 'Emanuel', city: cities.first)
diff --git a/lib/assets/.gitkeep b/lib/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/log/.gitkeep b/log/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..9a48320
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,26 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..83660ab
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,26 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..f3648a0
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,25 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..b410b01
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+# We dont' really need anybody spidering this.
+User-Agent: *
+Disallow: /
diff --git a/script/rails b/script/rails
new file mode 100755
index 0000000..f8da2cf
--- /dev/null
+++ b/script/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require File.expand_path('../../config/boot', __FILE__)
+require 'rails/commands'
diff --git a/sidekiq.yml b/sidekiq.yml
new file mode 100644
index 0000000..fe7960f
--- /dev/null
+++ b/sidekiq.yml
@@ -0,0 +1,7 @@
+---
+:verbose: true
+:concurrency: 150
+:pidfile: ./tmp/pids/sidekiq.pid
+:queues:
+ - [scans, 5]
+ - [fullscans, 1]
diff --git a/test/fixtures/.gitkeep b/test/fixtures/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/domains.yml b/test/fixtures/domains.yml
new file mode 100644
index 0000000..57c4189
--- /dev/null
+++ b/test/fixtures/domains.yml
@@ -0,0 +1,9 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+ name: MyString
+ parent:
+
+two:
+ name: MyString
+ parent:
diff --git a/test/fixtures/ip_addresses.yml b/test/fixtures/ip_addresses.yml
new file mode 100644
index 0000000..e77f750
--- /dev/null
+++ b/test/fixtures/ip_addresses.yml
@@ -0,0 +1,13 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+ address: MyString
+ tags: MyString
+ region:
+ tag:
+
+two:
+ address: MyString
+ tags: MyString
+ region:
+ tag:
diff --git a/test/fixtures/ports.yml b/test/fixtures/ports.yml
new file mode 100644
index 0000000..c63aac0
--- /dev/null
+++ b/test/fixtures/ports.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
diff --git a/test/fixtures/regions.yml b/test/fixtures/regions.yml
new file mode 100644
index 0000000..970312d
--- /dev/null
+++ b/test/fixtures/regions.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+ name: MyString
+ utc_start_test: 1.5
+ utc_end_test: 1.5
+
+two:
+ name: MyString
+ utc_start_test: 1.5
+ utc_end_test: 1.5
diff --git a/test/fixtures/scans.yml b/test/fixtures/scans.yml
new file mode 100644
index 0000000..34ea7e6
--- /dev/null
+++ b/test/fixtures/scans.yml
@@ -0,0 +1,9 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+ ip_address:
+ results: MyText
+
+two:
+ ip_address:
+ results: MyText
diff --git a/test/fixtures/screenshots.yml b/test/fixtures/screenshots.yml
new file mode 100644
index 0000000..c63aac0
--- /dev/null
+++ b/test/fixtures/screenshots.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
diff --git a/test/functional/.gitkeep b/test/functional/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/functional/domains_controller_test.rb b/test/functional/domains_controller_test.rb
new file mode 100644
index 0000000..f15b62f
--- /dev/null
+++ b/test/functional/domains_controller_test.rb
@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class DomainsControllerTest < ActionController::TestCase
+ setup do
+ @domain = domains(:one)
+ end
+
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:domains)
+ end
+
+ test "should get new" do
+ get :new
+ assert_response :success
+ end
+
+ test "should create domain" do
+ assert_difference('Domain.count') do
+ post :create, domain: { name: @domain.name }
+ end
+
+ assert_redirected_to domain_path(assigns(:domain))
+ end
+
+ test "should show domain" do
+ get :show, id: @domain
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get :edit, id: @domain
+ assert_response :success
+ end
+
+ test "should update domain" do
+ put :update, id: @domain, domain: { name: @domain.name }
+ assert_redirected_to domain_path(assigns(:domain))
+ end
+
+ test "should destroy domain" do
+ assert_difference('Domain.count', -1) do
+ delete :destroy, id: @domain
+ end
+
+ assert_redirected_to domains_path
+ end
+end
diff --git a/test/functional/dynamic_controller_test.rb b/test/functional/dynamic_controller_test.rb
new file mode 100644
index 0000000..4d81491
--- /dev/null
+++ b/test/functional/dynamic_controller_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class DynamicControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/functional/home_controller_test.rb b/test/functional/home_controller_test.rb
new file mode 100644
index 0000000..730478d
--- /dev/null
+++ b/test/functional/home_controller_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class HomeControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/functional/ip_addresses_controller_test.rb b/test/functional/ip_addresses_controller_test.rb
new file mode 100644
index 0000000..34276f1
--- /dev/null
+++ b/test/functional/ip_addresses_controller_test.rb
@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class IpAddressesControllerTest < ActionController::TestCase
+ setup do
+ @ip_address = ip_addresses(:one)
+ end
+
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:ip_addresses)
+ end
+
+ test "should get new" do
+ get :new
+ assert_response :success
+ end
+
+ test "should create ip_address" do
+ assert_difference('IpAddress.count') do
+ post :create, ip_address: { address: @ip_address.address, tags: @ip_address.tags }
+ end
+
+ assert_redirected_to ip_address_path(assigns(:ip_address))
+ end
+
+ test "should show ip_address" do
+ get :show, id: @ip_address
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get :edit, id: @ip_address
+ assert_response :success
+ end
+
+ test "should update ip_address" do
+ put :update, id: @ip_address, ip_address: { address: @ip_address.address, tags: @ip_address.tags }
+ assert_redirected_to ip_address_path(assigns(:ip_address))
+ end
+
+ test "should destroy ip_address" do
+ assert_difference('IpAddress.count', -1) do
+ delete :destroy, id: @ip_address
+ end
+
+ assert_redirected_to ip_addresses_path
+ end
+end
diff --git a/test/functional/ports_controller_test.rb b/test/functional/ports_controller_test.rb
new file mode 100644
index 0000000..d75fed6
--- /dev/null
+++ b/test/functional/ports_controller_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class PortsControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/functional/regions_controller_test.rb b/test/functional/regions_controller_test.rb
new file mode 100644
index 0000000..6224599
--- /dev/null
+++ b/test/functional/regions_controller_test.rb
@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class RegionsControllerTest < ActionController::TestCase
+ setup do
+ @region = regions(:one)
+ end
+
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:regions)
+ end
+
+ test "should get new" do
+ get :new
+ assert_response :success
+ end
+
+ test "should create region" do
+ assert_difference('Region.count') do
+ post :create, region: { name: @region.name, utc_end_test: @region.utc_end_test, utc_start_test: @region.utc_start_test }
+ end
+
+ assert_redirected_to region_path(assigns(:region))
+ end
+
+ test "should show region" do
+ get :show, id: @region
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get :edit, id: @region
+ assert_response :success
+ end
+
+ test "should update region" do
+ put :update, id: @region, region: { name: @region.name, utc_end_test: @region.utc_end_test, utc_start_test: @region.utc_start_test }
+ assert_redirected_to region_path(assigns(:region))
+ end
+
+ test "should destroy region" do
+ assert_difference('Region.count', -1) do
+ delete :destroy, id: @region
+ end
+
+ assert_redirected_to regions_path
+ end
+end
diff --git a/test/integration/.gitkeep b/test/integration/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/performance/browsing_test.rb b/test/performance/browsing_test.rb
new file mode 100644
index 0000000..3fea27b
--- /dev/null
+++ b/test/performance/browsing_test.rb
@@ -0,0 +1,12 @@
+require 'test_helper'
+require 'rails/performance_test_help'
+
+class BrowsingTest < ActionDispatch::PerformanceTest
+ # Refer to the documentation for all available options
+ # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
+ # :output => 'tmp/performance', :formats => [:flat] }
+
+ def test_homepage
+ get '/'
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..8bf1192
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,13 @@
+ENV["RAILS_ENV"] = "test"
+require File.expand_path('../../config/environment', __FILE__)
+require 'rails/test_help'
+
+class ActiveSupport::TestCase
+ # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
+ #
+ # Note: You'll currently still have to declare fixtures explicitly in integration tests
+ # -- they do not yet inherit this setting
+ fixtures :all
+
+ # Add more helper methods to be used by all tests here...
+end
diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/unit/domain_test.rb b/test/unit/domain_test.rb
new file mode 100644
index 0000000..d1e1c1f
--- /dev/null
+++ b/test/unit/domain_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class DomainTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/unit/helpers/domains_helper_test.rb b/test/unit/helpers/domains_helper_test.rb
new file mode 100644
index 0000000..37b228d
--- /dev/null
+++ b/test/unit/helpers/domains_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class DomainsHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/dynamic_helper_test.rb b/test/unit/helpers/dynamic_helper_test.rb
new file mode 100644
index 0000000..76952f5
--- /dev/null
+++ b/test/unit/helpers/dynamic_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class DynamicHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/home_helper_test.rb b/test/unit/helpers/home_helper_test.rb
new file mode 100644
index 0000000..4740a18
--- /dev/null
+++ b/test/unit/helpers/home_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class HomeHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/ip_addresses_helper_test.rb b/test/unit/helpers/ip_addresses_helper_test.rb
new file mode 100644
index 0000000..d6a5690
--- /dev/null
+++ b/test/unit/helpers/ip_addresses_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class IpAddressesHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/ports_helper_test.rb b/test/unit/helpers/ports_helper_test.rb
new file mode 100644
index 0000000..8d8d687
--- /dev/null
+++ b/test/unit/helpers/ports_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class PortsHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/regions_helper_test.rb b/test/unit/helpers/regions_helper_test.rb
new file mode 100644
index 0000000..bf82290
--- /dev/null
+++ b/test/unit/helpers/regions_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class RegionsHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/ip_address_test.rb b/test/unit/ip_address_test.rb
new file mode 100644
index 0000000..d60a5c0
--- /dev/null
+++ b/test/unit/ip_address_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class IpAddressTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/unit/port_test.rb b/test/unit/port_test.rb
new file mode 100644
index 0000000..9f2d961
--- /dev/null
+++ b/test/unit/port_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class PortTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/unit/region_test.rb b/test/unit/region_test.rb
new file mode 100644
index 0000000..4fe2409
--- /dev/null
+++ b/test/unit/region_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class RegionTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/unit/scan_test.rb b/test/unit/scan_test.rb
new file mode 100644
index 0000000..475571b
--- /dev/null
+++ b/test/unit/scan_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ScanTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/unit/screenshot_test.rb b/test/unit/screenshot_test.rb
new file mode 100644
index 0000000..461fe6b
--- /dev/null
+++ b/test/unit/screenshot_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ScreenshotTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/vendor/assets/javascripts/.gitkeep b/vendor/assets/javascripts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/assets/stylesheets/.gitkeep b/vendor/assets/stylesheets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/plugins/.gitkeep b/vendor/plugins/.gitkeep
new file mode 100644
index 0000000..e69de29