diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 683f254..76bf278 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -9,7 +9,6 @@ jobs:
fail-fast: false
matrix:
ruby:
- - '3.0'
- '3.1'
- '3.2'
- '3.3'
diff --git a/.gitignore b/.gitignore
index 1a37534..ff37739 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
/Gemfile.lock
/coverage
/doc
+/pkg
/log
/man/*.1
/tmp
diff --git a/.ruby-version b/.ruby-version
index 3c2f3ca..27fea83 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-ruby-3.1
+ruby-3.3
diff --git a/ChangeLog.md b/ChangeLog.md
index faf8e7c..ebc8b5a 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,4 +1,38 @@
-### 0.1.0 / 2023-XX-XX
+### 0.1.0 / 2024-07-22
* Initial release:
+ * Provides a web interface to explore and search the
+ [ronin database][ronin-db].
+ * Allows managing [ronin-repos] from the web interface.
+ * Allows listing and building the built-in or installed 3rd-party
+ [payloads][ronin-payloads].
+ * Allows listing installed 3rd-party [exploits][ronin-exploits].
+ * Supports automating [nmap] and [masscan] scans and importing their results
+ into the [ronin database][ronin-db].
+ * Supports automating [spidering websites][ronin-web-spider] and importing all
+ visited URLs into the [ronin database][ronin-db].
+ * Supports performing recon using [ronin-recon] and importing all discovered
+ hostnames, IPs, and URLs into [ronin database][ronin-db].
+ * Supports testing URLs for web vulnerabilities using [ronin-vulns].
+[sqlite]: https://sqlite.org/
+[redis]: https://redis.io/
+[nmap]: https://nmap.org/
+[masscan]: https://github.com/robertdavidgraham/masscan#readme
+
+[Ruby]: https://www.ruby-lang.org/
+[dry-types]: https://dry-rb.org/gems/dry-types/
+[dry-schema]: https://dry-rb.org/gems/dry-schema/
+[dry-validation]: https://dry-rb.org/gems/dry-validation/
+
+[ronin-support]: https://github.com/ronin-rb/ronin-support#readme
+[ronin-repos]: https://github.com/ronin-rb/ronin-repos#readme
+[ronin-db]: https://github.com/ronin-rb/ronin-db#readme
+[ronin-payloads]: https://github.com/ronin-rb/ronin-payloads#readme
+[ronin-vulns]: https://github.com/ronin-rb/ronin-vulns#readme
+[ronin-exploits]: https://github.com/ronin-rb/ronin-exploits#readme
+[ronin-nmap]: https://github.com/ronin-rb/ronin-nmap#readme
+[ronin-masscan]: https://github.com/ronin-rb/ronin-masscan#readme
+[ronin-web-spider]: https://github.com/ronin-rb/ronin-web-spider#readme
+[ronin-recon]: https://github.com/ronin-rb/ronin-recon#readme
+[ronin-vulns]: https://github.com/ronin-rb/ronin-vulns#readme
diff --git a/Gemfile b/Gemfile
index f384c51..df8cc43 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
gemspec
-gem 'ruby-masscan', '~> 0.3', github: 'postmodern/ruby-masscan',
- branch: '0.3.0'
+# gem 'ruby-masscan', '~> 0.3', github: 'postmodern/ruby-masscan',
+# branch: 'main'
# NOTE: do not auto-load gems which are meant to be executed at runtime
gem 'puma', require: false
@@ -13,35 +13,38 @@ gem 'sidekiq', require: false
#
# Ronin dependencies
#
-gem 'ronin-support', '~> 1.1', github: 'ronin-rb/ronin-support',
- branch: '1.1.0'
-gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core',
- branch: '0.2.0'
-gem 'ronin-db', '~> 0.2', github: 'ronin-rb/ronin-db',
- branch: '0.2.0'
-
-gem 'ronin-db-activerecord', '~> 0.2', github: 'ronin-rb/ronin-db-activerecord',
- branch: '0.2.0'
-
-gem 'ronin-payloads', '~> 0.1', github: 'ronin-rb/ronin-payloads'
-# gem 'ronin-exploits', '~> 1.0', github: 'ronin-rb/ronin-exploits'
-gem 'ronin-vulns', '~> 0.2', github: 'ronin-rb/ronin-vulns',
- branch: '0.2.0'
-gem 'ronin-web-spider', '~> 0.2', github: 'ronin-rb/ronin-web-spider',
- branch: '0.2.0'
-gem 'ronin-recon', '~> 0.1', github: 'ronin-rb/ronin-recon'
-gem 'ronin-nmap', '~> 0.1', github: 'ronin-rb/ronin-nmap'
-gem 'ronin-masscan', '~> 0.1', github: 'ronin-rb/ronin-masscan'
-gem 'ronin-repos', '~> 0.2', github: 'ronin-rb/ronin-repos',
- branch: '0.2.0'
+# gem 'ronin-support', '~> 1.1', github: 'ronin-rb/ronin-support',
+# branch: 'main'
+# gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core',
+# branch: 'main'
+# gem 'ronin-db', '~> 0.2', github: 'ronin-rb/ronin-db',
+# branch: 'main'
+
+# gem 'ronin-db-activerecord', '~> 0.2', github: 'ronin-rb/ronin-db-activerecord',
+# branch: 'main'
+
+# gem 'ronin-payloads', '~> 0.2', github: 'ronin-rb/ronin-payloads'
+# gem 'ronin-exploits', '~> 1.1', github: 'ronin-rb/ronin-exploits',
+# branch: 'main'
+# gem 'ronin-vulns', '~> 0.2', github: 'ronin-rb/ronin-vulns',
+# branch: 'main'
+# gem 'ronin-web-spider', '~> 0.2', github: 'ronin-rb/ronin-web-spider',
+# branch: 'main'
+# gem 'ronin-recon', '~> 0.1', github: 'ronin-rb/ronin-recon'
+# gem 'ronin-nmap', '~> 0.1', github: 'ronin-rb/ronin-nmap'
+# gem 'ronin-masscan', '~> 0.1', github: 'ronin-rb/ronin-masscan'
+# gem 'ronin-repos', '~> 0.2', github: 'ronin-rb/ronin-repos',
+# branch: 'main'
group :development do
gem 'rake', require: false
+ gem 'rack-test', '~> 2.1', require: false
gem 'rubygems-tasks', '~> 0.2'
gem 'rspec', '~> 3.0', require: false
gem 'simplecov', '~> 0.20', require: false
+ gem 'capybara', '~> 3.40', require: false
gem 'kramdown', '~> 2.0', require: false
gem 'kramdown-man', '~> 1.0', require: false
diff --git a/README.md b/README.md
index f32f76a..7466684 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@
ronin-app is a small web application that is meant to be ran locally by the
user. It provides a web interface to [ronin-support], [ronin-repos], [ronin-db],
[ronin-payloads], [ronin-exploits], as well as automating
-[ronin-nmap], [ronin-masscan], and [ronin-web-spider].
+[ronin-nmap], [ronin-masscan], [ronin-web-spider], [ronin-recon], and
+[ronin-vulns].
## Features
@@ -22,9 +23,65 @@ user. It provides a web interface to [ronin-support], [ronin-repos], [ronin-db],
into the [ronin database][ronin-db].
* Supports automating [spidering websites][ronin-web-spider] and importing all
visited URLs into the [ronin database][ronin-db].
+* Supports performing recon using [ronin-recon] and importing all discovered
+ hostnames, IPs, and URLs into [ronin database][ronin-db].
+* Supports testing URLs for web vulnerabilities using [ronin-vulns].
* Small memory footprint (~184K).
* Fast (~1.251ms response time).
+## Screenshots
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
## Synopsis
```
@@ -54,7 +111,15 @@ http://localhost:1337, if ran in a real terminal.
* [redis-server][redis] >= 6.2
* [nmap]
* [masscan]
-* [Ruby] >= 3.0.0
+* [Ruby] >= 3.1.0
+
+**Note:** both `nmap` and `masscan` require additional Linux capabilities in
+order to be ran without `sudo` or `root` privileges.
+
+```shell
+sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip "$(which nmap)"
+sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip "$(which masscan)"
+```
## Security
@@ -146,3 +211,5 @@ along with ronin-app. If not, see .
[ronin-nmap]: https://github.com/ronin-rb/ronin-nmap#readme
[ronin-masscan]: https://github.com/ronin-rb/ronin-masscan#readme
[ronin-web-spider]: https://github.com/ronin-rb/ronin-web-spider#readme
+[ronin-recon]: https://github.com/ronin-rb/ronin-recon#readme
+[ronin-vulns]: https://github.com/ronin-rb/ronin-vulns#readme
diff --git a/Rakefile b/Rakefile
index b9ab0e3..d166a13 100644
--- a/Rakefile
+++ b/Rakefile
@@ -40,3 +40,5 @@ Ronin::DB::Tasks.new(
database: 'db/dev.sqlite3'
}
)
+
+task :setup => %w[man db:migrate]
diff --git a/app.rb b/app.rb
index 6d1a248..540bbfd 100644
--- a/app.rb
+++ b/app.rb
@@ -27,8 +27,8 @@
require 'sinatra/reloader'
# configuration
-require './config/database'
-require './config/sidekiq'
+require_relative 'config/database'
+require_relative 'config/sidekiq'
# ronin libraries
require 'ronin/repos'
@@ -39,6 +39,7 @@
# param validations
require 'ronin/app/validations/install_repo_params'
require 'ronin/app/validations/import_params'
+require 'ronin/app/validations/http_params'
# schema builders
require 'ronin/app/schemas/payloads/encoders/encode_schema'
@@ -48,12 +49,12 @@
require 'ronin/app/helpers/html'
# worker classes
-require './workers/install_repo'
-require './workers/update_repo'
-require './workers/update_repos'
-require './workers/remove_repo'
-require './workers/purge_repos'
-require './workers/import'
+require_relative 'workers/install_repo'
+require_relative 'workers/update_repo'
+require_relative 'workers/update_repos'
+require_relative 'workers/remove_repo'
+require_relative 'workers/purge_repos'
+require_relative 'workers/import'
require 'ronin/app/version'
require 'sidekiq/api'
@@ -86,6 +87,10 @@ class App < Sinatra::Base
include Pagy::Frontend
end
+ after do
+ ActiveRecord::Base.connection_handler.clear_active_connections!
+ end
+
get '/' do
erb :index
end
@@ -297,7 +302,7 @@ class App < Sinatra::Base
erb :"exploits/index"
end
- get %r{/exploits(?[a-z0-9_-]+(?:/[a-z0-9_-]+)*)} do
+ get %r{/exploits/(?[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*)} do
@exploit = Ronin::Exploits.load_class(params[:exploit_id])
erb :"exploits/show"
@@ -350,6 +355,28 @@ class App < Sinatra::Base
erb :queue
end
+ get '/network/http' do
+ erb :"network/http"
+ end
+
+ post '/network/http' do
+ result = Validations::HTTPParams.call(params)
+
+ if result.success?
+ kwargs = result.to_h
+ method = kwargs.delete(:method)
+ url = kwargs.delete(:url)
+
+ @http_response = Ronin::Support::Network::HTTP.request(method,url,**kwargs)
+
+ erb :"network/http"
+ else
+ @params = params
+ @errors = result.errors
+ halt 400, erb(:"network/http")
+ end
+ end
+
private
#
diff --git a/app/db.rb b/app/db.rb
index 59ef891..1c3a282 100644
--- a/app/db.rb
+++ b/app/db.rb
@@ -85,6 +85,17 @@ class App < Sinatra::Base
end
end
+ post '/db/host_names/import' do
+ begin
+ host_name = Ronin::DB::HostName.find_or_import(params[:host_name])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/host_names"
+ end
+
+ redirect "/db/host_names/#{host_name.id}"
+ end
+
{
mac_addresses: Ronin::DB::MACAddress,
ip_addresses: Ronin::DB::IPAddress,
@@ -118,8 +129,8 @@ class App < Sinatra::Base
delete "/db/#{name}/:id/notes/:note_id" do
@record = model.find(params[:id])
- if @record
- @record.notes.destroy(params[:note_id])
+ if @record && @record.notes.destroy(params[:note_id])
+ redirect "db/#{name}/#{params[:id]}"
else
halt 404
end
@@ -214,6 +225,17 @@ class App < Sinatra::Base
end
end
+ post '/db/ports/import' do
+ begin
+ port = Ronin::DB::Port.find_or_import(params[:port])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/ports"
+ end
+
+ redirect "/db/ports/#{port.id}"
+ end
+
get '/db/services' do
@pagy, @services = pagy(Ronin::DB::Service)
@@ -230,6 +252,17 @@ class App < Sinatra::Base
end
end
+ post '/db/services/import' do
+ begin
+ service = Ronin::DB::Service.find_or_import(params[:service])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/services"
+ end
+
+ redirect "/db/services/#{service.id}"
+ end
+
get '/db/urls' do
@pagy, @urls = pagy(Ronin::DB::URL)
@@ -246,6 +279,17 @@ class App < Sinatra::Base
end
end
+ post '/db/urls/import' do
+ begin
+ url = Ronin::DB::URL.find_or_import(params[:url])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/urls"
+ end
+
+ redirect "/db/urls/#{url.id}"
+ end
+
get '/db/url_schemes' do
@pagy, @url_schemes = pagy(Ronin::DB::URLScheme)
@@ -294,6 +338,17 @@ class App < Sinatra::Base
end
end
+ post '/db/email_addresses/import' do
+ begin
+ email_address = Ronin::DB::EmailAddress.find_or_import(params[:email_address])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/email_addresses"
+ end
+
+ redirect "/db/email_addresses/#{email_address.id}"
+ end
+
get '/db/user_names' do
@pagy, @user_names = pagy(Ronin::DB::UserName)
@@ -310,6 +365,17 @@ class App < Sinatra::Base
end
end
+ post '/db/user_names/import' do
+ begin
+ user_name = Ronin::DB::UserName.find_or_import(params[:user_name])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/user_names"
+ end
+
+ redirect "/db/user_names/#{user_name.id}"
+ end
+
get '/db/passwords' do
@pagy, @passwords = pagy(Ronin::DB::Password)
@@ -326,6 +392,17 @@ class App < Sinatra::Base
end
end
+ post '/db/passwords/import' do
+ begin
+ password = Ronin::DB::Password.find_or_import(params[:password])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/passwords"
+ end
+
+ redirect "/db/passwords/#{password.id}"
+ end
+
get '/db/credentials' do
@pagy, @credentials = pagy(Ronin::DB::Credential)
@@ -342,6 +419,17 @@ class App < Sinatra::Base
end
end
+ post '/db/credentials/import' do
+ begin
+ credential = Ronin::DB::Credential.find_or_import(params[:cred])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/credentials"
+ end
+
+ redirect "/db/credentials/#{credential.id}"
+ end
+
get '/db/advisories' do
@pagy, @advisories = pagy(Ronin::DB::Advisory)
@@ -358,6 +446,17 @@ class App < Sinatra::Base
end
end
+ post '/db/advisories/import' do
+ begin
+ advisory = Ronin::DB::Advisory.find_or_import(params[:id])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/advisories"
+ end
+
+ redirect "/db/advisories/#{advisory.id}"
+ end
+
get '/db/software' do
@pagy, @software = pagy(Ronin::DB::Software)
@@ -434,6 +533,17 @@ class App < Sinatra::Base
end
end
+ post '/db/phone_numbers/import' do
+ begin
+ phone_number = Ronin::DB::PhoneNumber.find_or_import(params[:phone_number])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/phone_numbers"
+ end
+
+ redirect "/db/phone_numbers/#{phone_number.id}"
+ end
+
get '/db/street_addresses' do
@pagy, @street_addresses = pagy(Ronin::DB::StreetAddress)
@@ -441,9 +551,9 @@ class App < Sinatra::Base
end
get '/db/street_addresses/:id' do
- @phone_number = Ronin::DB::StreetAddress.find(params[:id])
+ @street_address = Ronin::DB::StreetAddress.find(params[:id])
- if @phone_number
+ if @street_address
erb :"db/street_addresses/show"
else
halt 404
@@ -466,6 +576,17 @@ class App < Sinatra::Base
end
end
+ post '/db/organizations/import' do
+ begin
+ organization = Ronin::DB::Organization.find_or_import(params[:name])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/organizations"
+ end
+
+ redirect "/db/organizations/#{organization.id}"
+ end
+
get '/db/organization_departments/:id' do
@organization_department = Ronin::DB::OrganizationDepartment.find(params[:id])
@@ -502,6 +623,17 @@ class App < Sinatra::Base
end
end
+ post '/db/people/import' do
+ begin
+ person = Ronin::DB::Person.find_or_import(params[:person])
+ rescue ArgumentError => error
+ flash[:danger] = error.message
+ redirect "db/people"
+ end
+
+ redirect "/db/people/#{person.id}"
+ end
+
{
host_names: Ronin::DB::HostName,
asns: Ronin::DB::ASN,
diff --git a/app/scanning.rb b/app/scanning.rb
index 52a61bc..5234455 100644
--- a/app/scanning.rb
+++ b/app/scanning.rb
@@ -29,11 +29,11 @@
require 'ronin/app/helpers/html'
# worker classes
-require './workers/nmap'
-require './workers/masscan'
-require './workers/spider'
-require './workers/recon'
-require './workers/vulns'
+require_relative '../workers/nmap'
+require_relative '../workers/masscan'
+require_relative '../workers/spider'
+require_relative '../workers/recon'
+require_relative '../workers/vulns'
#
# App class containing routes for scanning.
diff --git a/config/database.rb b/config/database.rb
index 84227f3..afbea42 100644
--- a/config/database.rb
+++ b/config/database.rb
@@ -11,3 +11,7 @@
end
Ronin::DB.connect(database, pool: pool_size)
+
+at_exit do
+ ActiveRecord::Base.connection_pool.disconnect!
+end
diff --git a/config/sidekiq.rb b/config/sidekiq.rb
index 2aab424..950ef9c 100644
--- a/config/sidekiq.rb
+++ b/config/sidekiq.rb
@@ -2,12 +2,18 @@
require 'sidekiq'
require 'redis/namespace'
+require 'middleware/sidekiq/active_record_connection_pool'
+
redis_config = {
url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}"
}
Sidekiq.configure_server do |config|
config.redis = redis_config
+
+ config.server_middleware do |chain|
+ chain.add Middleware::Sidekiq::ActiveRecordConnectionPool
+ end
end
Sidekiq.configure_client do |config|
diff --git a/gemspec.yml b/gemspec.yml
index 48a8a71..9bfe9f2 100644
--- a/gemspec.yml
+++ b/gemspec.yml
@@ -1,12 +1,12 @@
name: ronin-app
summary: A local web interface for Ronin
description: |
- ronin-app is a small web application that is meant to be ran locally by the
+ ronin-app is a small web application that is meant to be ran locally by the
user. It provides a web interface to ronin-support, ronin-repos, ronin-db,
ronin-payloads, ronin-exploits, as well as automating
- ronin-nmap, ronin-masscan, and ronin-web-spider.
+ ronin-nmap, ronin-masscan, ronin-web-spider, ronin-recon, and ronin-vulns.
-license: AGPL-3.0
+license: AGPL-3.0-or-later
authors: Postmodern
email: postmodern.mod3@gmail.com
homepage: https://ronin-rb.dev/
@@ -21,7 +21,10 @@ metadata:
generated_files:
- man/ronin-app.1
-required_ruby_version: ">= 3.0.0"
+excluded_files:
+ - screenshots/*.svg
+
+required_ruby_version: ">= 3.1.0"
dependencies:
dry-schema: ~> 1.0
@@ -38,10 +41,11 @@ dependencies:
# Ronin dependencies:
ronin-support: ~> 1.1
ronin-core: ~> 0.2
+ ronin-repos: ~> 0.2
ronin-db-activerecord: ~> 0.2
ronin-db: ~> 0.2
- ronin-payloads: ~> 0.1
- ronin-exploits: ~> 1.0
+ ronin-payloads: ~> 0.2
+ ronin-exploits: ~> 1.1
ronin-vulns: ~> 0.2
ronin-web-spider: ~> 0.2
ronin-nmap: ~> 0.1
diff --git a/lib/middleware/sidekiq/active_record_connection_pool.rb b/lib/middleware/sidekiq/active_record_connection_pool.rb
new file mode 100644
index 0000000..57d3ed2
--- /dev/null
+++ b/lib/middleware/sidekiq/active_record_connection_pool.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+#
+# ronin-app - a local web app for Ronin.
+#
+# Copyright (C) 2023 Hal Brodigan (postmodern.mod3@gmail.com)
+#
+# ronin-app is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ronin-app is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with ronin-app. If not, see .
+#
+
+require 'active_record'
+
+module Middleware
+ module Sidekiq
+ #
+ # Sidekiq middleware to clear the ActiveRecord connection pool after each
+ # job.
+ #
+ class ActiveRecordConnectionPool
+
+ #
+ # Allows the job to be processed, then clears the ActiveRecord connection
+ # pool.
+ #
+ def call(*)
+ yield
+ ensure
+ begin
+ ActiveRecord::Base.connection_handler.clear_active_connections!
+ rescue => error
+ warn error.message
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/ronin/app/cli.rb b/lib/ronin/app/cli.rb
index ca8d37b..4d05ada 100644
--- a/lib/ronin/app/cli.rb
+++ b/lib/ronin/app/cli.rb
@@ -21,12 +21,13 @@
require 'ronin/core/cli/command'
require 'ronin/core/cli/logging'
require 'ronin/db/config_file'
-require 'ronin/app/root'
-require 'ronin/app/version'
require 'command_kit/options/version'
require 'command_kit/open_app'
+require_relative 'root'
+require_relative 'version'
+
module Ronin
module App
#
diff --git a/lib/ronin/app/schemas/params_schema.rb b/lib/ronin/app/schemas/params_schema.rb
index b6efecb..074c313 100644
--- a/lib/ronin/app/schemas/params_schema.rb
+++ b/lib/ronin/app/schemas/params_schema.rb
@@ -18,9 +18,9 @@
# along with ronin-app. If not, see .
#
-require 'dry-schema'
-require 'ronin/app/types'
+require_relative '../types'
+require 'dry-schema'
require 'ronin/core/params/types'
module Ronin
diff --git a/lib/ronin/app/schemas/payloads/build_schema.rb b/lib/ronin/app/schemas/payloads/build_schema.rb
index e5c0e4e..891ba0f 100644
--- a/lib/ronin/app/schemas/payloads/build_schema.rb
+++ b/lib/ronin/app/schemas/payloads/build_schema.rb
@@ -18,9 +18,9 @@
# along with ronin-app. If not, see .
#
-require 'dry/schema'
+require_relative '../params_schema'
-require 'ronin/app/schemas/params_schema'
+require 'dry/schema'
module Ronin
module App
diff --git a/lib/ronin/app/schemas/payloads/encoders/encode_schema.rb b/lib/ronin/app/schemas/payloads/encoders/encode_schema.rb
index 28e4058..fb15ba3 100644
--- a/lib/ronin/app/schemas/payloads/encoders/encode_schema.rb
+++ b/lib/ronin/app/schemas/payloads/encoders/encode_schema.rb
@@ -18,9 +18,9 @@
# along with ronin-app. If not, see .
#
-require 'dry/schema'
+require_relative '../../params_schema'
-require 'ronin/app/schemas/params_schema'
+require 'dry/schema'
module Ronin
module App
diff --git a/lib/ronin/app/types/import.rb b/lib/ronin/app/types/import.rb
index a09bfcf..3862149 100644
--- a/lib/ronin/app/types/import.rb
+++ b/lib/ronin/app/types/import.rb
@@ -18,7 +18,7 @@
# along with ronin-app. If not, see .
#
-require 'ronin/app/types'
+require_relative '../types'
module Ronin
module App
diff --git a/lib/ronin/app/types/nmap.rb b/lib/ronin/app/types/nmap.rb
index d2d175a..d3563c0 100644
--- a/lib/ronin/app/types/nmap.rb
+++ b/lib/ronin/app/types/nmap.rb
@@ -18,7 +18,7 @@
# along with ronin-app. If not, see .
#
-require 'ronin/app/types'
+require_relative '../types'
require 'nmap/command'
diff --git a/lib/ronin/app/types/spider.rb b/lib/ronin/app/types/spider.rb
index ff4f915..0a8bd81 100644
--- a/lib/ronin/app/types/spider.rb
+++ b/lib/ronin/app/types/spider.rb
@@ -18,7 +18,7 @@
# along with ronin-app. If not, see .
#
-require 'ronin/app/types'
+require_relative '../types'
module Ronin
module App
diff --git a/lib/ronin/app/types/vulns.rb b/lib/ronin/app/types/vulns.rb
index 7a7a403..0ba38e5 100644
--- a/lib/ronin/app/types/vulns.rb
+++ b/lib/ronin/app/types/vulns.rb
@@ -18,7 +18,7 @@
# along with ronin-app. If not, see .
#
-require 'ronin/app/types'
+require_relative '../types'
module Ronin
module App
diff --git a/lib/ronin/app/validations/http_params.rb b/lib/ronin/app/validations/http_params.rb
new file mode 100644
index 0000000..e506576
--- /dev/null
+++ b/lib/ronin/app/validations/http_params.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+#
+# ronin-app - a local web app for Ronin.
+#
+# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
+#
+# ronin-app is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ronin-app is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with ronin-app. If not, see .
+#
+
+require 'dry/validation'
+
+module Ronin
+ module App
+ module Validations
+ #
+ # Validations for the form params submitted to `POST /network/http`.
+ #
+ class HTTPParams < Dry::Validation::Contract
+
+ HTTPMethods = Types::Symbol.enum(
+ copy: 'COPY',
+ delete: 'DELETE',
+ get: 'GET',
+ head: 'HEAD',
+ lock: 'LOCK',
+ mkcol: 'MKCOL',
+ move: 'MOVE',
+ options: 'OPTIONS',
+ patch: 'PATCH',
+ post: 'POST',
+ propfind: 'PROPFIND',
+ proppatch: 'PROPPATCH',
+ put: 'PUT',
+ trace: 'TRACE',
+ unlock: 'UNLOCK'
+ )
+
+ Versions = (Types::Float | Types::Integer).enum(
+ 1 => '1.0',
+ 1.1 => '1.1',
+ 1.2 => '1.2'
+ )
+
+ VerificationModes = Types::Symbol.enum(
+ none: 'none',
+ peer: 'peer',
+ fail_if_no_peer_cert: 'fail_if_no_peer_cert'
+ )
+
+ Headers = Types::Hash.constructor do |input,type|
+ if input.is_a?(String)
+ headers = {}
+
+ input.strip.each_line(chomp: true) do |line|
+ name, value = line.split(/:\s*/,2)
+
+ case (previous_value = headers[name])
+ when nil # no value yet
+ headers[name] = value
+ when String # previous value
+ headers[name] = [previous_value, value]
+ when Array # multiple previous values
+ previous_value << value
+ end
+ end
+
+ headers unless headers.empty?
+ elsif type.valid?(input)
+ input
+ else
+ type.call(input)
+ end
+ end
+
+ params do
+ required(:method).filled(HTTPMethods)
+ required(:url).filled(:string)
+
+ optional(:body).maybe(:string)
+ optional(:headers).maybe(Headers)
+
+ optional(:proxy).maybe(:string)
+ optional(:user_agent).maybe(:string)
+ optional(:user).maybe(:string)
+ optional(:password).maybe(:string)
+ optional(:cookie).maybe(:string)
+
+ optional(:ssl).hash do
+ optional(:timeout).maybe(:integer)
+ optional(:version).maybe(Versions)
+ optional(:min_version).maybe(Versions)
+ optional(:max_version).maybe(Versions)
+ optional(:verify).maybe(VerificationModes)
+ optional(:verify_depth).maybe(:integer)
+ optional(:verify_hostname).maybe(:bool)
+ end
+ end
+
+ #
+ # Initializes and calls the validation contract.
+ #
+ # @param [Hash{String => Object}] params
+ # The HTTP params to validate.
+ #
+ # @return [Dry::Validation::Result]
+ # The validation result.
+ #
+ def self.call(params)
+ new.call(params)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/ronin/app/validations/import_params.rb b/lib/ronin/app/validations/import_params.rb
index 2fbf203..627710d 100644
--- a/lib/ronin/app/validations/import_params.rb
+++ b/lib/ronin/app/validations/import_params.rb
@@ -18,7 +18,7 @@
# along with ronin-app. If not, see .
#
-require 'ronin/app/types/import'
+require_relative '../types/import'
require 'dry/validation'
require 'set'
diff --git a/lib/ronin/app/validations/masscan_params.rb b/lib/ronin/app/validations/masscan_params.rb
index baa23b2..bf51811 100644
--- a/lib/ronin/app/validations/masscan_params.rb
+++ b/lib/ronin/app/validations/masscan_params.rb
@@ -18,9 +18,9 @@
# along with ronin-app. If not, see .
#
-require 'dry/validation'
-require 'ronin/app/types'
+require_relative '../types'
+require 'dry/validation'
require 'masscan/command'
module Ronin
@@ -33,8 +33,8 @@ class MasscanParams < Dry::Validation::Contract
params do
required(:ips).filled(Types::Args).each(:filled?)
+ required(:ports).filled(:string)
- optional(:ports).maybe(:string)
optional(:banners).maybe(:bool)
optional(:rate).maybe(:integer)
optional(:config_file).maybe(:string)
diff --git a/lib/ronin/app/validations/nmap_params.rb b/lib/ronin/app/validations/nmap_params.rb
index c8023fe..96a14ce 100644
--- a/lib/ronin/app/validations/nmap_params.rb
+++ b/lib/ronin/app/validations/nmap_params.rb
@@ -18,10 +18,10 @@
# along with ronin-app. If not, see .
#
-require 'dry/validation'
-require 'ronin/app/types'
-require 'ronin/app/types/nmap'
+require_relative '../types'
+require_relative '../types/nmap'
+require 'dry/validation'
require 'nmap/command'
module Ronin
diff --git a/lib/ronin/app/validations/recon_params.rb b/lib/ronin/app/validations/recon_params.rb
index fc200e5..32280f4 100644
--- a/lib/ronin/app/validations/recon_params.rb
+++ b/lib/ronin/app/validations/recon_params.rb
@@ -18,9 +18,9 @@
# along with ronin-app. If not, see .
#
-require 'dry/validation'
-require 'ronin/app/types'
+require_relative '../types'
+require 'dry/validation'
require 'ronin/recon/value/parser'
module Ronin
diff --git a/lib/ronin/app/validations/spider_params.rb b/lib/ronin/app/validations/spider_params.rb
index e7481dc..3f3c28f 100644
--- a/lib/ronin/app/validations/spider_params.rb
+++ b/lib/ronin/app/validations/spider_params.rb
@@ -18,8 +18,9 @@
# along with ronin-app. If not, see .
#
+require_relative '../types/spider'
+
require 'dry/validation'
-require 'ronin/app/types/spider'
module Ronin
module App
diff --git a/lib/ronin/app/validations/vulns_params.rb b/lib/ronin/app/validations/vulns_params.rb
index e7fa1c1..5c588ac 100644
--- a/lib/ronin/app/validations/vulns_params.rb
+++ b/lib/ronin/app/validations/vulns_params.rb
@@ -18,8 +18,9 @@
# along with ronin-app. If not, see .
#
+require_relative '../types/vulns'
+
require 'dry/validation'
-require 'ronin/app/types/vulns'
module Ronin
module App
diff --git a/public/stylesheets/app.css b/public/stylesheets/app.css
index 060fcff..0781677 100644
--- a/public/stylesheets/app.css
+++ b/public/stylesheets/app.css
@@ -16,6 +16,7 @@ body {
main {
clear: both;
+ min-height: 70vh;
}
header {
@@ -23,6 +24,13 @@ header {
background-color: black;
}
+#logo {
+ margin: 0.5em;
+}
+
+/*
+ * Top nav menu rules.
+ */
nav#top-menu {
margin-left: auto;
padding: 0.5em 0.5em;
@@ -53,10 +61,10 @@ button#dark-mode-toggle svg#dark-mode-moon {
display: none;
}
-#logo {
- margin: 0.5em;
-}
-
+/*
+ * Navbar rules.
+ * https://bulma.io/documentation/components/navbar/
+ */
nav.navbar {
font-size: 140%;
}
@@ -70,6 +78,18 @@ nav.navbar a:hover {
color: black;
}
+nav.navbar .navbar-dropdown {
+ background-color: black;
+ padding-top: 0;
+ border-top: 0;
+}
+
+nav.navbar .navbar-dropdown {
+ background-color: black;
+ padding-top: 0;
+ border-top: 0;
+}
+
main > nav.breadcrumb {
margin-top: 1em;
}
@@ -78,6 +98,9 @@ main > nav.breadcrumb a {
color: var(--fg-color);
}
+/*
+ * Content rules.
+ */
.content {
margin-top: 1em;
margin-bottom: 1em;
@@ -107,28 +130,68 @@ main > nav.breadcrumb a {
overflow-x: auto;
}
-.content .advanced {
- transition: all 0.2s ease-in-out;
- overflow: hidden;
+
+.content .label.is-required::after,
+.content .control.is-required::after {
+ content: "*";
+ color: red;
+ display: inline;
+ font-size: 1.2rem;
+}
+
+.content .control.is-required::after {
+ position: absolute;
+ right: 0.2rem;
+ top: 0;
}
-.content .advanced > a.advanced-toggle {
- border-bottom: 1px solid #c3c3c3;
+/*
+ * Content Tab rules.
+ */
+.content .tabs {
+ margin: 0 !important;
}
-.content .advanced.hidden > .advanced-content {
+.content .tabs-content {
+ padding: 1rem 1.5rem !important;
+}
+
+.content .tabs ul {
+ margin: 0 !important;
+ border: none;
+}
+
+.content .tabs ul li {
+ padding: 4px 0;
+}
+
+.content .tabs ul li a {
+ text-decoration: none;
+}
+
+.content .tabs ul li a:hover {
+ background-color: var(--fg-color);
+}
+
+.content .content-tab {
display: none;
}
+.content .content-tab.is-active {
+ display: block;
+}
+
footer.footer {
clear: both;
- margin-top: 10rem;
color: gray;
background-color: var(--fg-color);
font-size: medium;
background-color: black;
}
+/*
+ * Dark-mode rules.
+ */
body.dark-mode {
color: var(--dark-mode-fg-color);
background-color: var(--dark-mode-bg-color);
@@ -158,24 +221,9 @@ body.dark-mode main a {
color: var(--dark-mode-fg-color);
}
-.label.is-required::after,
-.control.is-required::after {
- content: "*";
- color: red;
- display: inline;
- font-size: 1.2rem;
-}
-
-.control.is-required::after {
- position: absolute;
- right: 0.2rem;
- top: 0;
-}
-
-.navbar-dropdown {
- background-color: black;
- padding-top: 0;
- border-top: 0;
+body.dark-mode .content .tabs ul li a:hover {
+ color: var(--dark-mode-bg-color);
+ background-color: var(--dark-mode-fg-color);
}
body.dark-mode .dropdown-content {
diff --git a/ronin-app.gemspec b/ronin-app.gemspec
index 45f4e52..aa4d9ff 100644
--- a/ronin-app.gemspec
+++ b/ronin-app.gemspec
@@ -27,6 +27,7 @@ Gem::Specification.new do |gem|
gem.files = `git ls-files`.split($/)
gem.files = glob[gemspec['files']] if gemspec['files']
gem.files += Array(gemspec['generated_files'])
+ gem.files -= glob[gemspec['excluded_files']] if gemspec['excluded_files']
# exclude test files from the packages gem
gem.files -= glob[gemspec['test_files'] || 'spec/{**/}*']
diff --git a/screenshots/ronin_app_db.svg b/screenshots/ronin_app_db.svg
new file mode 100644
index 0000000..51a8bb5
--- /dev/null
+++ b/screenshots/ronin_app_db.svg
@@ -0,0 +1,495 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_db_ip_address.svg b/screenshots/ronin_app_db_ip_address.svg
new file mode 100644
index 0000000..12e6c32
--- /dev/null
+++ b/screenshots/ronin_app_db_ip_address.svg
@@ -0,0 +1,461 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_exploits.svg b/screenshots/ronin_app_exploits.svg
new file mode 100644
index 0000000..f26e5d7
--- /dev/null
+++ b/screenshots/ronin_app_exploits.svg
@@ -0,0 +1,249 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_exploits_show.svg b/screenshots/ronin_app_exploits_show.svg
new file mode 100644
index 0000000..a279f33
--- /dev/null
+++ b/screenshots/ronin_app_exploits_show.svg
@@ -0,0 +1,650 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_payloads.svg b/screenshots/ronin_app_payloads.svg
new file mode 100644
index 0000000..9fa121d
--- /dev/null
+++ b/screenshots/ronin_app_payloads.svg
@@ -0,0 +1,442 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_payloads_build.svg b/screenshots/ronin_app_payloads_build.svg
new file mode 100644
index 0000000..e824c3f
--- /dev/null
+++ b/screenshots/ronin_app_payloads_build.svg
@@ -0,0 +1,413 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_payloads_show.svg b/screenshots/ronin_app_payloads_show.svg
new file mode 100644
index 0000000..4b5caaa
--- /dev/null
+++ b/screenshots/ronin_app_payloads_show.svg
@@ -0,0 +1,483 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_repos.svg b/screenshots/ronin_app_repos.svg
new file mode 100644
index 0000000..6adfa31
--- /dev/null
+++ b/screenshots/ronin_app_repos.svg
@@ -0,0 +1,260 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_repos_show.svg b/screenshots/ronin_app_repos_show.svg
new file mode 100644
index 0000000..3e4e4c1
--- /dev/null
+++ b/screenshots/ronin_app_repos_show.svg
@@ -0,0 +1,367 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_scanning_masscan.svg b/screenshots/ronin_app_scanning_masscan.svg
new file mode 100644
index 0000000..5462c6d
--- /dev/null
+++ b/screenshots/ronin_app_scanning_masscan.svg
@@ -0,0 +1,617 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_scanning_nmap.svg b/screenshots/ronin_app_scanning_nmap.svg
new file mode 100644
index 0000000..f22b034
--- /dev/null
+++ b/screenshots/ronin_app_scanning_nmap.svg
@@ -0,0 +1,936 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_scanning_recon.svg b/screenshots/ronin_app_scanning_recon.svg
new file mode 100644
index 0000000..2f8e079
--- /dev/null
+++ b/screenshots/ronin_app_scanning_recon.svg
@@ -0,0 +1,278 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_scanning_spider.svg b/screenshots/ronin_app_scanning_spider.svg
new file mode 100644
index 0000000..7b33e8b
--- /dev/null
+++ b/screenshots/ronin_app_scanning_spider.svg
@@ -0,0 +1,517 @@
+
\ No newline at end of file
diff --git a/screenshots/ronin_app_scanning_vulns.svg b/screenshots/ronin_app_scanning_vulns.svg
new file mode 100644
index 0000000..7b732e0
--- /dev/null
+++ b/screenshots/ronin_app_scanning_vulns.svg
@@ -0,0 +1,476 @@
+
\ No newline at end of file
diff --git a/scripts/setup b/scripts/setup
index 00d9b4e..167cae8 100755
--- a/scripts/setup
+++ b/scripts/setup
@@ -428,10 +428,16 @@ auto_install_bundler
install_dependencies
set_caps
+# default to installing gems into vendor/bundle
+if [[ ! -f .bundle/config ]]; then
+ bundle config set --local path vendor/bundle >/dev/null || \
+ fail "Failed to run 'bundle config'"
+fi
+
bundle install || fail "Failed to install gems!"
-log "Creating the default database ..."
-bundle exec rake db:migrate || fail "Failed to create database!"
+log "Setting up the project ..."
+bundle exec rake setup || fail "Failed to setup project!"
log "ronin-app is now ready to be ran!"
echo
diff --git a/spec/middleware/sidekiq/active_record_connection_pool_spec.rb b/spec/middleware/sidekiq/active_record_connection_pool_spec.rb
new file mode 100644
index 0000000..edab1de
--- /dev/null
+++ b/spec/middleware/sidekiq/active_record_connection_pool_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require 'middleware/sidekiq/active_record_connection_pool'
+
+describe Middleware::Sidekiq::ActiveRecordConnectionPool do
+ describe "#call" do
+ let(:worker) { double('Worker') }
+ let(:job) { double('job') }
+ let(:queue) { double('queue') }
+
+ it "must yield then call ActiveRecord::Base.connection_handler.clear_active_connections!" do
+ expect(ActiveRecord::Base.connection_handler).to receive(:clear_active_connections!)
+
+ expect { |b|
+ subject.call(worker,job,queue,&b)
+ }.to yield_control
+ end
+
+ context "when the block raises an exception" do
+ let(:message) { "error!" }
+ let(:exception) { RuntimeError.new(message) }
+
+ it "must still call ActiveRecord::Base.connection_handler.clear_active_connections!" do
+ expect(ActiveRecord::Base.connection_handler).to receive(:clear_active_connections!)
+
+ expect {
+ subject.call(worker,job,queue) do
+ raise(exception)
+ end
+ }.to raise_error(exception)
+ end
+ end
+
+ context "when ActiveRecord::Base.connection_handler.clear_active_connections! raises an exception" do
+ let(:message) { "ActiveRecord error!" }
+ let(:exception) { RuntimeError.new(message) }
+
+ it "must print the error message to stderr" do
+ expect(ActiveRecord::Base.connection_handler).to receive(:clear_active_connections!).and_raise(exception)
+
+ expect { |b|
+ subject.call(worker,job,queue,&b)
+ }.to yield_control.and(output("#{message}#{$/}").to_stderr)
+ end
+ end
+ end
+end
diff --git a/spec/routes/db_spec.rb b/spec/routes/db_spec.rb
new file mode 100644
index 0000000..8b138cb
--- /dev/null
+++ b/spec/routes/db_spec.rb
@@ -0,0 +1,616 @@
+require 'spec_helper'
+require './app'
+
+describe App, type: :feature do
+ after do
+ Ronin::DB::HostName.destroy_all
+ Ronin::DB::ASN.destroy_all
+ Ronin::DB::IPAddress.destroy_all
+ Ronin::DB::MACAddress.destroy_all
+ Ronin::DB::OpenPort.destroy_all
+ Ronin::DB::Port.destroy_all
+ Ronin::DB::Service.destroy_all
+ Ronin::DB::WebVuln.destroy_all
+ Ronin::DB::URL.destroy_all
+ Ronin::DB::URLScheme.destroy_all
+ Ronin::DB::Password.destroy_all
+ Ronin::DB::Credential.destroy_all
+ Ronin::DB::Advisory.destroy_all
+ Ronin::DB::Software.destroy_all
+ Ronin::DB::SoftwareVendor.destroy_all
+ Ronin::DB::OS.destroy_all
+ Ronin::DB::PhoneNumber.destroy_all
+ Ronin::DB::StreetAddress.destroy_all
+ Ronin::DB::Organization.destroy_all
+ Ronin::DB::Person.destroy_all
+ end
+
+ context 'GET /db' do
+ [
+ '/db/host_names',
+ '/db/asns',
+ '/db/ip_addresses',
+ '/db/mac_addresses',
+ '/db/open_ports',
+ '/db/ports',
+ '/db/services',
+ '/db/vulns',
+ '/db/urls',
+ '/db/url_schemes',
+ '/db/passwords',
+ '/db/credentials',
+ '/db/advisories',
+ '/db/software',
+ '/db/software_vendors',
+ '/db/oses',
+ '/db/phone_numbers',
+ '/db/street_addresses',
+ '/db/organizations',
+ '/db/people'
+ ].each do |path|
+ it "must contain a link to #{path}" do
+ visit '/db'
+
+ expect(page).to have_xpath("//a[@href='#{path}']")
+ end
+ end
+ end
+
+ context 'GET /db/host_names' do
+ let!(:host_name) { Ronin::DB::HostName.find_or_import('example.com') }
+ let(:xpath) { "//a[text()='#{host_name}'][@href='/db/host_names/#{host_name.id}']" }
+
+ it 'must return status 200 and include link to a host name in the body' do
+ visit '/db/host_names'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/host_names/:id' do
+ let!(:host_name) { Ronin::DB::HostName.find_or_import('example.com') }
+ let(:header_xpath) { "//h1[text()='Host Name: #{host_name}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/host_names/#{host_name.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/asns' do
+ let!(:asn) { Ronin::DB::ASN.find_or_create_by(version: 4, range_start: '1.2.3.4', range_end: '1.2.3.4', number: 1234) }
+ let(:xpath) { "//a[text()='#{asn}'][@href='/db/asns/#{asn.id}']" }
+
+ it 'must return status 200 and include link to a asn in the body' do
+ visit '/db/asns'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/asns/:id' do
+ let!(:asn) { Ronin::DB::ASN.find_or_create_by(version: 4, range_start: '1.2.3.4', range_end: '1.2.3.4', number: 1234) }
+ let(:header_xpath) { "//h1[text()='ASN: #{asn}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/asns/#{asn.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/ip_addresses' do
+ let!(:ip_address) { Ronin::DB::IPAddress.find_or_import('1.2.3.4') }
+ let(:xpath) { "//a[text()='#{ip_address}'][@href='/db/ip_addresses/#{ip_address.id}']" }
+
+ it 'must return status 200 and include link to a ip address in the body' do
+ visit '/db/ip_addresses'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/ip_addresses/:id' do
+ let!(:ip_address) { Ronin::DB::IPAddress.find_or_import('1.2.3.4') }
+ let(:header_xpath) { "//h1[text()='IP Address: #{ip_address}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/ip_addresses/#{ip_address.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/mac_addresses' do
+ let!(:mac_address) { Ronin::DB::MACAddress.find_or_import('01:23:45:67:89:ab') }
+ let(:xpath) { "//a[text()='#{mac_address}'][@href='/db/mac_addresses/#{mac_address.id}']" }
+
+ it 'must return status 200 and include link to a mac address in the body' do
+ visit '/db/mac_addresses'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/mac_addresses/:id' do
+ let!(:mac_address) { Ronin::DB::MACAddress.find_or_import('01:23:45:67:89:ab') }
+ let(:header_xpath) { "//h1[text()='MAC Address: #{mac_address}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/mac_addresses/#{mac_address.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/open_ports' do
+ let!(:port) { Ronin::DB::Port.find_or_create_by(number: 11) }
+ let!(:ip_address) { Ronin::DB::IPAddress.find_or_import('1.2.3.4') }
+ let!(:open_port) { Ronin::DB::OpenPort.find_or_create_by(port: port, ip_address: ip_address) }
+ let(:xpath) { "//a[text()='#{open_port}'][@href='/db/open_ports/#{open_port.id}']" }
+
+ it 'must return status 200 and include link to a mac address in the body' do
+ visit '/db/open_ports'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/open_ports/:id' do
+ let!(:port) { Ronin::DB::Port.find_or_create_by(number: 11) }
+ let!(:ip_address) { Ronin::DB::IPAddress.find_or_import('1.2.3.4') }
+ let!(:open_port) { Ronin::DB::OpenPort.find_or_create_by(port: port, ip_address: ip_address) }
+ let(:header_xpath) { "//h1[text()='Open Port: #{open_port}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/open_ports/#{open_port.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/ports' do
+ let!(:port) { Ronin::DB::Port.find_or_create_by(number: 11) }
+ let(:xpath) { "//a[text()='#{port}'][@href='/db/ports/#{port.id}']" }
+
+ it 'must return status 200 and include link to a port in the body' do
+ visit '/db/ports'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/ports/:id' do
+ let!(:port) { Ronin::DB::Port.find_or_create_by(number: 11) }
+ let(:header_xpath) { "//h1[text()='Port: #{port}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/ports/#{port.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/services' do
+ let!(:service) { Ronin::DB::Service.find_or_import('https') }
+ let(:xpath) { "//a[text()='#{service}'][@href='/db/services/#{service.id}']" }
+
+ it 'must return status 200 and include link to a services in the body' do
+ visit '/db/services'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/services/:id' do
+ let!(:service) { Ronin::DB::Service.find_or_import('https') }
+ let(:header_xpath) { "//h1[text()='Service: #{service}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/services/#{service.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/vulns' do
+ let!(:url) { Ronin::DB::URL.find_or_import('http://example.com') }
+ let!(:vuln) { Ronin::DB::WebVuln.find_or_create_by(url: url, type: 'reflected_xss', query_param: 'cat', request_method: :get) }
+ let(:xpath) { "//a[text()='#{vuln.url}'][@href='/db/vulns/#{vuln.id}']" }
+
+ it 'must return status 200 and include link to a vulns in the body' do
+ visit '/db/vulns'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/vulns/:id' do
+ let!(:url) { Ronin::DB::URL.find_or_import('http://example.com') }
+ let!(:vuln) { Ronin::DB::WebVuln.find_or_create_by(url: url, type: 'reflected_xss', query_param: 'cat', request_method: :get) }
+ let(:header_xpath) { "//h1[text()='Vulnerability: #{vuln.url}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/vulns/#{vuln.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/urls' do
+ let!(:url) { Ronin::DB::URL.find_or_import('http://example.com') }
+ let(:xpath) { "//a[text()='#{url}'][@href='/db/urls/#{url.id}']" }
+
+ it 'must return status 200 and include link to a url in the body' do
+ visit '/db/urls'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/urls/:id' do
+ let!(:url) { Ronin::DB::URL.find_or_import('http://example.com') }
+ let(:header_xpath) { "//h1[text()='URL: #{url}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/urls/#{url.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/url_schemes' do
+ let!(:url_scheme) { Ronin::DB::URLScheme.find_or_create_by(name: 'https') }
+ let(:xpath) { "//a[text()='#{url_scheme}'][@href='/db/url_schemes/#{url_scheme.id}']" }
+
+ it 'must return status 200 and include link to a url scheme in the body' do
+ visit '/db/url_schemes'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/url_schemes/:id' do
+ let!(:url_scheme) { Ronin::DB::URLScheme.find_or_create_by(name: 'https') }
+ let(:header_xpath) { "//h1[text()='URL Scheme: #{url_scheme}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/url_schemes/#{url_scheme.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/url_query_param_names' do
+ let!(:url_query_param_name) { Ronin::DB::URLQueryParamName.find_or_create_by(name: 'cat') }
+ let(:xpath) { "//a[text()='#{url_query_param_name}'][@href='/db/url_query_param_names/#{url_query_param_name.id}']" }
+
+ it 'must return status 200 and include link to a url query param name in the body' do
+ visit '/db/url_query_param_names'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/url_query_param_names/:id' do
+ let!(:url_query_param_name) { Ronin::DB::URLQueryParamName.find_or_create_by(name: 'cat') }
+ let(:header_xpath) { "//h1[text()='URL Query Param Name: #{url_query_param_name}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/url_query_param_names/#{url_query_param_name.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/email_addresses' do
+ let!(:email_address) { Ronin::DB::EmailAddress.find_or_import('foo@example.com') }
+ let(:xpath) { "//a[text()='#{email_address}'][@href='/db/email_addresses/#{email_address.id}']" }
+
+ it 'must return status 200 and include link to an email address in the body' do
+ visit '/db/email_addresses'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/email_addresses/:id' do
+ let!(:email_address) { Ronin::DB::EmailAddress.find_or_import('foo@example.com') }
+ let(:header_xpath) { "//h1[text()='Email Address: #{email_address}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/email_addresses/#{email_address.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/user_names' do
+ let!(:user_name) { Ronin::DB::UserName.find_or_import('user_name') }
+ let(:xpath) { "//a[text()='#{user_name}'][@href='/db/user_names/#{user_name.id}']" }
+
+ it 'must return status 200 and include link to an user name in the body' do
+ visit '/db/user_names'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/user_names/:id' do
+ let!(:user_name) { Ronin::DB::UserName.find_or_import('user_name') }
+ let(:header_xpath) { "//h1[text()='User Name: #{user_name}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/user_names/#{user_name.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/passwords' do
+ let!(:password) { Ronin::DB::Password.find_or_import('password') }
+ let(:xpath) { "//a[text()='#{password}'][@href='/db/passwords/#{password.id}']" }
+
+ it 'must return status 200 and include link to a passwords in the body' do
+ visit '/db/passwords'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/passwords/:id' do
+ let!(:password) { Ronin::DB::Password.find_or_import('password') }
+ let(:header_xpath) { "//h1[text()='Password: #{password}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/passwords/#{password.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/credentials' do
+ let!(:credential) { Ronin::DB::Credential.find_or_import('user:password') }
+ let(:xpath) { "//a[text()='#{credential}'][@href='/db/credentials/#{credential.id}']" }
+
+ it 'must return status 200 and include link to a credential in the body' do
+ visit '/db/credentials'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/credentials/:id' do
+ let!(:credential) { Ronin::DB::Credential.find_or_import('user:password') }
+ let(:header_xpath) { "//h1[text()='Credential: #{credential}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/credentials/#{credential.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/advisories' do
+ let!(:advisory) { Ronin::DB::Advisory.find_or_import('ABC-2023:12-34') }
+ let(:xpath) { "//a[text()='#{advisory}'][@href='/db/advisories/#{advisory}']" }
+
+ it 'must return status 200 and include link to a advisory in the body' do
+ visit '/db/advisories'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/advisories/:id' do
+ let!(:advisory) { Ronin::DB::Advisory.find_or_import('ABC-2023:12-34') }
+ let(:header_xpath) { "//h1[text()='Advisory: #{advisory}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/advisories/#{advisory.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/software' do
+ let!(:software) { Ronin::DB::Software.find_or_create_by(name: 'software', version: 1) }
+ let(:xpath) { "//a[text()='#{software}'][@href='/db/software/#{software.id}']" }
+
+ it 'must return status 200 and include link to a software in the body' do
+ visit '/db/software'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/software/:id' do
+ let!(:software) { Ronin::DB::Software.find_or_create_by(name: 'software', version: 1) }
+ let(:header_xpath) { "//h1[text()='Software: #{software}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/software/#{software.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/software_vendors' do
+ let!(:software_vendor) { Ronin::DB::SoftwareVendor.find_or_create_by(name: 'software vendor') }
+ let(:xpath) { "//a[text()='#{software_vendor}'][@href='/db/software_vendors/#{software_vendor.id}']" }
+
+ it 'must return status 200 and include link to a software vendor in the body' do
+ visit '/db/software_vendors'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/software_vendors/:id' do
+ let!(:software_vendor) { Ronin::DB::SoftwareVendor.find_or_create_by(name: 'software vendor') }
+ let(:header_xpath) { "//h1[text()='Software Vendor: #{software_vendor}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/software_vendors/#{software_vendor.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/oses' do
+ let!(:os) { Ronin::DB::OS.find_or_create_by(name: 'os', version: 1) }
+ let(:xpath) { "//a[text()='#{os}'][@href='/db/oses/#{os.id}']" }
+
+ it 'must return status 200 and include link to a os in the body' do
+ visit '/db/oses'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/oses/:id' do
+ let!(:os) { Ronin::DB::OS.find_or_create_by(name: 'os', version: 1) }
+ let(:header_xpath) { "//h1[text()='OS: #{os}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/oses/#{os.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/phone_numbers' do
+ let!(:phone_number) { Ronin::DB::PhoneNumber.find_or_import('+1 123-456-7890') }
+ let(:xpath) { "//a[text()='#{phone_number}'][@href='/db/phone_numbers/#{phone_number.id}']" }
+
+ it 'must return status 200 and include link to a phone number in the body' do
+ visit '/db/phone_numbers'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/phone_numbers/:id' do
+ let!(:phone_number) { Ronin::DB::PhoneNumber.find_or_import('+1 123-456-7890') }
+ let(:header_xpath) { "//h1[text()='Phone Number: #{phone_number}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/phone_numbers/#{phone_number.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/street_addresses' do
+ let!(:street_address) { Ronin::DB::StreetAddress.find_or_create_by(address: 'Address', city: 'City', state: 'State', zipcode: '00-000', country: 'Country') }
+ let(:xpath) { "//a[text()='#{street_address}'][@href='/db/street_addresses/#{street_address.id}']" }
+
+ it 'must return status 200 and include link to a street address in the body' do
+ visit '/db/street_addresses'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/street_addresses/:id' do
+ let!(:street_address) { Ronin::DB::StreetAddress.find_or_create_by(address: 'Address', city: 'City', state: 'State', zipcode: '00-000', country: 'Country') }
+ let(:header_xpath) { "//h1[text()='Street Address: #{street_address}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/street_addresses/#{street_address.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/organizations' do
+ let!(:organization) { Ronin::DB::Organization.find_or_import('org_name') }
+ let(:xpath) { "//a[text()='#{organization}'][@href='/db/organizations/#{organization.id}']" }
+
+ it 'must return status 200 and include link to a organization in the body' do
+ visit '/db/organizations'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/organizations/:id' do
+ let!(:organization) { Ronin::DB::Organization.find_or_import('org_name') }
+ let(:header_xpath) { "//h1[text()='Organization: #{organization}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/organizations/#{organization.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+
+ context 'GET /db/people' do
+ let!(:person) { Ronin::DB::Person.find_or_import('full name') }
+ let(:xpath) { "//a[text()='#{person}'][@href='/db/people/#{person.id}']" }
+
+ it 'must return status 200 and include link to a person in the body' do
+ visit '/db/people'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(xpath)
+ end
+ end
+
+ context 'GET /db/people/:id' do
+ let!(:person) { Ronin::DB::Person.find_or_import('full name') }
+ let(:header_xpath) { "//h1[text()='Person: #{person}']" }
+
+ it 'must return status 200 and include records data' do
+ visit "/db/people/#{person.id}"
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_xpath(header_xpath)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fca56cc..76b4697 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,5 +2,14 @@
require 'rspec'
require 'simplecov'
+require 'rack/test'
+require 'capybara/rspec'
+require_relative '../app'
+
+RSpec.configure do |config|
+ config.include Rack::Test::Methods
+
+ Capybara.app = App
+end
SimpleCov.start
diff --git a/spec/validations/http_params_spec.rb b/spec/validations/http_params_spec.rb
new file mode 100644
index 0000000..bb5eed0
--- /dev/null
+++ b/spec/validations/http_params_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+require 'ronin/app/validations/http_params'
+
+describe Ronin::App::Validations::HTTPParams do
+ describe 'HTTPMethods' do
+ subject { described_class::HTTPMethods }
+
+ it "must map 'COPY' to :copy" do
+ expect(subject['COPY']).to eq(:copy)
+ end
+
+ it "must map 'DELETE' to :delete" do
+ expect(subject['DELETE']).to eq(:delete)
+ end
+
+ it "must map 'GET' to :get" do
+ expect(subject['GET']).to eq(:get)
+ end
+
+ it "must map 'HEAD' to :head" do
+ expect(subject['HEAD']).to eq(:head)
+ end
+
+ it "must map 'LOCK' to :lock" do
+ expect(subject['LOCK']).to eq(:lock)
+ end
+
+ it "must map 'MKCOL' to :mkcol" do
+ expect(subject['MKCOL']).to eq(:mkcol)
+ end
+
+ it "must map 'MOVE' to :move" do
+ expect(subject['MOVE']).to eq(:move)
+ end
+
+ it "must map 'OPTIONS' to :options" do
+ expect(subject['OPTIONS']).to eq(:options)
+ end
+
+ it "must map 'PATCH' to :patch" do
+ expect(subject['PATCH']).to eq(:patch)
+ end
+
+ it "must map 'POST' to :post" do
+ expect(subject['POST']).to eq(:post)
+ end
+
+ it "must map 'PROPFIND' to :propfind" do
+ expect(subject['PROPFIND']).to eq(:propfind)
+ end
+
+ it "must map 'PROPPATCH' to :proppatch" do
+ expect(subject['PROPPATCH']).to eq(:proppatch)
+ end
+
+ it "must map 'PUT' to :put" do
+ expect(subject['PUT']).to eq(:put)
+ end
+
+ it "must map 'TRACE' to :trace" do
+ expect(subject['TRACE']).to eq(:trace)
+ end
+
+ it "must map 'UNLOCK' to :unlock" do
+ expect(subject['UNLOCK']).to eq(:unlock)
+ end
+ end
+
+ describe "Versions" do
+ subject { described_class::Versions }
+
+ it "must map '1.0' to 1" do
+ expect(subject['1.0']).to eq(1)
+ end
+
+ it "must map '1.1' to 1.1" do
+ expect(subject['1.1']).to eq(1.1)
+ end
+
+ it "must map '1.2' to 1.2" do
+ expect(subject['1.2']).to eq(1.2)
+ end
+ end
+
+ describe "VerificationModes" do
+ subject { described_class::VerificationModes }
+
+ it "must map 'none' to :none" do
+ expect(subject['none']).to eq(:none)
+ end
+
+ it "must map 'peer' to :peer" do
+ expect(subject['peer']).to eq(:peer)
+ end
+
+ it "must map 'fail_if_no_peer_cert' to :fail_if_no_peer_cert" do
+ expect(subject['fail_if_no_peer_cert']).to eq(:fail_if_no_peer_cert)
+ end
+ end
+
+ describe "Headers" do
+ subject { described_class::Headers }
+
+ context "when given a String" do
+ it "must parse multiple 'Foo: bar' header lines into a Hash of names and values" do
+ string = <<~HEADERS
+ X-Foo: foo
+ X-Bar: bar
+ HEADERS
+
+ expect(subject[string]).to eq(
+ {
+ 'X-Foo' => 'foo',
+ 'X-Bar' => 'bar'
+ }
+ )
+ end
+
+ it "must group together repeated header names into an Array of values" do
+ string = <<~HEADERS
+ X-Foo: foo1
+ X-Foo: foo2
+ X-Bar: bar
+ HEADERS
+
+ expect(subject[string]).to eq(
+ {
+ 'X-Foo' => ['foo1', 'foo2'],
+ 'X-Bar' => 'bar'
+ }
+ )
+ end
+
+ context "when it's empty" do
+ it "must return nil" do
+ expect(subject['']).to eq(nil)
+ end
+ end
+
+ context "when it's whitespace" do
+ it "must return nil" do
+ expect(subject["\n \n"]).to eq(nil)
+ end
+ end
+ end
+ end
+
+ describe "rules" do
+ let(:method) { 'GET' }
+ let(:url) { 'https://example.com/' }
+
+ describe ":method" do
+ it "must require an :method key" do
+ result = subject.call({url: url})
+
+ expect(result).to be_failure
+ expect(result.errors[:method]).to eq(["is missing"])
+ end
+ end
+
+ describe ":url" do
+ it "must require an :url key" do
+ result = subject.call({method: 'GET'})
+
+ expect(result).to be_failure
+ expect(result.errors[:url]).to eq(["is missing"])
+ end
+ end
+
+ shared_examples_for "optional value" do |key|
+ let(:params) do
+ {method: method, url: url}
+ end
+
+ it "must coerce an empty value for #{key.inspect} into nil" do
+ result = subject.call(params.merge(key => ""))
+
+ expect(result).to be_success
+ expect(result[key]).to be(nil)
+ end
+ end
+
+ [
+ :body,
+ :headers,
+ :proxy,
+ :user_agent,
+ :user,
+ :password,
+ :cookie
+ ].each do |key|
+ include_context "optional value", key
+ end
+
+ shared_examples_for "optional :ssl value" do |key|
+ let(:params) do
+ {method: method, url: url}
+ end
+
+ it "must coerce an empty value for #{key.inspect} into nil" do
+ result = subject.call(params.merge(ssl: {key => ""}))
+
+ expect(result).to be_success
+ expect(result[:ssl][key]).to be(nil)
+ end
+ end
+
+ [
+ :timeout,
+ :version,
+ :min_version,
+ :max_version,
+ :verify,
+ :verify_depth,
+ :verify_hostname
+ ].each do |key|
+ include_context "optional :ssl value", key
+ end
+ end
+
+ describe ".call" do
+ subject { described_class }
+
+ it "must initialize #{described_class} and call #call" do
+ expect(subject.call({})).to be_kind_of(Dry::Validation::Result)
+ end
+ end
+end
diff --git a/spec/validations/masscan_params_spec.rb b/spec/validations/masscan_params_spec.rb
index 7af2c52..ce99256 100644
--- a/spec/validations/masscan_params_spec.rb
+++ b/spec/validations/masscan_params_spec.rb
@@ -5,14 +5,14 @@
describe "rules" do
describe ":ips" do
it "must require an :ips key" do
- result = subject.call({})
+ result = subject.call({ports: "80,443"})
expect(result).to be_failure
expect(result.errors[:ips]).to eq(["is missing"])
end
it "must require a non-empty value for :ips" do
- result = subject.call({ips: ""})
+ result = subject.call({ips: "", ports: "80,443"})
expect(result).to be_failure
expect(result.errors[:ips]).to eq(["is missing"])
@@ -21,7 +21,7 @@
it "must split the String value for :ips into an Array of Strings" do
ip1 = '192.168.1.1'
ip2 = '192.168.1.2'
- result = subject.call({ips: "#{ip1} #{ip2}"})
+ result = subject.call({ips: "#{ip1} #{ip2}", ports: "80,443"})
expect(result).to be_success
expect(result[:ips]).to eq([ip1, ip2])
@@ -29,9 +29,23 @@
end
describe ":ports" do
+ it "must require a :ports key" do
+ result = subject.call({ips: "192.168.1.1"})
+
+ expect(result).to be_failure
+ expect(result.errors[:ports]).to eq(["is missing"])
+ end
+
+ it "must require a non-empty value for :ports" do
+ result = subject.call({ips: "192.168.1.1", ports: ""})
+
+ expect(result).to be_failure
+ expect(result.errors[:ports]).to eq(["is missing"])
+ end
+
context "and when :ports is a valid masscan ports list" do
it "must return a valid result" do
- result = subject.call({ips: ['192.168.1.1'], ports: "1,2,3,4-10"})
+ result = subject.call({ips: '192.168.1.1', ports: "1,2,3,4-10"})
expect(result).to be_success
end
@@ -48,8 +62,12 @@
end
shared_examples_for "optional value" do |key|
+ let(:params) do
+ {ips: '192.168.1.1', ports: "80,443"}
+ end
+
it "must coerce an empty value for #{key.inspect} into nil" do
- result = subject.call({:ips => "192.168.1.1", key => ""})
+ result = subject.call(params.merge(key => ""))
expect(result).to be_success
expect(result[key]).to be(nil)
@@ -57,7 +75,6 @@
end
[
- :ports,
:banners,
:rate,
:config_file,
@@ -98,8 +115,12 @@
describe ".call" do
subject { described_class }
+ let(:params) do
+ {ips: '192.168.1.1', ports: "80,443"}
+ end
+
it "must initialize #{described_class} and call #call" do
- expect(subject.call({})).to be_kind_of(Dry::Validation::Result)
+ expect(subject.call(params)).to be_kind_of(Dry::Validation::Result)
end
end
end
diff --git a/spec/validations/nmap_params_spec.rb b/spec/validations/nmap_params_spec.rb
index d2e0681..3c5339c 100644
--- a/spec/validations/nmap_params_spec.rb
+++ b/spec/validations/nmap_params_spec.rb
@@ -15,7 +15,7 @@
describe ":ports" do
context "and when :ports is a valid nmap ports list" do
it "must return a valid result" do
- result = subject.call({ports: "1,2,3,4-10", targets: ['192.168.1.1']})
+ result = subject.call({ports: "1,2,3,4-10", targets: '192.168.1.1'})
expect(result).to be_success
end
diff --git a/views/about.erb b/views/about.erb
index 4977ca6..b3df6c8 100644
--- a/views/about.erb
+++ b/views/about.erb
@@ -1,4 +1,4 @@
-
<% end %>
diff --git a/views/exploits/show.erb b/views/exploits/show.erb
index db010b7..39ff904 100644
--- a/views/exploits/show.erb
+++ b/views/exploits/show.erb
@@ -13,6 +13,7 @@
Quality:
<%=h case @exploit.quality
+ when :untested then 'Untested'
when :testing then 'Testing'
when :poc then 'POC'
when :weaponized then 'Weaponized'
@@ -89,6 +90,16 @@
+ <% if defined?(Ronin::Exploits::Metadata::Shouts) &&
+ @exploit.include?(Ronin::Exploits::Metadata::Shouts) %>
+