Ecsypno Codename SCNR scripting interface
Powered by DSeL.
require 'scnr/engine/api'
require "#{Options.paths.root}/tmp/scripts/with_helpers/helpers"
SCNR::Engine::API.run do
Scan {
# Can also be written as:
#
# options.set(
# url: 'http://my-site.com',
# audit: {
# elements: [:links, :forms, :cookies, :ui_inputs, :ui_forms]
# },
# checks: ['*']
# )
Options {
set url: 'http://my-site.com',
audit: {
elements: [:links, :forms, :cookies, :ui_inputs, :ui_forms]
},
checks: ['*']
}
# Scan session configuration.
Session {
# Login using the #fill_in_and_submit_the_login_form method from the helpers.rb file.
to :login, :fill_in_and_submit_the_login_form
# Check for a valid session using the #find_welcome_message method from the helpers.rb file.
to :check, :find_welcome_message
}
# Scan scope configuration.
Scope {
# Limit the scope of the scan based on URL.
select :url, :within_the_eshop
# Limit the scope of the scan based on Element.
reject :element, :with_sensitive_action; also :with_weird_nonce
# Only select pages that are in the admin panel.
select :page, :in_admin_panel
# Limit the scope of the scan based on Page.
reject :page, :with_error
# Limit the scope of the scan based on DOM events and DOM elements.
# In this case, never click the logout button!
reject :event, :that_clicks_the_logout_button
}
# Run the scan and handle the results (in this case print to STDOUT) using #handle_results.
run! :handle_results
}
Logging {
# Error and exception handling.
on :error, :log_error
on :exception, :log_exception
}
Data {
# Don't store issues in memory, we'll send them to the DB.
issues.disable(:storage).on :new, :save_to_db
# Could also be written as:
#
# Issues {
# disable(:storage)
# on :new, :save_to_db)
# }
#
# Or:
#
# Issues { disable(:storage); on :new, :save_to_db) }
# Store every page in the DB too for later analysis.
pages.on :new, :save_to_db
# Or:
#
# Pages {
# on :new, :save_to_db
# }
}
Http {
on :request, :add_special_auth_header
on :response, :gather_traffic_data; also :increment_http_performer_count
}
Checks {
# Add a custom check on the fly to check for something simple specifically
# for this scan.
as :missing_important_header, with_missing_important_header_info,
:log_pages_with_missing_important_headers
}
# Been having trouble with this scan, collect some runtime statistics.
plugins.as :remote_debug, send_debugging_info_to_remote_server_info,
:send_debugging_info_to_remote_server
# Serves PHP scripts under the extension 'x'.
fingerprinters.as :php_x, :treat_x_as_php
Input {
# Vouchers and serial numbers need to come from an algorithm.
values :with_valid_role_id
}
Dom {
# Let's have a look inside the live JS env of those interesting pages,
# setup the data collection.
before :load, :start_js_data_gathering
after :load, :retrieve_js_data; also :event, :retrieve_event_js_data
}
end
# State
def log_error( error )
# ...
end
def log_exception( exception )
# ...
end
# Data
def save_to_db( obj )
# Do stufff...
end
def save_js_data_to_db( data, element, event )
# Do other stufff...
end
# Scope
def within_the_eshop( url )
url.path.start_with? '/eshop'
end
def with_error( page )
/Error/i.match? page.body
end
def in_admin_panel( page )
/Admin panel/i.match? page.body
end
def that_clicks_the_logout_button( event, element )
event == :click && element.tag_name == :button &&
element.attributes['id'] == 'logout'
end
def with_sensitive_action( element )
element.action.include? '/sensitive.php'
end
def with_weird_nonce( element )
element.inputs.include? 'weird_nonce'
end
# HTTP
def generate_request_header
# ...
end
def save_raw_http_response( response )
# ...
end
def save_raw_http_request( request )
# ...
end
def add_special_auth_header( request )
request.headers['Special-Auth-Header'] ||= generate_request_header
end
def increment_http_performer_count( response )
# Count the amount of requests/responses this system component has
# performed/received.
#
# Performers can be browsers, checks, plugins, session, etc.
stuff( response.request.performer.class )
end
def gather_traffic_data( response )
# Collect raw HTTP traffic data.
save_raw_http_response( response.to_s )
save_raw_http_request( response.request.to_s )
end
# Checks
def with_missing_important_header_info
{
name: 'Missing Important-Header',
description: %q{Checks pages for missing `Important-Header` headers.},
elements: [ Element::Server ],
issue: {
name: %q{Missing 'Important-Header' header},
severity: Severity::INFORMATIONAL
}
}
end
# This will run from the context of a Check::Base.
def log_pages_with_missing_important_headers
return if audited?( page.parsed_url.host ) ||
page.response.headers['Important-Header']
audited( page.parsed_url.host )
log(
vector: Element::Server.new( page.url ),
proof: page.response.headers_string
)
end
# Plugins
# This will run from the context of a Plugin::Base.
def send_debugging_info_to_remote_server
address = '192.168.0.11'
port = 81
auth = Utilities.random_seed
url = `start_remote_debug_server.sh -a #{address} -p #{port} --auth #{auth}`
url.strip!
http.post( url,
body: SCNR::Engine::SCNR::Engine::Options.to_h.to_json,
mode: :sync
)
while framework.running? && sleep( 5 )
http.post( "#{url}/statistics",
body: framework.statistics.to_json,
mode: :sync
)
end
end
def send_debugging_info_to_remote_server_info
{
name: 'Debugger'
}
end
# Fingerprinters
# This will run from the context of a Fingerprinter::Base.
def treat_x_as_php
return if extension != 'x'
platforms << :php
end
# Session
def fill_in_and_submit_the_login_form( browser )
browser.load "#{SCNR::Engine::SCNR::Engine::Options.url}/login"
form = browser.form
form.text_field( name: 'username' ).set 'john'
form.text_field( name: 'password' ).set 'doe'
form.input( name: 'submit' ).click
end
def find_welcome_message
http.get( SCNR::Engine::Options.url, mode: :sync ).body.include?( 'Welcome user!' )
end
# Inputs
def with_valid_code( name, current_value )
{
'voucher-code' => voucher_code_generator( current_value ),
'serial-number' => serial_number_generator( current_value )
}[name]
end
def with_valid_role_id( inputs )
return if !inputs.include?( 'role-type' )
inputs['role-id'] ||= (inputs['role-type'] == 'manager' ? 1 : 2)
inputs
end
# Browser
def start_js_data_gathering( page, browser )
return if !page.url.include?( 'something/interesting' )
browser.javascript.inject <<JS
// Gather JS data from listeners etc.
window.secretJSData = {};
JS
end
def retrieve_js_data( page, browser )
return if !page.url.include?( 'something/interesting' )
save_js_data_to_db(
browser.javascript.run( 'return window.secretJSData' ),
page, :load
)
end
def retrieve_event_js_data( event, element, browser )
return if !browser.url.include?( 'something/interesting' )
save_js_data_to_db(
browser.javascript.run( 'return window.secretJSData' ),
element, event
)
end
def handle_results( report, statistics )
puts
puts '=' * 80
puts
puts "[#{report.sitemap.size}] Sitemap:"
puts
report.sitemap.sort_by { |url, _| url }.each do |url, code|
puts "\t[#{code}] #{url}"
end
puts
puts '-' * 80
puts
puts "[#{report.issues.size}] Issues:"
puts
report.issues.each.with_index do |issue, idx|
s = "\t[#{idx+1}] #{issue.name} in `#{issue.vector.type}`"
if issue.vector.respond_to?( :affected_input_name ) &&
issue.vector.affected_input_name
s << " input `#{issue.vector.affected_input_name}`"
end
puts s << '.'
puts "\t\tAt `#{issue.page.dom.url}` from `#{issue.referring_page.dom.url}`."
if issue.proof
puts "\t\tProof:\n\t\t\t#{issue.proof.gsub( "\n", "\n\t\t\t" )}"
end
puts
end
puts
puts '-' * 80
puts
puts "Statistics:"
puts
puts "\t" << statistics.ai.gsub( "\n", "\n\t" )
end
Gu