Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

alt text

Introduction

Description

Spectre Scan is a modular, distributed, high-performance DAST web application security scanner framework, capable of analyzing the behavior and security of modern web applications and web APIs.

You can access Spectre Scan via multiple interfaces, such as:

Back-end support

A wide range of back-end technologies is supported, including:

  1. Operating systems
    1. BSD
    2. Linux
    3. Unix
    4. Windows
    5. Solaris
  2. Databases
    1. SQL
      1. MySQL
      2. PostreSQL
      3. MSSQL
      4. Oracle
      5. SQLite
      6. Ingres
      7. EMC
      8. DB2
      9. Interbase
      10. Informix
      11. Firebird
      12. MaxDB
      13. Sybase
      14. Frontbase
      15. HSQLDB
      16. Access
    2. NoSQL
      1. MongoDB
  3. Web servers
    1. Apache
    2. IIS
    3. Nginx
    4. Tomcat
    5. Jetty
    6. Gunicorn
  4. Programming languages
    1. PHP
    2. ASP
    3. ASPX
    4. Java
    5. Python
    6. Ruby
    7. Javascript
  5. Frameworks
    1. Rack
    2. CakePHP
    3. Rails
    4. Django
    5. ASP.NET MVC
    6. JSF
    7. CherryPy
    8. Nette
    9. Symfony
    10. NodeJS
    11. Express

This list keeps growing but new platforms or failure to fingerprint supported ones don’t disable the Spectre Scan engine, they merely force it to be more extensive in its scan.

Upon successful identification or configuration of platform types, the scan will be much more focused, less resource intensive and require less time to complete.

Front-end support

HTML5, modern Javascript APIs and modern DOM APIs are supported by basing their execution and analysis on Google Chromium.

Spectre Scan injects a custom environment to monitor JS objects and APIs in order to trace execution and data flows and thus provide highly in-depth reporting as to how a client-side security issue was identified which also greatly assists in its remediation.

Incremental scans

Save valuable time by re-scanning only what has changed, rather than running full scans every single time.

In order to save time on subsequent scans of the same target, Spectre Scan allows you to extract a session file from completed/aborted scans, in order to allow for incremental re-scans.

This means that only newly introduced input vectors will be audited the next time around, which saves immense amounts of time from your workflow.

For example, a seed (first) scan of a website that requires an hour to complete, can result in re-scan times of less that 10 minutes – depending on how many new input vectors were introduced.

Behavioral analysis

Spectre Scan will study the web application/service to identify how each input interacts with the front and back ends and tailor the audit for each specific input’s characteristics.

This results in highly self-optimized scans using less resources and requiring less time to complete, as well as less server stress.

Training also continues during the audit process and new inputs that may appear during that time will be incorporated into the scan in whole.

Extendability

Its modular architecture allows for easy augmentation when it comes to security checks, arbitrary custom functionality in the form of plugins and bespoke reporting.

Entities which perform tasks crucial to the operation of a web scanner have been abstracted to be components, more to be easily added by anyone in order to extend functionality.

Components are split into the following types:

  • Checks – Security checks.
    • Active – They actively engage the web application via its inputs.
    • Passive – They passively look for objects.
  • Plugins – Add arbitrary functionality to the system, accept options and run in parallel to the scan.
  • Reporters – They export the scan results in several formats.
  • Path extractors – They extract paths for the crawler to follow.
  • Fingerprinters – They identify OS version, platforms, servers, etc.

Customization

Furthermore, scripted scans allow for the creation of basically tailor made scans by moving decision making points and configuration to user-specified methods and can extend to even creating a custom scanner for any web application backed by the Spectre Scan engine.

The API is tidy and simple and easily allows you to plug-in to key API1 scan points in order to get the best results from any scan.

Scripts are written in Ruby and can thus be stored in your favorite CVS, this enables you to work side-by-side with the web application development team and have the right script revision alongside the respective web application revision.

Scalability

No dependencies, no configuration; Spectre Scan can build a cloud of itself that allows you to scale both horizontally and vertically.

Scale up by plugging more nodes to its Grid, or down by unplugging them.

Furthermore, with multi-Instance scans you can not only distribute multiple scans across nodes, but also individual scans, for super fast scanning of large sites.

Finally, with its quick suspend-to-disk/restore feature, running scans can easily be moved from node to node, accommodating highly optimized load-balancing and cost saving policies.

Deployment

Deployment options range from command-line utilities for direct scans, scripted scans (for configuration and custom scanners) as well as distributed deployments to perform scans from remote hosts and Grid/cloud/SaaS setups.

Its simple distributed architecture2 allows for easy creation of self-healing, load-balanced (vertically and horizontally) scanner grids; basically allowing for the creation of private scanner clouds in either yours or a Cloud provider’s infrastructure.

Conclusion

Thus, Spectre Scan can in essence fit into any SDLC with great grace, ease and little care.


  1. API/script functionality is provided by DSeL.

  2. Distributed functionality is provided by Cuboid.

Installation

For installation instructions please refer to the installer.

System requirements

Operating SystemArchitectureRAMDiskCPU
Linuxx86 64bit2GB4GBMulticore

Resource constrained environments

To optimize the resources a scan may use please consult:

In addition, Agents and other servers can have their max-slots adjusted to a user-specified value, instead of the default, which is auto and based on the aforementioned system requirements.

Please issue the -h flag to see available options for each executable in order to examine the applicable overrides.

Direct

The easiest approach is a direct scan using the spectre CLI executable.

To see all available options run:

bin/spectre -h

Example

The following command will run a scan with default settings against http://testhmtml5.vulnweb.com.

bin/spectre http://testhmtml5.vulnweb.com

Scripted

Scripted scans allow you to configure the system and take over decision making points for a much more fine-grained scan. Aside from that, scripts also allow you to quickly add custom components on the fly.

Scan scripts can either be a form of configuration or standalone scanners.

Examples

As configuration

With helpers

html5.config.rb:

SCNR::Application::API.run do
  require '/home/user/script/helpers'

  Dom {
    on :event, &method(:on_event_handler)
  }

  Checks {

    # This will run from the context of SCNR::Engine::Check::Base; it
    # basically creates a new check component on the fly.
    as :not_found, check_404_info, method(:check_404)
 
  }

  Plugins {

    # This will run from the context of SCNR::Engine::Plugin::Base; it
    # basically creates a new plugin component on the fly.
    as :my_plugin, my_plugin_info, method(:my_plugin)

  }

  Scan {

    Session {
      to :login, &method(:login)
      to :check, &method(:login_check)
    }

    Scope {
      # Don't visit resources that will end the session.
      reject :url, &method(:to_logout)
    }
  }

end

helpers.rb:

# Allow some time for the modal animation to complete in order for
# the login form to appear.
#
# (Not actually necessary, this is just an example on how to hande quirks.)
def on_event_handler( result, locator, event, options, browser )
  return if locator.attributes['href'] != '#myModal' || event != :click
  sleep 1
end

# Does something really simple, logs an issue for each 404 page.
def check_404
  response = page.response
  return if response.code != 404

  log(
    proof:    response.status_line,
    vector:   SCNR::Engine::Element::Server.new( response.url ),
    response: response
  )
end

def check_404_info
  {
    issue: {
      name:     'Page not found',
      severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
    }
  }
end

def my_plugin
  # Do stuff then wait until scan completes.
  wait_while_framework_running
  # Do stuff after scan completes.
end

def my_plugin_info
  {
    name: 'My Plugin',
    description: 'Just waits for the scan to finish,'
  }
end

def login( browser )
  # Login with whichever interface you prefer.
  watir    = browser.watir
  selenium = browser.selenium

  watir.goto SCNR::Engine::Options.url

  watir.link( href: '#myModal' ).click
  form = watir.form( id: 'loginForm' )

  form.text_field( name: 'username' ).set 'admin'
  form.text_field( name: 'password' ).set 'admin'
  form.submit
end

def login_check( &in_async_mode )
  http_client = SCNR::Engine::HTTP::Client
  check       = proc { |r| r.body.optimized_include? '<b>admin' }

  # If an async block is passed, then the framework would rather
  # schedule it to run asynchronously.
  if in_async_mode
    http_client.get SCNR::Engine::Options.url do |response|
      in_async_mode.call check.call( response )
    end
  else
    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
    check.call( response )
  end
end

def to_logout( url )
  url.path.optimized_include?( 'login' ) ||
    url.path.optimized_include?( 'logout' )
end

Single file

SCNR::Application::API.run do

    Dom {

        # Allow some time for the modal animation to complete in order for
        # the login form to appear.
        # 
        # (Not actually necessary, this is just an example on how to hande quirks.)
        on :event do |_, locator, event, *|
            next if locator.attributes['href'] != '#myModal' || event != :click
            sleep 1
        end
    }

    Checks {

        # This will run from the context of SCNR::Engine::Check::Base; it
        # basically creates a new check component on the fly.
        #
        # Does something really simple, logs an issue for each 404 page.
        as :not_found,
           issue: {
             name:     'Page not found',
             severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
           } do
            response = page.response
            next if response.code != 404

            log(
              proof:    response.status_line,
              vector:   SCNR::Engine::Element::Server.new( response.url ),
              response: response
            )
        end

    }

    Plugins {

        # This will run from the context of SCNR::Engine::Plugin::Base; it
        # basically creates a new plugin component on the fly.
        as :my_plugin do
            # Do stuff then wait until scan completes.
            wait_while_framework_running
            # Do stuff after scan completes.
        end

    }

    Scan {

        Session {
            to :login do |browser|
                # Login with whichever interface you prefer.
                watir    = browser.watir
                selenium = browser.selenium

                watir.goto SCNR::Engine::Options.url

                watir.link( href: '#myModal' ).click

                form = watir.form( id: 'loginForm' )
                form.text_field( name: 'username' ).set 'admin'
                form.text_field( name: 'password' ).set 'admin'
                form.submit
            end

            to :check do |async|
                http_client = SCNR::Engine::HTTP::Client
                check       = proc { |r| r.body.optimized_include? '<b>admin' }

                # If an async block is passed, then the framework would rather
                # schedule it to run asynchronously.
                if async
                    http_client.get SCNR::Engine::Options.url do |response|
                        success = check.call( response )
                        async.call success
                    end
                else
                    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
                    check.call( response )
                end
            end
        }

        Scope {
            # Don't visit resources that will end the session.
            reject :url do |url|
                url.path.optimized_include?( 'login' ) ||
                  url.path.optimized_include?( 'logout' )
            end
        }
    }

end

Supposing the above is saved as html5.config.rb:

bin/spectre http://testhtml5.vulnweb.com --script=html5.config.rb

Standalone

This basically creates a custom scanner.

The difference is that these scripts will run a scan and handle its results on their own, and not just serve as configuration.

With helpers

When a scan script is large-ish and/or complicated it’s better to split it into the main file and helper handler methods.

bin/spectre_script scanner.rb

scanner.rb:

require 'scnr/engine/api'

require "#{Options.paths.root}/tmp/scripts/with_helpers/helpers"

SCNR::Application::API.run do

  Scan {

    # Can also be written as:
    #
    # options.set(
    #   url:    'http://testhtml5.vulnweb.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

helpers.rb:

# 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

Single file

require 'scnr/engine/api'

# Mute output messages from the CLI interface, we've got our own output methods.
SCNR::UI::CLI::Output.mute

SCNR::Application::API.run do

    State {
        on :change do |state|
            puts "State\t\t- #{state.status.capitalize}"
        end
    }

    Data {
        Issues {
            on :new do |issue|
                puts "Issue\t\t- #{issue.name} from `#{issue.referring_page.dom.url}`" <<
                       " in `#{issue.vector.type}`."
            end
        }
    }

    Logging {
        on :error do |error|
            $stderr.puts "Error\t\t- #{error}"
        end

        # Way too much noise.
        # on :exception do |exception|
        #     ap exception
        #     ap exception.backtrace
        # end
    }

    Dom {

        # Allow some time for the modal animation to complete in order for
        # the login form to appear.
        # 
        # (Not actually necessary, this is just an example on how to hande quirks.)
        on :event do |_, locator, event, *|
            next if locator.attributes['href'] != '#myModal' || event != :click
            sleep 1
        end
    }

    Checks {

        # This will run from the context of SCNR::Engine::Check::Base; it
        # basically creates a new check component on the fly.
        #
        # Does something really simple, logs an issue for each 404 page.
        as :not_found,
           issue: {
             name:     'Page not found',
             severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
           } do
            response = page.response
            next if response.code != 404

            log(
              proof:    response.status_line,
              vector:   SCNR::Engine::Element::Server.new( response.url ),
              response: response
            )
        end

    }

    Plugins {

        # This will run from the context of SCNR::Engine::Plugin::Base; it
        # basically creates a new plugin component on the fly.
        as :my_plugin do
            puts "#{shortname}\t- Running..."
            wait_while_framework_running
            puts "#{shortname}\t- Done!"
        end

    }

    Scan {
        Options {
            set url:    'http://testhtml5.vulnweb.com',
                audit:  {
                  elements: [:links, :forms, :cookies]
                },
                checks: ['*']
        }

        Session {
            to :login do |browser|
                print "Session\t\t- Logging in..."

                # Login with whichever interface you prefer.
                watir    = browser.watir
                selenium = browser.selenium

                watir.goto SCNR::Engine::Options.url

                watir.link( href: '#myModal' ).click

                form = watir.form( id: 'loginForm' )
                form.text_field( name: 'username' ).set 'admin'
                form.text_field( name: 'password' ).set 'admin'
                form.submit

                if browser.response.body =~ /<b>admin/
                    puts 'done!'
                else
                    puts 'failed!'
                end
            end

            to :check do |async|
                print "Session\t\t- Checking..."

                http_client = SCNR::Engine::HTTP::Client
                check       = proc { |r| r.body.optimized_include? '<b>admin' }

                # If an async block is passed, then the framework would rather
                # schedule it to run asynchronously.
                if async
                    http_client.get SCNR::Engine::Options.url do |response|
                        success = check.call( response )

                        puts "logged #{success ? 'in' : 'out'}!"

                        async.call success
                    end
                else
                    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
                    success = check.call( response )

                    puts "logged #{success ? 'in' : 'out'}!"

                    success
                end
            end
        }

        Scope {
            # Don't visit resources that will end the session.
            reject :url do |url|
                url.path.optimized_include?( 'login' ) ||
                  url.path.optimized_include?( 'logout' )
            end
        }

        before :page do |page|
            puts "Processing\t- [#{page.response.code}] #{page.dom.url}"
        end

        on :page do |page|
            puts "Scanning\t- [#{page.response.code}] #{page.dom.url}"
        end

        after :page do |page|
            puts "Scanned\t\t- [#{page.response.code}] #{page.dom.url}"
        end

        run! do |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
    }

end

Supposing the above is saved as html5.scanner.rb:

bin/spectre_script html5.scanner.rb

Distributed

Distribution features are deferred to Cuboid; hence, a quick read through its readme will outline the architecture.

In this case, the Cuboid application is Spectre Scan.

Agent

To start an Agent run the spectre_agent CLI executable.

To see all available options run:

bin/spectre_agent -h

Each Agent should run on a different machine and its main role is to provide Instances to clients; each Instance is a scanner process.

The Agent will also split the available resources of the machine on which it runs into slots, with each slot corresponding to enough space for one Instance.

(To see how many slots a machine has you can use the spectre_system_info utility.)

Example

Server

In one terminal run:

bin/spectre_agent

The default port at the time of writing is 7331, so you should see something like:

I, [2022-01-23T09:54:21.849679 #1121060]  INFO -- System: RPC Server started.
I, [2022-01-23T09:54:21.849730 #1121060]  INFO -- System: Listening on 127.0.0.1:7331

Client

To start a scan originating from that Agent you must issue a spawn call in order to obtain an Instance; this can be achieved using the spectre_spawn CLI executable.

In another terminal run:

bin/spectre_spawn --agent-url=127.0.0.1:7331 http://testhtml5.vulnweb.com

The above will run a scan with the default options against http://testhtml5.vulnweb.com, originating from the Agent node.

The spectre_spawn utility largely accepts the same options as spectre.

If the Agent is out of slots you will see the following message:

[~] Agent is at maximum utilization, please try again later.

In which case you can keep retrying until a slot opens up.

Grid

A Grid is simply a group of Agents and its setup is as simple as specifying an already running Agent as a peer to a future Agent.

The order in which you start or specify peers is irrelevant, Agents will reach convergence on their own and keep track of their connectivity status with each other.

After a Grid is configured, when a spawn call is issued to any Grid member it will be served by any of its Agents based on the desired distribution strategy and not necessarily by the one receiving it.

Strategies

Horizontal (default)

spawn calls will be served by the least burdened Agent, i.e. the Agent with the least utilization of its slots.

This strategy helps to keep the overall Grid health good by spreading the workload across as many nodes as possible.

Vertical

spawn calls will be served by the most burdened Agent, i.e. the Agent with the most utilization of its slots.

This strategy helps to keep the overall Grid size (and thus cost) low by utilizing as few Grid nodes as possible.

It will also let you know if you have over-provisioned as extra nodes will not be receiving any workload.

Examples

Server

In one terminal run:

bin/spectre_agent

In another terminal run:

bin/spectre_agent --port=7332 --peer=127.0.0.1:7331

In another terminal run:

bin/spectre_agent --port=7333 --peer=127.0.0.1:7332

(It doesn’t matter who the peer is as long as it’s part of the Grid.)

Now we have a Grid of 3 Agents.

The point of course is to run each Agent on a different machine in real life.

Client

Same as Agent client.

Scheduler

To start a Scheduler run the spectre_scheduler CLI executable.

To see all available options run:

bin/spectre_scheduler -h

The main role of the Scheduler is to:

  1. Queue scans based on their assigned priority.
  2. Run them if there is an available slot.
  3. Monitor their progress.
  4. Grab and store reports once scans complete.

Default

By default, scans will run on the same machine as the Scheduler.

With Agent

When a Agent has been provided, spawn calls are going to be issued in order to acquire Instances to run the scans.

Grid

In the case where the given Agent is a Grid member, scans will be load-balanced across the Grid according the the configured strategy.

Examples

Server

In one terminal run:

bin/spectre_scheduler

Client

Pushing

In another terminal run:

bin/spectre_scheduler_push --scheduler-url=localhost:7331 http://testhtml5.vulnweb.com

Then you should see something like:

 [~] Pushed scan with ID: 5fed6c50f3699bacb841cc468cc97094

Monitoring

To see what the Scheduler is doing run:

bin/spectre_scheduler_list localhost:7331

Then you should see something like:

 [~] Queued [0]


 [*] Running [1]

[1] 5fed6c50f3699bacb841cc468cc97094: 127.0.0.1:3390/070116f5e2c0acaa0a6432acdcc7230a

 [+] Completed [0]


 [-] Failed [0]

If you run the same command after a while and the scan has completed:

 [~] Queued [0]


 [*] Running [0]


 [+] Completed [1]

[1] 5fed6c50f3699bacb841cc468cc97094: /home/username/.cuboid/reports/5fed6c50f3699bacb841cc468cc97094.crf

 [-] Failed [0]

Introspector

The Spectre Scan Introspector is basically middleware for your web application.

When this middleware is used, advanced execution and data flow information about the web application is gathered, allowing for easier identification and remediation of each identified issue.

Ruby

Install

gem install scnr-introspector

Use middleware

Options

OptionDescriptionDefaultExample
path_start_withOnly instrument classes whose path starts with this prefixnoneexample/
path_ends_withOnly instrument classes whose path ends with this suffixnoneapp.rb
path_include_patternsOnly instrument classes whose path matches all regex patternsnone.*service.*
path_exclude_patternsExclude classes matching whose path matches any regex patternsnone.*test.*

app.rb:

require 'scnr/introspector' # Include!
require 'sinatra/base'

class MyApp < Sinatra::Base
    # Use!
    use SCNR::Introspector, scope: {
      path_start_with: __FILE__
    }

    def noop
    end

    def process_params( params )
        noop
        params.values.join( ' ' )
    end

    get '/' do
        @instance_variable = {
            blah: 'foo'
        }
        local_variable = 1

        <<EOHTML
        #{process_params( params )}
        <a href="?v=stuff">XSS</a>
EOHTML
    end

    run!
end

Verify

Run the Web App:

bundle exec ruby examples/sinatra/app.rb

You should see this at the beginning:

[INTROSPECTOR] Spectre Scan Introspector Initialized.

Along with these types of messages:

[INTROSPECTOR] Injecting trace code for MyApp#process_params in examples/sinatra/app.rb:12

As an integration test, you can run:

curl -i http://localhost:4567/ -H "X-Scnr-Engine-Scan-Seed:Test" -H "X-Scnr-Introspector-Trace:1" -H "X-SCNR-Request-ID:1"

You should see something like this (the comments are the important part):

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 7055


<a href="?v=stuff">XSS</a>
<!-- Test
{"execution_flow":{"points":[{"path":"examples/sinatra/app.rb","line_number":17,"class_name":"MyApp","method_name":"GET /","event":"call","source":"    get '/' do\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":19,"class_name":"MyApp","method_name":"GET /","event":"line","source":"            blah: 'foo'\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":21,"class_name":"MyApp","method_name":"GET /","event":"line","source":"        local_variable = 1\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":23,"class_name":"MyApp","method_name":"GET /","event":"line","source":"        <<EOHTML\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":12,"class_name":"MyApp","method_name":"process_params","event":"call","source":"    def process_params( params )\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":13,"class_name":"MyApp","method_name":"process_params","event":"line","source":"        noop\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":9,"class_name":"MyApp","method_name":"noop","event":"call","source":"    def noop\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":14,"class_name":"MyApp","method_name":"process_params","event":"line","source":"        params.values.join( ' ' )\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":14,"class_name":"Hash","method_name":"values","event":"c_call","source":"        params.values.join( ' ' )\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"},{"path":"examples/sinatra/app.rb","line_number":14,"class_name":"Array","method_name":"join","event":"c_call","source":"        params.values.join( ' ' )\n","file_contents":"require 'scnr/introspector'\nrequire 'sinatra/base'\n\nclass MyApp < Sinatra::Base\n    use SCNR::Introspector, scope: {\n      path_start_with: __FILE__\n    }\n\n    def noop\n    end\n\n    def process_params( params )\n        noop\n        params.values.join( ' ' )\n    end\n\n    get '/' do\n        @instance_variable = {\n            blah: 'foo'\n        }\n        local_variable = 1\n\n        <<EOHTML\n        #{process_params( params )}\n        <a href=\"?v=stuff\">XSS</a>\nEOHTML\n    end\n\n    run!\nend\n"}]},"platforms":["ruby","linux"]}
-->

Java

Options

OptionDescriptionDefaultExample
path_start_withOnly instrument classes whose path starts with this prefixnonecom/example
path_ends_withOnly instrument classes whose path ends with this suffixnoneController
path_include_patternOnly instrument classes matching this regex patternnone.*Service.*
path_exclude_patternExclude classes matching this regex patternnone.*Test.*
source_directoryRoot directory containing source filessrc/main/java//path/to/src

Download

Download the latest JAR archive.

Install Middleware

webapp/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <filter>
        <filter-name>introspectorFilter</filter-name>
        <filter-class>com.ecsypno.introspector.middleware.IntrospectorFilter</filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>introspectorFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

Load Agent along with your webapp

MAVEN_OPTS="-javaagent:introspector.jar=path_start_with=com/example" mvn clean package tomcat7:run

Verify

You should see this at the beginning:

[INTROSPECTOR] Initializing Spectre Scan Introspector agent...
[INTROSPECTOR] Setting up instrumentation.

And messages like this after initialization for each traced line:

[INTROSPECTOR] Injecting trace code for com/example/XssServlet.<init> line 11 in src/main/java//com/example/XssServlet.java

Finally, for an integration test, to make sure:

curl -i http://localhost:8080/ -H "X-Scnr-Engine-Scan-Seed:Test" -H "X-Scnr-Introspector-Trace:1" -H "X-SCNR-Request-ID:1"

At the end of the HTTP response you should be seeing something like:

<!-- Test
{
    "platforms": ["java"],
    "execution_flow": {
        "points": [
            {
                "method_name": "doGet",
                "class_name": "com/example/SampleWebApp",
                "path": "src/main/java//com/example/SampleWebApp.java",
                "line_number": 16,
                "source": "        resp.setContentType(\"text/html\");",
                "file_contents": "package com.example;\n\nimport java.io.IOException;\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n@WebServlet(\"/\")\npublic class SampleWebApp extends HttpServlet {\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) \n            throws ServletException, IOException {\n\n        resp.setContentType(\"text/html\");\n\n        resp.getWriter().println(\"<html><body>\");\n        resp.getWriter().println(\"<ul>\");\n        resp.getWriter().println(\"<li><a href='/xss'>XSS</a></li>\");\n        resp.getWriter().println(\"<li><a href='/cmd'>OS Command Injection</a></li>\");\n        resp.getWriter().println(\"</ul>\");\n        resp.getWriter().println(\"</body></html>\");\n    }\n}"
            },
            [...]
        ]
    }
}
Test -->

.NET

Installation

Install Middleware

Add to your project:

dotnet add package Introspector.Web

Install patcher

dotnet tool install --global Introspector.CLI
introspector

Use in a Web Application

Ecsypno.TestApp.csproj:

  <ItemGroup>
    <PackageReference Include="Introspector.Web"/>
  </ItemGroup>

Ecsypno.TestApp.cs:

using Introspector.Web.Extensions;
using System.Web;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// Add Introspector middleware
app.UseIntrospector();

string ProcessQuery(string input)
{

    return input;
}

app.MapGet("/", () => "Hello, world!");
app.MapGet("/xss", (HttpContext context) =>
{
    var query = ProcessQuery(context.Request.Query["input"]);
    var response = $@"
        <html>
            <body>
                <h1>XSS Example</h1>
                <form method='get' action='/xss'>
                    <label for='input'>Input:</label>
                    <input type='text' id='input' name='input' value='{query}' />
                    <button type='submit'>Submit</button>
                </form>
                <p>{query}</p>
            </body>
        </html>";
    context.Response.ContentType = "text/html";
    return response;
});

app.Run();

dotnet run --project Ecsypno.TestApp -c Release

Should output [INTROSPECTOR] Spectre Scan Introspector middleware initialized. at the top.

Patch

dotnet build Ecsypno.TestApp -c Release # Build first.
introspector Ecsypno.TestApp/bin/Release/ --path-ends-with Ecsypno.TestApp.dll --path-exclude-pattern "ref|obj"
# Processing: Ecsypno.TestApp/bin/Release/net8.0/Ecsypno.TestApp.dll
# Instrumenting Program.<Main>$( args )
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:4
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:6
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:9
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:17
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:20
# Instrumenting Program.<Main>$ at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:39
# Instrumenting Program.<<Main>$>g__ProcessQuery|0_0( input )
# Instrumenting Program.<<Main>$>g__ProcessQuery|0_0 at /home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs:14

Verify

Run the Web App again:

dotnet run --project Ecsypno.TestApp -c Release --no-build

Should output:

[INTROSPECTOR] Patched assembly loaded: Ecsypno.TestApp/bin/Release/net8.0/Ecsypno.TestApp.dll
[INTROSPECTOR] Spectre Scan Introspector middleware initialized.
curl -i http://localhost:5055/xss?input=test -H "X-Scnr-Engine-Scan-Seed:Test" -H "X-Scnr-Introspector-Trace:1" -H "X-SCNR-Request-ID:1"

You should see something like this (the comments are the important part):

HTTP/1.1 200 OK
Content-Length: 2135
Content-Type: text/html
Date: Sat, 11 Jan 2025 10:22:46 GMT
Server: Kestrel


        <html>
            <body>
                <h1>XSS Example</h1>
                <form method='get' action='/xss'>
                    <label for='input'>Input:</label>
                    <input type='text' id='input' name='input' value='test' />
                    <button type='submit'>Submit</button>
                </form>
                <p>test</p>
            </body>
        </html>
<!-- Test
{
  "data_flow": [],
  "execution_flow": {
    "points": [
      {
        "class_name": "Program",
        "method_name": "\u003C\u003CMain\u003E$\u003Eg__ProcessQuery|0_0",
        "path": "/home/zapotek/workspace/scnr/dotnet-instrumentation-example/Ecsypno.TestApp/Program.cs",
        "line_number": 14,
        "source": "    return input;",
        "file_contents": "using Introspector.Web.Extensions;\nusing System.Web;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nvar app = builder.Build();\n\n// Add Introspector middleware\napp.UseIntrospector();\n\nstring ProcessQuery(string input)\n{\n\n    return input;\n}\n\napp.MapGet(\u0022/\u0022, () =\u003E \u0022Hello, world!\u0022);\n\n// Add an XSS example route with a form\napp.MapGet(\u0022/xss\u0022, (HttpContext context) =\u003E\n{\n    var query = ProcessQuery(context.Request.Query[\u0022input\u0022]);\n    var response = $@\u0022\n        \u003Chtml\u003E\n            \u003Cbody\u003E\n                \u003Ch1\u003EXSS Example\u003C/h1\u003E\n                \u003Cform method=\u0027get\u0027 action=\u0027/xss\u0027\u003E\n                    \u003Clabel for=\u0027input\u0027\u003EInput:\u003C/label\u003E\n                    \u003Cinput type=\u0027text\u0027 id=\u0027input\u0027 name=\u0027input\u0027 value=\u0027{query}\u0027 /\u003E\n                    \u003Cbutton type=\u0027submit\u0027\u003ESubmit\u003C/button\u003E\n                \u003C/form\u003E\n                \u003Cp\u003E{query}\u003C/p\u003E\n            \u003C/body\u003E\n        \u003C/html\u003E\u0022;\n    context.Response.ContentType = \u0022text/html\u0022;\n    return response;\n});\n\napp.Run();"
      }
    ]
  },
  "platforms": [
    "aspx"
  ]
}
Test -->

CLI

Command-line interface executables can be found under the bin/ directory and at the time of writing are:

Basic, Pro, Enterprise

  1. spectre – Direct scanning utility.
  2. spectre_reporter – Generates reports from .crf (Cuboid report file) and .ser (Spectre Scan report) report files.
  3. spectre_reproduce – Reproduces an issue(s) from a given report.
  4. spectre_restore – Restores a suspended scan based on a snapshot file.
  5. spectre_script – Runs a Ruby script under the context of SCNR::Engine.

Pro

  1. spectre_pro – Starts a Web interface server.

REST, SDLC, Enterprise

  1. spectre_rest_server – Starts a REST server.

MCP, SDLC, Enterprise

  1. spectre_mcp_server – Starts an MCP server.

Enterprise-only

  1. spectre_spawn – Issues spawn calls to Agents to start scans remotely.
  2. spectre_agent – Starts a Agent.
  3. spectre_scheduler – Starts a Scheduler.

Clients - no edition checks

  1. spectre_agent_monitor – Monitors an Agent.
  2. spectre_agent_unplug – Unplugs an Agent from its Grid.
  3. spectre_instance_connect – Utility to connect to an Instance.
  4. spectre_scheduler_attach – Attaches a detached Instance to the given Scheduler.
  5. spectre_scheduler_clear – Clears the Scheduler queue.
  6. spectre_scheduler_detach – Detaches an Instance from the Scheduler.
  7. spectre_scheduler_get – Retrieves information for a scheduled scan.
  8. spectre_scheduler_list – Lists information about all scans under the Scheduler’s control.
  9. spectre_scheduler_push – Scheduled a scan.
  10. spectre_scheduler_remove – Removes a scheduled scan from the queue.

License utilities

  1. spectre_activate
  2. spectre_edition
  3. spectre_available_seats
  4. spectre_license info

Other

  1. spectre_system_info – Presents system information about the host.

Ruby API

The Ruby API utilizes the DSeL DSL/API generator and runner and allows you to:

  1. Configure scans.
  2. Add custom components on the fly.
  3. Create custom scanners.

The API is separated into the following segments:

# Runs the DSL.
SCNR::Application::API.run do

    Data {
        Sitemap {}
        Urls {}
        Pages {}
        Issues {}
    }

    State { }

    Browserpool { }
    
    Dom { }

    Http { }

    Input { }

    Logging { }

    Checks { }

    Plugins { }

    Fingerprinters { }

    Scan {
        Options { }

        Scope { }

        Session { }
    }

end

Examples

As configuration

SCNR::Application::API.run do

    Dom {

        # Allow some time for the modal animation to complete in order for
        # the login form to appear.
        # 
        # (Not actually necessary, this is just an example on how to hande quirks.)
        on :event do |_, locator, event, *|
            next if locator.attributes['href'] != '#myModal' || event != :click
            sleep 1
        end
    }

    Checks {

        # This will run from the context of SCNR::Engine::Check::Base; it
        # basically creates a new check component on the fly.
        #
        # Does something really simple, logs an issue for each 404 page.
        as :not_found,
           issue: {
             name:     'Page not found',
             severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
           } do
            response = page.response
            next if response.code != 404

            log(
              proof:    response.status_line,
              vector:   SCNR::Engine::Element::Server.new( response.url ),
              response: response
            )
        end

    }

    Plugins {

        # This will run from the context of SCNR::Engine::Plugin::Base; it
        # basically creates a new plugin component on the fly.
        as :my_plugin do
            # Do stuff then wait until scan completes.
            wait_while_framework_running
            # Do stuff after scan completes.
        end

    }

    Scan {

        Session {
            to :login do |browser|
                # Login with whichever interface you prefer.
                watir    = browser.watir
                selenium = browser.selenium

                watir.goto SCNR::Engine::Options.url

                watir.link( href: '#myModal' ).click

                form = watir.form( id: 'loginForm' )
                form.text_field( name: 'username' ).set 'admin'
                form.text_field( name: 'password' ).set 'admin'
                form.submit
            end

            to :check do |async|
                http_client = SCNR::Engine::HTTP::Client
                check       = proc { |r| r.body.optimized_include? '<b>admin' }

                # If an async block is passed, then the framework would rather
                # schedule it to run asynchronously.
                if async
                    http_client.get SCNR::Engine::Options.url do |response|
                        success = check.call( response )
                        async.call success
                    end
                else
                    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
                    check.call( response )
                end
            end
        }

        Scope {
            # Don't visit resources that will end the session.
            reject :url do |url|
                url.path.optimized_include?( 'login' ) ||
                  url.path.optimized_include?( 'logout' )
            end
        }
    }

end

Supposing the above is saved as html5.config.rb:

bin/spectre http://testhtml5.vulnweb.com --script=html5.config.rb

Standalone

This basically creates a custom scanner.

require 'scnr/engine/api'

# Mute output messages from the CLI interface, we've got our own output methods.
SCNR::UI::CLI::Output.mute

SCNR::Application::API.run do

    State {
        on :change do |state|
            puts "State\t\t- #{state.status.capitalize}"
        end
    }

    Data {
        Issues {
            on :new do |issue|
                puts "Issue\t\t- #{issue.name} from `#{issue.referring_page.dom.url}`" <<
                       " in `#{issue.vector.type}`."
            end
        }
    }

    Logging {
        on :error do |error|
            $stderr.puts "Error\t\t- #{error}"
        end

        # Way too much noise.
        # on :exception do |exception|
        #     ap exception
        #     ap exception.backtrace
        # end
    }

    Dom {

        # Allow some time for the modal animation to complete in order for
        # the login form to appear.
        # 
        # (Not actually necessary, this is just an example on how to hande quirks.)
        on :event do |_, locator, event, *|
            next if locator.attributes['href'] != '#myModal' || event != :click
            sleep 1
        end
    }

    Checks {

        # This will run from the context of SCNR::Engine::Check::Base; it
        # basically creates a new check component on the fly.
        #
        # Does something really simple, logs an issue for each 404 page.
        as :not_found,
           issue: {
             name:     'Page not found',
             severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
           } do
            response = page.response
            next if response.code != 404

            log(
              proof:    response.status_line,
              vector:   SCNR::Engine::Element::Server.new( response.url ),
              response: response
            )
        end

    }

    Plugins {

        # This will run from the context of SCNR::Engine::Plugin::Base; it
        # basically creates a new plugin component on the fly.
        as :my_plugin do
            puts "#{shortname}\t- Running..."
            wait_while_framework_running
            puts "#{shortname}\t- Done!"
        end

    }

    Scan {
        Options {
            set url:    'http://testhtml5.vulnweb.com',
                audit:  {
                  elements: [:links, :forms, :cookies]
                },
                checks: ['*']
        }

        Session {
            to :login do |browser|
                print "Session\t\t- Logging in..."

                # Login with whichever interface you prefer.
                watir    = browser.watir
                selenium = browser.selenium

                watir.goto SCNR::Engine::Options.url

                watir.link( href: '#myModal' ).click

                form = watir.form( id: 'loginForm' )
                form.text_field( name: 'username' ).set 'admin'
                form.text_field( name: 'password' ).set 'admin'
                form.submit

                if browser.response.body =~ /<b>admin/
                    puts 'done!'
                else
                    puts 'failed!'
                end
            end

            to :check do |async|
                print "Session\t\t- Checking..."

                http_client = SCNR::Engine::HTTP::Client
                check       = proc { |r| r.body.optimized_include? '<b>admin' }

                # If an async block is passed, then the framework would rather
                # schedule it to run asynchronously.
                if async
                    http_client.get SCNR::Engine::Options.url do |response|
                        success = check.call( response )

                        puts "logged #{success ? 'in' : 'out'}!"

                        async.call success
                    end
                else
                    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
                    success = check.call( response )

                    puts "logged #{success ? 'in' : 'out'}!"

                    success
                end
            end
        }

        Scope {
            # Don't visit resources that will end the session.
            reject :url do |url|
                url.path.optimized_include?( 'login' ) ||
                  url.path.optimized_include?( 'logout' )
            end
        }

        before :page do |page|
            puts "Processing\t- [#{page.response.code}] #{page.dom.url}"
        end

        on :page do |page|
            puts "Scanning\t- [#{page.response.code}] #{page.dom.url}"
        end

        after :page do |page|
            puts "Scanned\t\t- [#{page.response.code}] #{page.dom.url}"
        end

        run! do |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
    }

end

Supposing the above is saved as html5.scanner.rb:

bin/spectre_script html5.scanner.rb

Data

Encapsulates functionality that has to do with the data of SCNR::Engine.

SCNR::Application::API.run do

    Data {
        Sitemap {}
        Urls {}
        Pages {}
        Issues {}
    }

end

Sitemap

SCNR::Application::API.run do

    Data {
        Sitemap {
            on :new do |entry|
                p entry
                # => { "http://example.com" => 200 }
                #           URL             => HTTP code
            end
        }
    }

end

Example

bin/spectre http://example.com/ --checks=- --script=sitemap.rb

Urls

SCNR::Application::API.run do

    Data {
        Urls {
            on :new do |url|
                p url
                # => "http://example.com"
            end
        }
    }

end

Example

bin/spectre http://example.com/ --checks=- --script=urls.rb

Pages

SCNR::Application::API.run do

    Data {
        Pages {
            on :new do |page|
                p page
                # => #<SCNR::Engine::Page:7240 @url="http://testhtml5.vulnweb.com/ajax/popular?offset=0" @dom=#<SCNR::Engine::Page::DOM:7260 @url="http://testhtml5.vulnweb.com/ajax/popular?offset=0" @transitions=1 @data_flow_sinks=0 @execution_flow_sinks=0>>
            end
        }
    }

end

Example

bin/spectre http://testhtml5.vulnweb.com/ --checks=- --script=pages.rb

Issues

SCNR::Application::API.run do

    Data {
        Issues {
            on :new do |issue|
                p issue
                # => #<SCNR::Engine::Issue:0x00007f8c50d825a0 @name="Allowed HTTP methods", @description="\nThere are a number of HTTP methods that can be used on a webserver (`OPTIONS`,\n`HEAD`, `GET`, `POST`, `PUT`, `DELETE` etc.).  Each of these methods perform a\ndifferent function and each have an associated level of risk when their use is\npermitted on the webserver.\n\nA client can use the `OPTIONS` method within a request to query a server to\ndetermine which methods are allowed.\n\nCyber-criminals will almost always perform this simple test as it will give a\nvery quick indication of any high-risk methods being permitted by the server.\n\nSCNR::Engine discovered that several methods are supported by the server.\n", @references={"Apache.org"=>"http://httpd.apache.org/docs/2.2/mod/core.html#limitexcept"}, @tags=["http", "methods", "options"], @severity=#<SCNR::Engine::Issue::Severity::Base:0x00007f8c50dccee8 @severity=:informational>, @remedy_guidance="\nIt is recommended that a whitelisting approach be taken to explicitly permit the\nHTTP methods required by the application and block all others.\n\nTypically the only HTTP methods required for most applications are `GET` and\n`POST`. All other methods perform actions that are rarely required or perform\nactions that are inherently risky.\n\nThese risky methods (such as `PUT`, `DELETE`, etc) should be protected by strict\nlimitations, such as ensuring that the channel is secure (SSL/TLS enabled) and\nonly authorised and trusted clients are permitted to use them.\n", @check={:name=>"Allowed methods", :description=>"Checks for supported HTTP methods.", :elements=>[SCNR::Engine::Element::Server], :cost=>1, :author=>"Tasos \"Zapotek\" Laskos <[email protected]>", :version=>"0.2", :shortname=>"allowed_methods"}, @vector=#<SCNR::Engine::Element::Server url="http://example.com/">, @proof="OPTIONS, GET, HEAD, POST", @referring_page=#<SCNR::Engine::Page:6560 @url="http://example.com/" @dom=#<SCNR::Engine::Page::DOM:6580 @url="http://example.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0>>, @platform_name=nil, @platform_type=nil, @page=#<SCNR::Engine::Page:6600 @url="http://example.com/" @dom=#<SCNR::Engine::Page::DOM:6620 @url="http://example.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0>>, @remarks={}, @trusted=true>
            end

            # Disables Issue storage.
            disable :storage
        }
    }

end

Example

bin/spectre http://example.com/ --checks=allowed_methods --script=issues.rb

State

SCNR::Application::API.run do

    State {
        on :change do |state|
            p state.status
            # => :preparing
        end
    }

end

Example

bin/spectre http://example.com/ --checks=- --script=state.rb

Browserpool

SCNR::Application::API.run do

    Browserpool {
      
        # When a job is queued.
        on :job do |job|
            p job
            # => #<SCNR::Engine::BrowserPool::Jobs::DOMExploration:6140 @resource=#<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false>
        end

        # When a job has completed.
        on :job_done do |job|
            p job
            # => #<SCNR::Engine::BrowserPool::Jobs::DOMExploration:6140 @resource=#<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0> time=2.805975399 timed_out=false>
        end
        
        # When a job has yielded a result.
        on :result do |result|
            p result
            # => #<SCNR::Engine::BrowserPool::Jobs::DOMExploration::Result:0x00007f51a167c218 @page=#<SCNR::Engine::Page:7340 @url="http://testhtml5.vulnweb.com/ajax/popular?offset=0" @dom=#<SCNR::Engine::Page::DOM:7360 @url="http://testhtml5.vulnweb.com/ajax/popular?offset=0" @transitions=1 @data_flow_sinks=0 @execution_flow_sinks=0>>, @job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration:7320 @resource= time= timed_out=false>>
        end
    }

end

Example

bin/spectre http://html5.vulnweb.com/ --checks=- --script=browserpool.rb

Dom

SCNR::Application::API.run do

    Dom {
      
        before :load do |resource, options, browser|
            p resource
            # => #<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0>
            p options
            # => {:take_snapshot=>true}
            p browser
            # => #<SCNR::Engine::BrowserPool::Worker pid= job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration:6140 @resource=#<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false> last-url=nil transitions=0>
        end
        
        before :event do |locator, event, options, browser|
            p locator
            # => <li class="active" id="popularLi">
            p event
            # => :click
            p options
            # => {}
            p browser
            # => #<SCNR::Engine::BrowserPool::Worker pid= job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration::EventTrigger:7760 @resource=#<SCNR::Engine::Page::DOM:7720 @url="http://testhtml5.vulnweb.com/#/popular" @transitions=17 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false> last-url="http://testhtml5.vulnweb.com/" transitions=17>
        end
        
        on :event do |success, locator, event, options, browser|
            p success
            # => true
            p locator
            # => <li class="active" id="popularLi">
            p event
            # => :click
            p options
            # => {}
            p browser
            # => #<SCNR::Engine::BrowserPool::Worker pid= job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration::EventTrigger:7760 @resource=#<SCNR::Engine::Page::DOM:7720 @url="http://testhtml5.vulnweb.com/#/popular" @transitions=17 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false> last-url="http://testhtml5.vulnweb.com/" transitions=17>
        end
        
        after :load do |resource, options, browser|
            p resource
            # => #<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0>
            p options
            # => {:take_snapshot=>true}
            p browser
            # => #<SCNR::Engine::BrowserPool::Worker pid= job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration:6140 @resource=#<SCNR::Engine::Page::DOM:6160 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false> last-url="http://testhtml5.vulnweb.com/" transitions=17>
        end
        
        after :event do |transition, locator, event, options, browser|
            p transition
            # => #<SCNR::Engine::Page::DOM::Transition:0x00007f50fc0739c0 @options={}, @event=:click, @element=<a data-scnr-engine-id="1270713017" href="#/popular">, @clock=nil, @time=0.036003384>
            p locator
            # => <a data-scnr-engine-id="1270713017" href="#/popular">
            p event
            # => :click
            p options
            # => {}
            p browser
            # => #<SCNR::Engine::BrowserPool::Worker pid= job=#<SCNR::Engine::BrowserPool::Jobs::DOMExploration::EventTrigger:7680 @resource=#<SCNR::Engine::Page::DOM:7620 @url="http://testhtml5.vulnweb.com/#/popular" @transitions=17 @data_flow_sinks=0 @execution_flow_sinks=0> time= timed_out=false> last-url="http://testhtml5.vulnweb.com/" transitions=17>
        end
        
    }

end

Example

bin/spectre http://html5.vulnweb.com/ --checks=- --script=dom.rb

Http

SCNR::Application::API.run do

    Http {
      
        on :request do |request|
            p request
            # => #<SCNR::Engine::HTTP::Request @id= @mode=async @method=get @url="https://wordpress.com/" @parameters={} @high_priority= @performer=#<SCNR::Engine::Framework (scanning) runtime=0.805773919 found-pages=0 audited-pages=0 issues=0 checks= plugins=autothrottle,healthmap,discovery,timing_attacks,uniformity>>
        end
        
        on :response do |response|
            p response
            # => #<SCNR::Engine::HTTP::Response:0x00007fd6e75923b8 ..>
        end
        
        on :cookies do |cookies|
            p cookies
            # => [#<SCNR::Engine::Element::Cookie (get) url="https://wordpress.com/start/?ref=logged-out-homepage-lp" action="https://wordpress.com/start/?ref=logged-out-homepage-lp" default-inputs={"country_code"=>"GR"} inputs={"country_code"=>"GR"} raw_inputs=[] >]
        end
        
        # Block to run after each HTTP request batch run.
        after :run do
        end
        
    }

end

Example

bin/spectre https://wordpress.com --checks=- --script=http.rb

Input

SCNR::Application::API.run do

    Input {

        # Fill-in values for the given element; must return Hash not alter the element.
        values do |element|
            p element
            # => #<SCNR::Engine::Element::Form (post) auditor=SCNR::Engine::Trainer::SinkTracer url="http://testhtml5.vulnweb.com/" action="http://testhtml5.vulnweb.com/login" default-inputs={"username"=>"admin", "password"=>"", "loginFormSubmit"=>""} inputs={"username"=>"admin", "password"=>"5543!%scnr_engine_secret", "loginFormSubmit"=>"1"} raw_inputs=[] >
            element.inputs
        end
   
    }

end

Example

bin/spectre https://testhtml5.vulnweb.com --checks=xss --script=input.rb

Logging

SCNR::Application::API.run do

    Logging {
      
        # Will get called for each error message that is logged.
        on :error do |error|
            p error
            # => "Error string"
        end

        # Will get called for each exception that is created, even if safely handled.
        on :exception do |exception|
            p exception
            # => #<SCNR::Engine::URICommon::Error: Failed to parse URL.>
        end
        
    }

end

Example

bin/spectre https://example.com --checks=- --script=logging.rb

Checks

SCNR::Application::API.run do

    Checks {
      
        # Will get called for each check that is run.
        on :run do |check|
            p check
            # => #<SCNR::Engine::Checks::BackupDirectories:0x00007f2d55c66bf8 @page=#<SCNR::Engine::Page:7920 @url="http://testhtml5.vulnweb.com/" @dom=#<SCNR::Engine::Page::DOM:7940 @url="http://testhtml5.vulnweb.com/" @transitions=0 @data_flow_sinks=0 @execution_flow_sinks=0>>>
        end

        # This will run from the context of SCNR::Engine::Check::Base; it
        # basically creates a new check component on the fly.
        #
        # This one does something really simple, logs an issue for each 404 page.
        as :not_found,
           issue: {
             name:     'Page not found',
             severity: SCNR::Engine::Issue::Severity::INFORMATIONAL
           } do
            response = page.response
            next if response.code != 404

            log(
              proof:    response.status_line,
              vector:   SCNR::Engine::Element::Server.new( response.url ),
              response: response
            )
        end

    }

end

Example

bin/spectre http://testhtml5.vulnweb.com --script=checks.rb

Plugins

SCNR::Application::API.run do

    Plugins {

        # Will get called upon plugin class initialization.
        on :initialize do |plugin|
            p plugin
            # => #<SCNR::Engine::Plugins::AutoThrottle:0x00007f8896ccacd8 @options={}>
        end

        # Will get called when each plugin's #prepare method is called.
        on :prepare do |plugin|
        end

        # Will get called when each plugin's #run method is called.
        on :run do |plugin|
        end

        # Will get called when each plugin's #clean_up method is called.
        on :clean_up do |plugin|
        end

        # Will get called when each plugin is done running.
        on :done do |plugin|
        end
        
        # This will run from the context of SCNR::Engine::Plugin::Base; it
        # basically creates a new plugin component on the fly.
        as :my_plugin do
            # Do stuff then wait until scan completes.
            wait_while_framework_running
            # Do stuff after scan completes.
        end

    }

end

Example

bin/spectre http://testhtml5.vulnweb.com --checks=- --script=plugins.rb

Fingerprinters

SCNR::Application::API.run do

    Fingerprinters {

        # Identify `*.x` resources as PHP.
        as :x_as_php do
            next unless extension == 'x'
            platforms << :php
        end
    }

end

Example

bin/spectre http://testhtml5.vulnweb.com --checks=- --script=fingerprinters.rb

Scan

Encapsulates functionality that has to do with the scan.

SCNR::Application::API.run do

    Scan {
        Options {}
        Scope {}
        Sesion {}
        
        # Called before each page audit.
        before :page do |page|
        end

        # Called on page audit.
        on :page do |page|
        end

        # Called after a page audit.
        after :page do |page|
        end
        
        # Perform the scan.
        run! do |report, statistics|
        end

        # Perform the scan.
        report, statistics = self.run
        
        # Get scan progress.
        progress = self.progress

        # Get scan progress updates for session :my_session (any user-provided session ID will do).
        progress = self.session_progress( :my_session )
        
        sitemap = self.sitemap

        status = self.status

        issues = self.issues

        statistics = self.statistics

        is_running  = self.running?
        is_scanning = self.scanning?

        # Pauses the scan.
        self.pause!
        # Resumes the scan.
        self.resume!
        # Aborts the scan.
        self.abort!
        # Suspends the scan.
        self.suspend!
        
        is_pausing    = self.pausing?
        is_paused     = self.paused?
        is_suspending = self.suspending?
        is_suspended  = self.suspended?
        
        # Restores a scan.
        self.restore!( snapshot_path )
        
        # Get a scan report.
        report = self.generate_report
    }

end

Example

SCNR::UI::CLI::Output.mute

api = SCNR::Application::API.new

api.scan.options.set url: 'http://testhtml5.vulnweb.com',
                     checks: %w(allowed_methods interesting_responses)

api.state.on :change do |state|
    puts "Status:"
    ap state.status
end

api.data.sitemap.on :new do |entry|
    puts "Sitemap entry:"
    ap entry
end
api.data.issues.on :new do |issue|
    puts "New issue:"
    ap issue
end

scan_thread = Thread.new { api.scan.run }

while scan_thread.alive?
    puts "Progress update:"
    ap api.scan.session_progress( :session )
    sleep 1
end

ap api.scan.generate_report

Assuming the above is saved as html5.scanner.rb:

bin/spectre_script html5.scanner.rb

Options

SCNR::Application::API.run do

    Scan {
        Options {

            # Sets options.
            set({
                  url: 'http://testhtml5.vulnweb.com',
                  audit: {
                    parameter_values: true,
                    paranoia: :medium,
                    exclude_vector_patterns: [],
                    include_vector_patterns: [],
                    link_templates: []
                  },
                  device: {
                    visible: false,
                    width: 1600,
                    height: 1200,
                    user_agent: "Mozilla/5.0 (Gecko) SCNR::Engine/v1.0dev",
                    pixel_ratio: 1.0,
                    touch: false
                  },
                  dom: {
                    engine: :chrome,
                    local_storage: {},
                    session_storage: {},
                    wait_for_elements: {},
                    pool_size: 4,
                    job_timeout: 60,
                    worker_time_to_live: 250,
                    wait_for_timers: false
                  },
                  http: {
                    request_timeout: 20000,
                    request_redirect_limit: 5,
                    request_concurrency: 10,
                    request_queue_size: 50,
                    request_headers: {},
                    response_max_size: 500000,
                    cookies: {},
                    authentication_type: "auto"
                  },
                  input: {
                    values: {},
                    default_values: {
                      "name" => "scnr_engine_name",
                      "user" => "scnr_engine_user",
                      "usr" => "scnr_engine_user",
                      "pass" => "5543!%scnr_engine_secret",
                      "txt" => "scnr_engine_text",
                      "num" => "132",
                      "amount" => "100",
                      "mail" => "[email protected]",
                      "account" => "12",
                      "id" => "1"
                    },
                    without_defaults: false,
                    force: false
                  },
                  scope: {
                    directory_depth_limit: 10,
                    auto_redundant_paths: 15,
                    redundant_path_patterns: {},
                    dom_depth_limit: 4,
                    dom_event_limit: 500,
                    dom_event_inheritance_limit: 500,
                    exclude_file_extensions: [],
                    exclude_path_patterns: [],
                    exclude_content_patterns: [],
                    include_path_patterns: [],
                    restrict_paths: [],
                    extend_paths: [],
                    url_rewrites: {}
                  },
                  session: {},
                  checks: [
                    "*"
                  ],
                  platforms: [],
                  plugins: {},
                  no_fingerprinting: false,
                  authorized_by: nil
            })
            
        }
    }

end

Scope

Determines which resources are in or out of scope. All return values will be cast to boolean.

SCNR::Application::API.run do

    Scan {
        Scope {
          
            select :url do |url|
            end

            select :page do |page|
            end

            select :element do |element|
            end

            select :event do |locator, event, options, browser|
            end

            reject :url do |url|
            end

            reject :page do |page|
            end

            reject :element do |element|
            end

            reject :event do |locator, event, options, browser|
            end

        }
    }

end

Example

bin/spectre http://testhtml5.vulnweb.com --checks=- --script=scope.rb

Session

SCNR::Application::API.run do

    Scan {

        Session {
          
            to :login do |browser|
                # Login with whichever interface you prefer.
                watir    = browser.watir
                selenium = browser.selenium

                watir.goto SCNR::Engine::Options.url

                watir.link( href: '#myModal' ).click

                form = watir.form( id: 'loginForm' )
                form.text_field( name: 'username' ).set 'admin'
                form.text_field( name: 'password' ).set 'admin'
                form.submit
            end

            to :check do |async|
                http_client = SCNR::Engine::HTTP::Client
                check       = proc { |r| r.body.optimized_include? '<b>admin' }

                # If an async block is passed, then the framework would rather
                # schedule it to run asynchronously.
                if async
                    http_client.get SCNR::Engine::Options.url do |response|
                        success = check.call( response )
                        async.call success
                    end
                else
                    response = http_client.get( SCNR::Engine::Options.url, mode: :sync )
                    check.call( response )
                end
            end
            
        }

    }

end

Example

bin/spectre http://testhtml5.vulnweb.com --checks=- --script=session.rb

REST API

Spectre Scan ships an HTTP/JSON REST surface for spawning, driving, and tearing down engine instances; pairing them with a Scheduler; and managing an Agent grid. It’s a thin layer on top of the same RPC plumbing the CLI and ui-pro use, so anything you can do locally you can do over the network.

Table of contents

Server

bin/spectre_rest_server          # starts the server (defaults to 127.0.0.1:7331)
bin/spectre_rest_server -h       # CLI options

Useful flags:

  • --address HOST / --port N – bind interface and port.
  • --username USER / --password PASS – enable HTTP Basic auth.
  • --ssl-ca / --server-ssl-private-key / --server-ssl-certificate – TLS termination + (with --ssl-ca) peer-cert verification.
  • --agent-url HOST:PORT – have the REST server hand instance spawning to an Agent (or grid) instead of forking locally.
  • --scheduler-url HOST:PORT – attach a Scheduler so POST /scheduler works.

Conventions

  • Content-Type: application/json everywhere, except GET /instances/:id/report.crf (binary application/octet-stream).
  • Session-bound: the server uses Rack::Session::Pool and remembers per-cookie state – GET /instances/:id/scan/progress returns deltas relative to what the calling cookie has already seen (issues / sitemap / errors). Hold the cookie across calls if you want cumulative output.
  • Errors: { "error": <Class>, "description": <message>, "backtrace": [<frame>, ...] } with status 5xx. 404 for unknown instance / scheduler / agent. 503 from POST /instances when the host is at max utilisation.

Authentication

Off by default. Pass --username and --password to enable HTTP Basic. With both set, every request must carry an Authorization: Basic … header or the server returns:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"

For TLS, see the SSL flags above. CA-signed peer verification is on automatically when --ssl-ca is provided.

Endpoints

Instances

MethodPathDescription
GET/instancesList spawned instances; map of instance_id → metadata.
POST/instancesSpawn a new instance. Body: spawn_instance.options – see below.
POST/instances/restoreSpawn a new instance from a saved scan session. Body: { "session": "<path or string>" }.
GET/instances/:idProgress envelope (status, busy, seed, statistics, messages, errors).
GET/instances/:id/summarySame as :id minus statistics (cheap to poll).
GET/instances/:id/report.crfCuboid native binary report (use the Report Ruby class to parse).
GET/instances/:id/report.jsonSame report as JSON.
PUT/instances/:id/schedulerHand the running instance over to the configured Scheduler.
PUT/instances/:id/pausePause an in-flight scan. Reverse with /resume.
PUT/instances/:id/resumeResume a paused scan.
DELETE/instances/:idAbort + shut down the instance. Idempotent.

POST /instances body — recommended minimum

{
  "url":     "http://example.com/",
  "checks":  ["*"],
  "audit":   { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] },
  "scope":   { "page_limit": 50 }
}

Or copy the spectre://option-presets/quick-scan preset verbatim and substitute the URL. The spectre://option-presets/full-scan preset is the same minus the scope.page_limit cap. Full key-by-key reference is the Options reference below.

Per-scan service (/instances/:id/scan/...)

Spectre-specific scan operations namespaced under each instance.

MethodPathDescription
GET/instances/:id/scan/progressIssues / sitemap / errors delta since this cookie’s last poll.
GET/instances/:id/scan/report.jsonFinal report as JSON (after status: done).
GET/instances/:id/scan/session{ "session": "<path>" } – snapshot for restore.

/scan/progress is the workhorse: poll it on a session-cookie- holding client and the response shrinks to “what’s new since you last asked.” Server-side state is keyed by (cookie, instance_id), so you can fan-poll multiple instances on one cookie.

Scheduler

Available only when the server is started with --scheduler-url HOST:PORT; otherwise the routes return 501.

MethodPathDescription
GET/schedulerStats + URL.
GET/scheduler/urlThe configured Scheduler URL.
PUT/scheduler/urlSet / change it. Body: { "url": "host:port" }.
DELETE/scheduler/urlDetach.
GET/scheduler/running{ <instance_id>: <info>, ... } for in-flight scans.
GET/scheduler/completedMap of completed scans → report path.
GET/scheduler/failedMap of failed scans → reason.
GET/scheduler/sizePending queue length.
DELETE/schedulerClear pending queue.
POST/schedulerPush a scan onto the queue. Body: same as POST /instances.
GET/scheduler/:instanceInfo for a queued / running instance.
PUT/scheduler/:instance/detachTake an instance back from the Scheduler’s care.
DELETE/scheduler/:instanceRemove a still-queued instance.

Agent

Available only when the server is started with --agent-url HOST:PORT; otherwise 501.

MethodPathDescription
GET/agent/urlThe configured Agent URL.
PUT/agent/urlSet / change. Body: { "url": "host:port" }.
DELETE/agent/urlDetach.

Grid

MethodPathDescription
GET/gridMember list + topology of the configured Agent grid.
GET/grid/:agentInfo for a single member by URL.
DELETE/grid/:agentUnplug a member.

Options reference

Same content is served at spectre://options/reference over MCP — single source of truth for both surfaces.

The full option surface accepted by spawn_instance.options (over MCP) and by the POST /instances body (over REST). Hash, all keys optional.

The bare engine defaults leave every audit element OFF and every check unloaded; only bin/spectre_scan (and the option presets) enable them. If you build options from scratch, ship at least url, audit.elements (or per-element booleans), and checks, or use spectre://option-presets/quick-scan.

Wire shape

This is what gets POSTed to /instances (REST) or sent as spawn_instance.options (MCP) — a single nested JSON object, all groups optional, every leaf documented further down. Each top-level key is its own JSON object (audit, scope, http, dom, device, input, session, timeout); the top-level scalars (url, checks, plugins, authorized_by, no_fingerprinting) sit alongside.

{
  "url":     "http://example.com/",
  "checks":  ["*"],
  "plugins": {},
  "authorized_by":     "[email protected]",
  "no_fingerprinting": false,

  "audit": {
    "elements":             ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"],
    "link_templates":       [],
    "parameter_values":     true,
    "parameter_names":      false,
    "with_raw_payloads":    false,
    "with_extra_parameter": false,
    "with_both_http_methods": false,
    "cookies_extensively":  false,
    "mode":                 "moderate",
    "exclude_vector_patterns": [],
    "include_vector_patterns": []
  },

  "scope": {
    "page_limit":                  50,
    "depth_limit":                 10,
    "directory_depth_limit":       10,
    "dom_depth_limit":             4,
    "dom_event_limit":             500,
    "dom_event_inheritance_limit": 500,
    "include_subdomains":          false,
    "https_only":                  false,
    "include_path_patterns":       [],
    "exclude_path_patterns":       [],
    "exclude_content_patterns":    [],
    "exclude_file_extensions":     ["gif","mp4","pdf","js","css"],
    "exclude_binaries":            false,
    "restrict_paths":              [],
    "extend_paths":                [],
    "redundant_path_patterns":     {},
    "auto_redundant_paths":        15,
    "url_rewrites":                {}
  },

  "http": {
    "request_concurrency":     10,
    "request_queue_size":      50,
    "request_timeout":         20000,
    "request_redirect_limit":  5,
    "response_max_size":       500000,
    "request_headers":         {},
    "cookies":                 {},
    "cookie_jar_filepath":     "/path/to/cookies.txt",
    "cookie_string":           "name=value; Path=/",
    "authentication_username": "user",
    "authentication_password": "pass",
    "authentication_type":     "auto",
    "proxy":                   "host:port",
    "proxy_host":              "host",
    "proxy_port":              8080,
    "proxy_username":          "user",
    "proxy_password":          "pass",
    "proxy_type":              "auto",
    "ssl_verify_peer":         false,
    "ssl_verify_host":         false,
    "ssl_certificate_filepath":"/path/to/cert.pem",
    "ssl_certificate_type":    "pem",
    "ssl_key_filepath":        "/path/to/key.pem",
    "ssl_key_type":            "pem",
    "ssl_key_password":        "secret",
    "ssl_ca_filepath":         "/path/to/ca.pem",
    "ssl_ca_directory":        "/path/to/ca-dir/",
    "ssl_version":             "tlsv1_3"
  },

  "dom": {
    "engine":              "chrome",
    "pool_size":           4,
    "job_timeout":         120,
    "worker_time_to_live": 1000,
    "wait_for_timers":     false,
    "local_storage":       {},
    "session_storage":     {},
    "wait_for_elements":   {}
  },

  "device": {
    "visible":     false,
    "width":       1600,
    "height":      1200,
    "user_agent":  "...",
    "pixel_ratio": 1.0,
    "touch":       false
  },

  "input": {
    "values":           {},
    "default_values":   {},
    "without_defaults": false,
    "force":            false
  },

  "session": {
    "check_url":     "https://example.com/account",
    "check_pattern": "Logout"
  },

  "timeout": {
    "duration": 3600,
    "suspend":  false
  }
}

In the per-key sections below, group.key is shorthand for the JSON path { "group": { "key": ... } }audit.elements means the elements field of the audit object, not a literal key called audit.elements.

Table of contents


Top-level

url

(string, required for a real scan)

The target. Anything reachable over HTTP(S). Required for any POST /instances (or spawn_instance with start: true); the only spawn path where it can be omitted is start: false (an idle instance set up to be configured later).

{ "url": "http://example.com/" }

checks

(string[], default: [] — no checks loaded)

Check shortnames or globs to load. Use ["*"] for the full catalogue (the bin/spectre_scan default). Examples:

  • ["xss*", "sql_injection*"] — XSS family + SQLi family.
  • ["xss"] — exactly the xss check.

Call the list_checks MCP tool (or bin/spectre_scan --list-checks) to enumerate the available shortnames + their severity / tags / element coverage.

{ "checks": ["xss*", "sql_injection*"] }

plugins

(object | string[] | string, default: {} — no plugins)

Plugins to load. Three accepted shapes:

{ "plugins": {} }                         // load nothing extra
{ "plugins": ["defaults/*"] }             // array of names / globs
{ "plugins": { "live": { "url": "..." } } } // hash with per-plugin options

The application always merges its default-plugin set in first; this key is purely for extras / overrides.

authorized_by

(string)

E-mail address of the authorising operator. Flows into outbound HTTP requests’ From header so target-site admins can identify the scan. Polite on third-party targets.

{ "authorized_by": "[email protected]" }

no_fingerprinting

(boolean, default: false)

Skip server / client tech fingerprinting. The fingerprint feeds platforms on each issue (tomcat,java, php,mysql, etc.) and narrows which checks run; turning it off speeds the start-up but loses platform-specific check skipping.

{ "no_fingerprinting": true }

audit

What the engine traces. All keys nest under the top-level "audit" object:

{ "audit": { "elements": ["links","forms"], "parameter_values": true } }

audit.elements

(string[])

Shortcut for the per-element booleans below. Pick from: links, forms, cookies, nested_cookies, headers, ui_inputs, ui_forms, jsons, xmls. Equivalent to setting each named boolean to true.

The presets ship the standard 8-element list (links, forms, cookies, headers, ui_inputs, ui_forms, jsons, xmls). nested_cookies is opt-in; link_templates is not an element — see below.

{ "audit": { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] } }

Per-element toggles

audit.links / audit.forms / audit.cookies / audit.headers / audit.jsons / audit.xmls / audit.ui_inputs / audit.ui_forms / audit.nested_cookies

(boolean)

Equivalent to listing the element name in audit.elements. Default on each is unset (nil), which the engine treats as off; bin/spectre_scan flips them on for the default 8.

{ "audit": { "links": true, "forms": true, "cookies": false } }

(regex[], default: [])

Regex patterns with named captures for extracting input info from REST-style paths. Example: (?<id>\d+) against /users/42 lets the engine treat 42 as the value of an id input. Not a boolean toggle — putting link_templates in audit.elements is an error.

{ "audit": { "link_templates": ["users/(?<id>\\d+)", "posts/(?<post_id>\\d+)"] } }

audit.parameter_values

(boolean, default: true)

Inject payloads into parameter values. Turning this off limits auditing to parameter names (with parameter_names: true) or extra-parameter injection — rarely what you want.

audit.parameter_names

(boolean, default: false)

Inject payloads into parameter names themselves. Catches mass-assignment / unintended-parameter classes of bug. Adds one extra mutation per known input.

audit.with_raw_payloads

(boolean, default: false)

Send payloads in raw form (no HTTP encoding). Useful when you suspect the target has a decoder that mangles encoded bytes.

audit.with_extra_parameter

(boolean, default: false)

Inject an additional, unexpected parameter into each element. Catches code paths that read undeclared parameters.

audit.with_both_http_methods

(boolean, default: false)

Audit each link / form with both GET and POST. Doubles audit time — only enable when the target’s behaviour is known to vary by method.

audit.cookies_extensively

(boolean, default: false)

Submit every link and form along with each cookie permutation. Severely increases scan time — useful when cookie state gates application behaviour.

audit.mode

(string, default: "moderate")

Audit aggressiveness. Values: light, moderate, aggressive. Higher modes try more payload variants per input.

audit.exclude_vector_patterns

(regex[], default: [])

Skip input vectors whose name matches any pattern. Example: ["^csrf$", "^_token$"] to leave anti-CSRF tokens alone.

audit.include_vector_patterns

(regex[], default: [])

Inverse of exclude_vector_patterns — only audit vectors whose name matches. Empty means “no whitelist.”


scope

Crawl bounds. All keys nest under "scope":

{ "scope": { "page_limit": 50, "include_subdomains": false } }

scope.page_limit

(int, default: nil — infinite)

Hard cap on crawled pages. The quick-scan preset sets this to 50; the full-scan preset omits it.

scope.depth_limit

(int, default: 10)

How deep to follow links from the seed. Counts every hop regardless of directory layout.

scope.directory_depth_limit

(int, default: 10)

How deep to descend into the URL path tree.

scope.dom_depth_limit

(int, default: 4)

How deep into the DOM tree of each JavaScript-rendered page. 0 disables browser analysis entirely.

scope.dom_event_limit

(int, default: 500)

Max DOM events triggered per DOM depth. Caps crawl time on event-heavy SPAs.

scope.dom_event_inheritance_limit

(int, default: 500)

How many descendant elements inherit a parent’s bound events.

scope.include_subdomains

(boolean, default: false)

Follow links to subdomains of the seed host.

scope.https_only

(boolean, default: false)

Refuse plaintext HTTP follow-throughs.

scope.include_path_patterns

(regex[], default: [])

Whitelist patterns for path segments. Empty = include all.

scope.exclude_path_patterns

(regex[], default: [])

Blacklist patterns. Pages whose paths match are skipped.

{ "scope": { "exclude_path_patterns": ["/logout", "/admin/.*"] } }

scope.exclude_content_patterns

(regex[], default: [])

Blacklist patterns for response body content. A page whose body matches gets dropped from the audit pool — useful for “don’t audit /logout” via response-side pattern.

scope.exclude_file_extensions

(string[])

Skip URLs ending in these extensions. Defaults to a long list of media / archive / executable / asset / document extensions (gif, mp4, pdf, js, css, …). Override if you need to audit something the default skips (e.g. force-include js for DOM analysis).

scope.exclude_binaries

(boolean, default: false)

Skip non-text-typed responses. Cheaper than maintaining a content-type allowlist; can confuse passive checks that pattern-match on bodies.

scope.restrict_paths

(string[], default: [])

Use these paths INSTEAD of crawling. Pre-seeded path discovery — the engine audits exactly what’s listed.

scope.extend_paths

(string[], default: [])

Add to whatever the crawler discovers. Useful for hidden URLs that aren’t linked from anywhere.

scope.redundant_path_patterns

(object: {regex: int}, default: {})

Pages matching the regex are crawled at most N times. Stops infinite-calendar / infinite-page traps.

{ "scope": { "redundant_path_patterns": { "calendar/\\d+": 1, "events/\\d+": 5 } } }

scope.auto_redundant_paths

(int, default: 15)

Follow URLs with the same query-parameter-name combination at most auto_redundant_paths times. Catches the ?page=1&offset=10, ?page=2&offset=20, … pattern without needing explicit redundant_path_patterns.

scope.url_rewrites

(object: {regex: string}, default: {})

Rewrite seed-discovered URLs before audit:

{ "scope": { "url_rewrites": { "articles/(\\d+)": "articles.php?id=\\1" } } }

http

HTTP client tuning. All keys nest under "http":

{ "http": { "request_concurrency": 5, "request_timeout": 30000 } }

Concurrency / queue / timeouts

  • http.request_concurrency (int, default: 10) — parallel requests in flight. The engine throttles down automatically if the target’s response time degrades.
  • http.request_queue_size (int, default: 50) — max requests queued client-side. Larger queue = better network utilisation, more RAM.
  • http.request_timeout (int, ms, default: 20000) — per-request timeout.
  • http.request_redirect_limit (int, default: 5) — max redirects to follow on each request.
  • http.response_max_size (int, bytes, default: 500000) — don’t download response bodies larger than this. Prevents runaway RAM on a target that streams large payloads.

Headers / cookies

  • http.request_headers (object, default: {}) — extra headers on every request:

    { "http": { "request_headers": { "X-API-Key": "abc123", "X-Debug": "1" } } }
    
  • http.cookies (object, default: {}) — preset cookies:

    { "http": { "cookies": { "session_id": "abc", "auth": "xyz" } } }
    
  • http.cookie_jar_filepath (string) — path to a Netscape-format cookie jar file.

  • http.cookie_string (string) — raw cookie string, Set-Cookie-style:

    { "http": { "cookie_string": "my_cookie=my_value; Path=/, other=other; Path=/test" } }
    

HTTP authentication

{ "http": {
    "authentication_username": "user",
    "authentication_password": "pass",
    "authentication_type":     "basic"
} }
  • http.authentication_username / http.authentication_password (string)
  • http.authentication_type (string, default: "auto") — explicit values: basic, digest, ntlm, negotiate, any, anysafe.

Proxy

{ "http": {
    "proxy":          "proxy.example.com:8080",
    "proxy_type":     "http",
    "proxy_username": "user",
    "proxy_password": "pass"
} }
  • http.proxy (string, "host:port" shortcut)
  • http.proxy_host / http.proxy_port — split form, overrides proxy if set.
  • http.proxy_username / http.proxy_password (string)
  • http.proxy_type (string, default: "auto")http, https, socks4, socks4a, socks5, socks5_hostname.

TLS / SSL

  • http.ssl_verify_peer / http.ssl_verify_host (boolean, default: false) — TLS peer / hostname verification. Off by default; both true for full chain validation.
  • http.ssl_certificate_filepath / http.ssl_certificate_type / http.ssl_key_filepath / http.ssl_key_type / http.ssl_key_password — client-cert auth. *_type values: pem, der, eng.
  • http.ssl_ca_filepath / http.ssl_ca_directory — custom CA bundle / directory for peer verification.
  • http.ssl_version (string) — pin a TLS version: tlsv1, tlsv1_0, tlsv1_1, tlsv1_2, tlsv1_3, sslv2, sslv3.
{ "http": {
    "ssl_verify_peer":          true,
    "ssl_verify_host":          true,
    "ssl_ca_filepath":          "/etc/ssl/cert.pem",
    "ssl_certificate_filepath": "/path/to/client.pem",
    "ssl_key_filepath":         "/path/to/client.key",
    "ssl_version":              "tlsv1_3"
} }

dom

Browser cluster + DOM crawl. All keys nest under "dom":

{ "dom": { "pool_size": 4, "job_timeout": 120, "wait_for_timers": true } }
  • dom.engine (string, default: "chrome") — browser engine. Chrome is the only supported value.

  • dom.pool_size (int, default: min(cpu_count/2, 10) || 1) — number of browser workers in the pool. More workers = faster DOM crawl on JS-heavy targets, more RAM.

  • dom.job_timeout (int, sec, default: 120) — per-page browser job ceiling. Pages that don’t settle are dropped from DOM-side analysis.

  • dom.worker_time_to_live (int, default: 1000) — re-spawn each browser after this many jobs. Caps memory leaks in long-lived headless instances.

  • dom.wait_for_timers (boolean, default: false) — wait for the longest setTimeout() on each page before considering DOM analysis “done”. Catches lazy-mounted UI.

  • dom.local_storage / dom.session_storage (object, default: {}) — pre-seed key/value maps:

    { "dom": {
        "local_storage":   { "user": "abc", "preferred_lang": "en" },
        "session_storage": { "csrf_token": "xyz" }
    } }
    
  • dom.wait_for_elements (object: {regex: css}, default: {}) — when navigating to a URL matching the key, wait for the CSS selector value to match before continuing:

    { "dom": { "wait_for_elements": {
        "/dashboard":  "#main-app .ready",
        "/settings/.*": "#settings-form"
    } } }
    

device

Browser viewport / identity. All keys nest under "device":

{ "device": { "width": 375, "height": 812, "touch": true, "pixel_ratio": 3.0 } }
  • device.visible (boolean, default: false) — show the browser window (head-ful mode). Massively slower; primarily for debugging login flows / interactive traps.
  • device.width / device.height (int) — viewport dimensions in CSS pixels.
  • device.user_agent (string) — override the User-Agent header / JS API.
  • device.pixel_ratio (float, default: 1.0) — device pixel ratio. Bump for high-DPI sniffing (some sites serve different markup at 2.0).
  • device.touch (boolean, default: false) — advertise as a touch device.

input

How inputs are auto-filled by the engine before mutation. All keys nest under "input":

{ "input": { "values": { "email": "[email protected]" }, "force": true } }
  • input.values (object: {regex: string}, default: {}) — match an input’s name against the regex key; use the value:

    { "input": { "values": {
        "email":          "[email protected]",
        "first_name":     "Scan",
        "creditcard|cc":  "4111111111111111"
    } } }
    
  • input.default_values (object) — layered under values — patterns the engine ships out of the box (first_name → “John”, etc.).

  • input.without_defaults (boolean, default: false) — skip the shipped default_values table; only your values get used.

  • input.force (boolean, default: false) — fill even non-empty inputs (overwrites pre-populated form fields).


session

Login-session monitoring. The engine periodically checks the target is still logged in. All keys nest under "session":

{ "session": {
    "check_url":     "https://example.com/account",
    "check_pattern": "Logout"
} }
  • session.check_url (string) — URL whose response body should match check_pattern while the session is valid.
  • session.check_pattern (regex) — matched against check_url’s body. Mismatch = session expired; the scan halts pending re-login.

Both fields are required to enable session monitoring; setting only one is rejected at validation time.


timeout

Wall-clock cap on the run. All keys nest under "timeout":

{ "timeout": { "duration": 3600, "suspend": true } }
  • timeout.duration (int, sec) — stop the scan after this many seconds.
  • timeout.suspend (boolean, default: false) — when the timeout fires, suspend to a snapshot file (loadable later via POST /instances/restore). Without this the run is aborted.

Quick start

Smallest working flow – spawn, poll, fetch report, tear down. Uses curl and assumes the server is at 127.0.0.1:7331.

# 1. Spawn an instance.
IID=$(curl -sS -c /tmp/cookies -b /tmp/cookies \
    -X POST http://127.0.0.1:7331/instances \
    -H 'Content-Type: application/json' \
    --data '{
        "url":     "http://testfire.net/",
        "checks":  ["*"],
        "audit":   { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] },
        "scope":   { "page_limit": 50 }
    }' | jq -r '.id')

# 2. Poll progress (deltas only, thanks to the cookie jar).
while true; do
    PROGRESS=$(curl -sS -c /tmp/cookies -b /tmp/cookies \
        http://127.0.0.1:7331/instances/$IID/scan/progress)
    BUSY=$(curl -sS -c /tmp/cookies -b /tmp/cookies \
        http://127.0.0.1:7331/instances/$IID | jq -r '.busy')
    [ "$BUSY" = "false" ] && break
    sleep 5
done

# 3. Final report.
curl -sS -c /tmp/cookies -b /tmp/cookies \
    http://127.0.0.1:7331/instances/$IID/scan/report.json > report.json

# 4. Tear down.
curl -sS -c /tmp/cookies -b /tmp/cookies \
    -X DELETE http://127.0.0.1:7331/instances/$IID

Client (Ruby)

A minimal Typhoeus-based client. The cookie jar is what makes progress deltas work.

require 'json'
require 'tmpdir'
require 'typhoeus'

COOKIES = "#{Dir.tmpdir}/cookiejar.txt"

def request( method, resource, params = nil )
    options = { cookiejar: COOKIES, cookiefile: COOKIES }
    if params
        if method == :get
            options[:params] = params
        else
            options[:body]    = params.to_json
            options[:headers] = { 'Content-Type' => 'application/json' }
        end
    end
    @last = Typhoeus.send(method, "http://127.0.0.1:7331/#{resource}", options)
end

def response_data
    JSON.load(@last.body)
end

# Spawn.
request :post, 'instances', {
    url:     'http://testfire.net/',
    checks:  ['*'],
    audit:   { elements: %w[links forms cookies headers ui_inputs ui_forms jsons xmls] },
    scope:   { page_limit: 50 }
}
iid = response_data['id']

# Poll until done.
loop do
    request :get, "instances/#{iid}/scan/progress"
    pp response_data    # deltas only

    request :get, "instances/#{iid}"
    break if !response_data['busy']

    sleep 1
end

# Report.
request :get, "instances/#{iid}/scan/report.json"
pp response_data

# Tear down.
request :delete, "instances/#{iid}"

Incremental rescans via sessions

/instances/:id/scan/session returns the path to a snapshot file that can be fed back to POST /instances/restore. The restored instance only audits new input vectors – huge speedup on re-scans of large apps.

# 1. First scan, fully audit the target.
request :post, 'instances', {
    url:    'https://ginandjuice.shop/',
    checks: ['*'],
    audit:  { elements: %w[links forms cookies headers ui_inputs ui_forms jsons xmls] }
}
iid = response_data['id']

# Poll-and-report helper (omitted; same as the Quick start example).
monitor_and_report(iid)

# 2. Save the session snapshot path.
request :get, "instances/#{iid}/scan/session"
session = response_data['session']

request :delete, "instances/#{iid}"

# 3. Re-spawn with `instances/restore`. New vectors only this time.
request :post, 'instances/restore', session: session
iid = response_data['id']

monitor_and_report(iid)

request :delete, "instances/#{iid}"

Status semantics

GET /instances/:id returns the same lifecycle states the MCP surface advertises: readypreparingscanningauditing → (paused/resumed) → cleanupdone (or aborted). busy flips to false only on done/aborted.

Things to know

  • Each spawned instance reserves engine resources up front (provisioned cores / RAM / disk). At capacity, POST /instances returns 503 – check /scheduler/size if you’ve configured a Scheduler so the request is queued instead.
  • Sessions are Rack::Session::Pool – in-memory, single-process. Fronting Spectre’s REST with multiple Pumas behind a load balancer requires a shared session store.
  • The REST server and the MCP server speak to the same engine instances. You can spawn over REST and inspect via MCP (or vice versa) – IDs are identical.

MCP

Spectre Scan ships a Model Context Protocol server so an AI client (Claude Desktop / Code, Cursor, Continue — anything that speaks MCP) can drive scans directly: spawn an Instance, watch its progress, fetch issues and reports, and tear it down again — over a single HTTP endpoint.

The full surface is exposed as MCP tools, prompts, and resources, and described to the client via the protocol’s own discovery calls (tools/list, prompts/list, resources/list). Whatever the model sees in its context is exactly what the surface advertises — the descriptions are the docs.

This page is the canonical reference. It is the only document an AI needs to understand and drive the surface end to end; everything else in this section either complements it or provides language bindings.

Table of contents

Server

To start the MCP server:

bin/spectre_mcp_server

To see CLI options:

bin/spectre_mcp_server -h

The transport is Streamable HTTP — every call is a JSON-RPC POST, optionally upgraded to a Server-Sent Events stream by the server. Authentication is configured in-application (see Auth below); there are no --username / --password flags.

Endpoint

A single URL — http://<host>:<port>/mcp. There is no per-instance sub-route; instance scoping is done by passing instance_id as an argument to every per-scan tool. One MCP server, one session per client.

serverInfo advertises { name: "spectre", version: "<release>" }, matching the running build. The brand and version are picked up automatically — there’s nothing to configure on the CLI.

Tools

The server flattens framework + scan tools into one tools/list response. Every tool that returns structured data declares an outputSchema; the response carries both content[0].text (JSON-encoded, for clients that don’t speak typed outputs) and structuredContent matching the schema (for clients that do).

Framework tools

ToolRequiredOptionalReturns (structuredContent)
list_instances{ instances: { <id>: { url } } }
spawn_instanceoptions, start=true, live=true{ instance_id, url, live? }
kill_instanceinstance_id{ killed: <id> }
list_checksseverities[], tags[]{ checks: [{ shortname, name, description, severity, elements[], tags[], platforms[] }] }
list_plugins{ plugins: [{ shortname, name, description, default, options[] }] }

list_checks is the catalog tool — call it BEFORE spawn_instance to discover what’s available and pick the shortnames you want to scope into options.checks. Filterable by severities (e.g. just high) or tags (e.g. xss, sqli). The response is sorted high-severity-first then by name.

list_plugins is the parallel catalog for plugins — shortname + name

  • description + per-plugin config schema. Plugins flagged default: true auto-load on every scan; you can name additional ones in options.plugins (array form: ["webhook_notify"]) or pass config inline (hash form: { "webhook_notify": { "url": "https://..." } }) using the keys in each plugin’s options[] array. The live plugin is intentionally hidden — it’s auto-attached by the MCP server when the session supports notifications, not a knob clients toggle.

spawn_instance.options is forwarded to instance.run(...). To spawn an Instance without running anything, pass start: false; passing options: {} does not skip the run.

live is on by default — when the call arrives over an MCP session that supports notifications, the server attaches a per-instance loopback receiver and the engine pushes every issue / sitemap entry / error / status change / final report back to the calling session as a brand-derived JSON-RPC notification. The response’s live sub-object tells the client which notification method to subscribe to (e.g. notifications/spectre/live). See Live events for the envelope shape and the end-to-end flow. Pass live: false to opt out and poll instead.

For the full options surface, read the spectre://options/reference resource (covered below) or the inlined Options reference further down this page.

Per-scan tools

Every per-scan tool requires instance_id. scan_progress is incremental via a caller-chosen session token — pass any string (typically a UUID) and the engine returns only items not previously emitted under that token. Reuse the same token across polls for the same logical view; pick a fresh one to start fresh. Without a token every poll returns the full set. The standalone scan_sitemap / scan_issues / scan_errors tools are direct one-shot fetches and take their own delta args (*_seen / *_since).

ToolRequiredOptionalReturns
scan_progressinstance_idsession, without_issues, without_errors, without_sitemap, without_statistics{ status, running, seed, statistics?, issues?, errors?, sitemap?, messages }
scan_reportinstance_id{ issues, sitemap, statistics, plugins }
scan_sitemapinstance_idsitemap_since=0{ sitemap: { <url>: <code> } }
scan_issuesinstance_idissues_seen=[]{ issues: { <digest>: <issue> } }
scan_errorsinstance_iderrors_since=0{ errors: [string] }
scan_pauseinstance_id{ status: 'paused' }
scan_resumeinstance_id{ status: 'resumed' }
scan_abortinstance_id{ status: 'aborted' }

Issue digests

Issue digest values are the keys of the returned issues hash (NOT a field nested inside the value) — unsigned 32-bit xxh32 integers, e.g. 3162940604. scan_issues accepts the digest array as integers or numeric strings (some JSON-RPC clients stringify large numbers); the server coerces. If you ever see the same issue stream back unchanged after passing it as issues_seen, a stringified-vs-int mismatch is the first thing to check.

Prompts

PromptRequiredDescription
quick_scan(url)urlCanned operator workflow for the bounded smoke test — expands into a 6-step user message that walks the AI through reading the options reference, building options from the quick-scan preset (scope.page_limit: 50 baked in), spawn_instance, polling scan_progress every 5 s using deltas, fetching scan_issues when status reaches done, and kill_instance-ing afterwards. Optional args: page_limit (override the default cap), checks, authorized_by, extra_options.
full_scan(url)urlSame shape as quick_scan minus the 50-page cap — drives a complete audit using the full-scan preset. Use when you want a thorough run and accept hours of polling. Optional args: checks, authorized_by, extra_options.

The expanded prompt body references resources by URI so the model has a clear pull path for the data — it doesn’t need to memorise option names.

Resources

URIMimeContents
spectre://glossarytext/markdownDomain terms (issue, digest, status, sitemap, statistics, check, scope, audit.elements). Read once before driving a scan.
spectre://options/referencetext/markdownConcrete keys for spawn_instance.options (url, scope, audit, checks, http, dom, plugins, authorized_by).
spectre://option-presets/quick-scanapplication/jsonJSON template — every audit element, every check, default plugins, scope.page_limit: 50 so a real-site smoke test finishes in minutes. Bump / drop the cap (or switch to full-scan) for a longer run.
spectre://option-presets/full-scanapplication/jsonSame shape as quick-scan minus the page cap — uncapped audit. Use when you want a complete run and accept a long wait.
spectre://how-to/optimize-scanstext/markdownHow to dial spawn_instance.options for a slow target, tight RAM, runaway crawls, JS-heavy apps, or a focused triage check set. MCP-flavoured port of How to ▸ Optimize scans.
spectre://how-to/maintain-a-valid-sessiontext/markdownHow to authenticate against a target behind a login wall — login_form, login_script, or external cookie-jar paths. MCP-flavoured port of How to ▸ Maintain a valid session.

Quick-scan preset:

{
  "url":     "<TARGET URL>",
  "checks":  ["*"],
  "audit":   { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] },
  "scope":   { "page_limit": 50 }
}

Full-scan preset (same minus scope):

{
  "url":    "<TARGET URL>",
  "checks": ["*"],
  "audit":  { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] }
}

Pulled in-band, this gives an AI client everything it needs to schematise spawn_instance.options without leaving the protocol.

Options reference

Same content is served at spectre://options/reference.

The full option surface accepted by spawn_instance.options. Hash, all keys optional.

The bare engine defaults leave every audit element OFF and every check unloaded; only bin/spectre_scan (and the option presets) enable them. If you build options from scratch, ship at least url, audit.elements (or per-element booleans), and checks, or use spectre://option-presets/quick-scan.

Wire shape

This is what gets sent as spawn_instance.options — a single nested JSON object, all groups optional, every leaf documented further down. Each top-level key is its own JSON object (audit, scope, http, dom, device, input, session, timeout); the top-level scalars (url, checks, plugins, authorized_by, no_fingerprinting) sit alongside.

{
  "url":     "http://example.com/",
  "checks":  ["*"],
  "plugins": {},
  "authorized_by":     "[email protected]",
  "no_fingerprinting": false,

  "audit": {
    "elements":             ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"],
    "link_templates":       [],
    "parameter_values":     true,
    "parameter_names":      false,
    "with_raw_payloads":    false,
    "with_extra_parameter": false,
    "with_both_http_methods": false,
    "cookies_extensively":  false,
    "mode":                 "moderate",
    "exclude_vector_patterns": [],
    "include_vector_patterns": []
  },

  "scope": {
    "page_limit":                  50,
    "depth_limit":                 10,
    "directory_depth_limit":       10,
    "dom_depth_limit":             4,
    "dom_event_limit":             500,
    "dom_event_inheritance_limit": 500,
    "include_subdomains":          false,
    "https_only":                  false,
    "include_path_patterns":       [],
    "exclude_path_patterns":       [],
    "exclude_content_patterns":    [],
    "exclude_file_extensions":     ["gif","mp4","pdf","js","css"],
    "exclude_binaries":            false,
    "restrict_paths":              [],
    "extend_paths":                [],
    "redundant_path_patterns":     {},
    "auto_redundant_paths":        15,
    "url_rewrites":                {}
  },

  "http": {
    "request_concurrency":     10,
    "request_queue_size":      50,
    "request_timeout":         20000,
    "request_redirect_limit":  5,
    "response_max_size":       500000,
    "request_headers":         {},
    "cookies":                 {},
    "cookie_jar_filepath":     "/path/to/cookies.txt",
    "cookie_string":           "name=value; Path=/",
    "authentication_username": "user",
    "authentication_password": "pass",
    "authentication_type":     "auto",
    "proxy":                   "host:port",
    "proxy_host":              "host",
    "proxy_port":              8080,
    "proxy_username":          "user",
    "proxy_password":          "pass",
    "proxy_type":              "auto",
    "ssl_verify_peer":         false,
    "ssl_verify_host":         false,
    "ssl_certificate_filepath":"/path/to/cert.pem",
    "ssl_certificate_type":    "pem",
    "ssl_key_filepath":        "/path/to/key.pem",
    "ssl_key_type":            "pem",
    "ssl_key_password":        "secret",
    "ssl_ca_filepath":         "/path/to/ca.pem",
    "ssl_ca_directory":        "/path/to/ca-dir/",
    "ssl_version":             "tlsv1_3"
  },

  "dom": {
    "engine":              "chrome",
    "pool_size":           4,
    "job_timeout":         120,
    "worker_time_to_live": 1000,
    "wait_for_timers":     false,
    "local_storage":       {},
    "session_storage":     {},
    "wait_for_elements":   {}
  },

  "device": {
    "visible":     false,
    "width":       1600,
    "height":      1200,
    "user_agent":  "...",
    "pixel_ratio": 1.0,
    "touch":       false
  },

  "input": {
    "values":           {},
    "default_values":   {},
    "without_defaults": false,
    "force":            false
  },

  "session": {
    "check_url":     "https://example.com/account",
    "check_pattern": "Logout"
  },

  "timeout": {
    "duration": 3600,
    "suspend":  false
  }
}

In the per-key sections below, group.key is shorthand for the JSON path { "group": { "key": ... } }audit.elements means the elements field of the audit object, not a literal key called audit.elements.

Table of contents


Top-level

url

(string, required for a real scan)

The target. Anything reachable over HTTP(S). Required for any spawn_instance with start: true; the only spawn path where it can be omitted is start: false (an idle instance set up to be configured later).

{ "url": "http://example.com/" }

checks

(string[], default: [] — no checks loaded)

Check shortnames or globs to load. Use ["*"] for the full catalogue (the bin/spectre_scan default). Examples:

  • ["xss*", "sql_injection*"] — XSS family + SQLi family.
  • ["xss"] — exactly the xss check.

Call the list_checks MCP tool (or bin/spectre_scan --list-checks) to enumerate the available shortnames + their severity / tags / element coverage.

{ "checks": ["xss*", "sql_injection*"] }

plugins

(object | string[] | string, default: {} — no plugins)

Plugins to load. Three accepted shapes:

{ "plugins": {} }                                    // load nothing extra
{ "plugins": ["defaults/*"] }                        // array of names / globs
{ "plugins": { "webhook_notify": { "url": "..." } } } // hash with per-plugin options

The application always merges its default-plugin set in first; this key is purely for extras / overrides.

authorized_by

(string)

E-mail address of the authorising operator. Flows into outbound HTTP requests’ From header so target-site admins can identify the scan. Polite on third-party targets.

{ "authorized_by": "[email protected]" }

no_fingerprinting

(boolean, default: false)

Skip server / client tech fingerprinting. The fingerprint feeds platforms on each issue (tomcat,java, php,mysql, etc.) and narrows which checks run; turning it off speeds the start-up but loses platform-specific check skipping.

{ "no_fingerprinting": true }

audit

What the engine traces. All keys nest under the top-level "audit" object:

{ "audit": { "elements": ["links","forms"], "parameter_values": true } }

audit.elements

(string[])

Shortcut for the per-element booleans below. Pick from: links, forms, cookies, nested_cookies, headers, ui_inputs, ui_forms, jsons, xmls. Equivalent to setting each named boolean to true.

The presets ship the standard 8-element list (links, forms, cookies, headers, ui_inputs, ui_forms, jsons, xmls). nested_cookies is opt-in; link_templates is not an element — see below.

{ "audit": { "elements": ["links","forms","cookies","headers","ui_inputs","ui_forms","jsons","xmls"] } }

Per-element toggles

audit.links / audit.forms / audit.cookies / audit.headers / audit.jsons / audit.xmls / audit.ui_inputs / audit.ui_forms / audit.nested_cookies

(boolean)

Equivalent to listing the element name in audit.elements. Default on each is unset (nil), which the engine treats as off; bin/spectre_scan flips them on for the default 8.

{ "audit": { "links": true, "forms": true, "cookies": false } }

(regex[], default: [])

Regex patterns with named captures for extracting input info from REST-style paths. Example: (?<id>\d+) against /users/42 lets the engine treat 42 as the value of an id input. Not a boolean toggle — putting link_templates in audit.elements is an error.

{ "audit": { "link_templates": ["users/(?<id>\\d+)", "posts/(?<post_id>\\d+)"] } }

audit.parameter_values

(boolean, default: true)

Inject payloads into parameter values. Turning this off limits auditing to parameter names (with parameter_names: true) or extra-parameter injection — rarely what you want.

audit.parameter_names

(boolean, default: false)

Inject payloads into parameter names themselves. Catches mass-assignment / unintended-parameter classes of bug. Adds one extra mutation per known input.

audit.with_raw_payloads

(boolean, default: false)

Send payloads in raw form (no HTTP encoding). Useful when you suspect the target has a decoder that mangles encoded bytes.

audit.with_extra_parameter

(boolean, default: false)

Inject an additional, unexpected parameter into each element. Catches code paths that read undeclared parameters.

audit.with_both_http_methods

(boolean, default: false)

Audit each link / form with both GET and POST. Doubles audit time — only enable when the target’s behaviour is known to vary by method.

audit.cookies_extensively

(boolean, default: false)

Submit every link and form along with each cookie permutation. Severely increases scan time — useful when cookie state gates application behaviour.

audit.mode

(string, default: "moderate")

Audit aggressiveness. Values: light, moderate, aggressive. Higher modes try more payload variants per input.

audit.exclude_vector_patterns

(regex[], default: [])

Skip input vectors whose name matches any pattern. Example: ["^csrf$", "^_token$"] to leave anti-CSRF tokens alone.

audit.include_vector_patterns

(regex[], default: [])

Inverse of exclude_vector_patterns — only audit vectors whose name matches. Empty means “no whitelist.”


scope

Crawl bounds. All keys nest under "scope":

{ "scope": { "page_limit": 50, "include_subdomains": false } }

scope.page_limit

(int, default: nil — infinite)

Hard cap on crawled pages. The quick-scan preset sets this to 50; the full-scan preset omits it.

scope.depth_limit

(int, default: 10)

How deep to follow links from the seed. Counts every hop regardless of directory layout.

scope.directory_depth_limit

(int, default: 10)

How deep to descend into the URL path tree.

scope.dom_depth_limit

(int, default: 4)

How deep into the DOM tree of each JavaScript-rendered page. 0 disables browser analysis entirely.

scope.dom_event_limit

(int, default: 500)

Max DOM events triggered per DOM depth. Caps crawl time on event-heavy SPAs.

scope.dom_event_inheritance_limit

(int, default: 500)

How many descendant elements inherit a parent’s bound events.

scope.include_subdomains

(boolean, default: false)

Follow links to subdomains of the seed host.

scope.https_only

(boolean, default: false)

Refuse plaintext HTTP follow-throughs.

scope.include_path_patterns

(regex[], default: [])

Whitelist patterns for path segments. Empty = include all.

scope.exclude_path_patterns

(regex[], default: [])

Blacklist patterns. Pages whose paths match are skipped.

{ "scope": { "exclude_path_patterns": ["/logout", "/admin/.*"] } }

scope.exclude_content_patterns

(regex[], default: [])

Blacklist patterns for response body content. A page whose body matches gets dropped from the audit pool — useful for “don’t audit /logout” via response-side pattern.

scope.exclude_file_extensions

(string[])

Skip URLs ending in these extensions. Defaults to a long list of media / archive / executable / asset / document extensions (gif, mp4, pdf, js, css, …). Override if you need to audit something the default skips (e.g. force-include js for DOM analysis).

scope.exclude_binaries

(boolean, default: false)

Skip non-text-typed responses. Cheaper than maintaining a content-type allowlist; can confuse passive checks that pattern-match on bodies.

scope.restrict_paths

(string[], default: [])

Use these paths INSTEAD of crawling. Pre-seeded path discovery — the engine audits exactly what’s listed.

scope.extend_paths

(string[], default: [])

Add to whatever the crawler discovers. Useful for hidden URLs that aren’t linked from anywhere.

scope.redundant_path_patterns

(object: {regex: int}, default: {})

Pages matching the regex are crawled at most N times. Stops infinite-calendar / infinite-page traps.

{ "scope": { "redundant_path_patterns": { "calendar/\\d+": 1, "events/\\d+": 5 } } }

scope.auto_redundant_paths

(int, default: 15)

Follow URLs with the same query-parameter-name combination at most auto_redundant_paths times. Catches the ?page=1&offset=10, ?page=2&offset=20, … pattern without needing explicit redundant_path_patterns.

scope.url_rewrites

(object: {regex: string}, default: {})

Rewrite seed-discovered URLs before audit:

{ "scope": { "url_rewrites": { "articles/(\\d+)": "articles.php?id=\\1" } } }

http

HTTP client tuning. All keys nest under "http":

{ "http": { "request_concurrency": 5, "request_timeout": 30000 } }

Concurrency / queue / timeouts

  • http.request_concurrency (int, default: 10) — parallel requests in flight. The engine throttles down automatically if the target’s response time degrades.
  • http.request_queue_size (int, default: 50) — max requests queued client-side. Larger queue = better network utilisation, more RAM.
  • http.request_timeout (int, ms, default: 20000) — per-request timeout.
  • http.request_redirect_limit (int, default: 5) — max redirects to follow on each request.
  • http.response_max_size (int, bytes, default: 500000) — don’t download response bodies larger than this. Prevents runaway RAM on a target that streams large payloads.

Headers / cookies

  • http.request_headers (object, default: {}) — extra headers on every request:

    { "http": { "request_headers": { "X-API-Key": "abc123", "X-Debug": "1" } } }
    
  • http.cookies (object, default: {}) — preset cookies:

    { "http": { "cookies": { "session_id": "abc", "auth": "xyz" } } }
    
  • http.cookie_jar_filepath (string) — path to a Netscape-format cookie jar file.

  • http.cookie_string (string) — raw cookie string, Set-Cookie-style:

    { "http": { "cookie_string": "my_cookie=my_value; Path=/, other=other; Path=/test" } }
    

HTTP authentication

{ "http": {
    "authentication_username": "user",
    "authentication_password": "pass",
    "authentication_type":     "basic"
} }
  • http.authentication_username / http.authentication_password (string)
  • http.authentication_type (string, default: "auto") — explicit values: basic, digest, ntlm, negotiate, any, anysafe.

Proxy

{ "http": {
    "proxy":          "proxy.example.com:8080",
    "proxy_type":     "http",
    "proxy_username": "user",
    "proxy_password": "pass"
} }
  • http.proxy (string, "host:port" shortcut)
  • http.proxy_host / http.proxy_port — split form, overrides proxy if set.
  • http.proxy_username / http.proxy_password (string)
  • http.proxy_type (string, default: "auto")http, https, socks4, socks4a, socks5, socks5_hostname.

TLS / SSL

  • http.ssl_verify_peer / http.ssl_verify_host (boolean, default: false) — TLS peer / hostname verification. Off by default; both true for full chain validation.
  • http.ssl_certificate_filepath / http.ssl_certificate_type / http.ssl_key_filepath / http.ssl_key_type / http.ssl_key_password — client-cert auth. *_type values: pem, der, eng.
  • http.ssl_ca_filepath / http.ssl_ca_directory — custom CA bundle / directory for peer verification.
  • http.ssl_version (string) — pin a TLS version: tlsv1, tlsv1_0, tlsv1_1, tlsv1_2, tlsv1_3, sslv2, sslv3.
{ "http": {
    "ssl_verify_peer":          true,
    "ssl_verify_host":          true,
    "ssl_ca_filepath":          "/etc/ssl/cert.pem",
    "ssl_certificate_filepath": "/path/to/client.pem",
    "ssl_key_filepath":         "/path/to/client.key",
    "ssl_version":              "tlsv1_3"
} }

dom

Browser cluster + DOM crawl. All keys nest under "dom":

{ "dom": { "pool_size": 4, "job_timeout": 120, "wait_for_timers": true } }
  • dom.engine (string, default: "chrome") — browser engine. Chrome is the only supported value.

  • dom.pool_size (int, default: min(cpu_count/2, 10) || 1) — number of browser workers in the pool. More workers = faster DOM crawl on JS-heavy targets, more RAM.

  • dom.job_timeout (int, sec, default: 120) — per-page browser job ceiling. Pages that don’t settle are dropped from DOM-side analysis.

  • dom.worker_time_to_live (int, default: 1000) — re-spawn each browser after this many jobs. Caps memory leaks in long-lived headless instances.

  • dom.wait_for_timers (boolean, default: false) — wait for the longest setTimeout() on each page before considering DOM analysis “done”. Catches lazy-mounted UI.

  • dom.local_storage / dom.session_storage (object, default: {}) — pre-seed key/value maps:

    { "dom": {
        "local_storage":   { "user": "abc", "preferred_lang": "en" },
        "session_storage": { "csrf_token": "xyz" }
    } }
    
  • dom.wait_for_elements (object: {regex: css}, default: {}) — when navigating to a URL matching the key, wait for the CSS selector value to match before continuing:

    { "dom": { "wait_for_elements": {
        "/dashboard":  "#main-app .ready",
        "/settings/.*": "#settings-form"
    } } }
    

device

Browser viewport / identity. All keys nest under "device":

{ "device": { "width": 375, "height": 812, "touch": true, "pixel_ratio": 3.0 } }
  • device.visible (boolean, default: false) — show the browser window (head-ful mode). Massively slower; primarily for debugging login flows / interactive traps.
  • device.width / device.height (int) — viewport dimensions in CSS pixels.
  • device.user_agent (string) — override the User-Agent header / JS API.
  • device.pixel_ratio (float, default: 1.0) — device pixel ratio. Bump for high-DPI sniffing (some sites serve different markup at 2.0).
  • device.touch (boolean, default: false) — advertise as a touch device.

input

How inputs are auto-filled by the engine before mutation. All keys nest under "input":

{ "input": { "values": { "email": "[email protected]" }, "force": true } }
  • input.values (object: {regex: string}, default: {}) — match an input’s name against the regex key; use the value:

    { "input": { "values": {
        "email":          "[email protected]",
        "first_name":     "Scan",
        "creditcard|cc":  "4111111111111111"
    } } }
    
  • input.default_values (object) — layered under values — patterns the engine ships out of the box (first_name → “John”, etc.).

  • input.without_defaults (boolean, default: false) — skip the shipped default_values table; only your values get used.

  • input.force (boolean, default: false) — fill even non-empty inputs (overwrites pre-populated form fields).


session

Login-session monitoring. The engine periodically checks the target is still logged in. All keys nest under "session":

{ "session": {
    "check_url":     "https://example.com/account",
    "check_pattern": "Logout"
} }
  • session.check_url (string) — URL whose response body should match check_pattern while the session is valid.
  • session.check_pattern (regex) — matched against check_url’s body. Mismatch = session expired; the scan halts pending re-login.

Both fields are required to enable session monitoring; setting only one is rejected at validation time.


timeout

Wall-clock cap on the run. All keys nest under "timeout":

{ "timeout": { "duration": 3600, "suspend": true } }
  • timeout.duration (int, sec) — stop the scan after this many seconds.
  • timeout.suspend (boolean, default: false) — when the timeout fires, suspend to a snapshot file (loadable later out of band). Without this the run is aborted.

Auth

Authentication is opt-in. When an embedder registers a bearer- token validator at boot, the server requires Authorization: Bearer <token> on every request and returns 401 otherwise (RFC 6750 — WWW-Authenticate: Bearer realm="MCP", error=…). Without a validator the server accepts unauthenticated traffic — fine for a loopback bind, dangerous on a public interface.

The resolved principal is stashed at env['cuboid.mcp.auth'] for any downstream middleware that wants to look it up.

Self-discovery flow

If you’re an AI seeing this server for the first time, do this once:

  1. initialize → check serverInfo.name (spectre) and version.
  2. resources/list → you’ll see four URIs. Read all four — they are tiny and answer most of the questions you’d otherwise have to ask. The glossary in particular grounds the field names you’ll see in scan_progress / scan_issues results.
  3. prompts/list → you’ll see quick_scan (capped 50-page smoke test) and full_scan (uncapped). If the user’s intent matches it (“scan this URL for issues”), use it: prompts/get with their URL gives you a full operator script.
  4. tools/list → discover the 12 tools. outputSchema on each tells you exactly what structuredContent to expect. list_checks (no instance_id required) hands back the full check catalog so you can scope spawn_instance.options.checks deliberately instead of defaulting to ["*"].
  5. Open the GET-SSE channel on /mcp (with the same mcp-session-id) to receive live events. The default spawn_instance call will start streaming on it — you do not need to poll unless you opt out of live with live: false.

After that, drive the scan with no further out-of-band knowledge.

Status semantics

scan_progress.status advances roughly:

ready ──► preparing ──► scanning ──► auditing ──► cleanup ──► done
                              │           │
                              └─► paused ─┘
                              │
                              └─► aborted (terminal)
  • ready — the Instance has been spawned but start: true hasn’t yet flipped it past instance.run(...). scan_progress called on a :ready instance returns a minimal payload (status + running + seed only — no statistics yet, no issues hash). Don’t trust delta arithmetic until status has advanced.
  • preparing — engine is loading checks/plugins, opening the seed URL, and warming the browser cluster. No issues yet, but the sitemap may start populating.
  • scanning — crawl is in flight; new sitemap entries appear, no audits running yet.
  • auditing — the crawl is winding down and checks are firing against discovered inputs. Most issues land here.
  • paused / abortedrunning: false, but only aborted is terminal. A paused scan can be resumed with scan_resume.
  • cleanup — engine is finalising state; close to done.
  • done — terminal. scan_report is now safe to call; running: false.

Treat anything other than done / aborted as still in flight.

Live events

The canonical way to track a scan is the live channel — spawn_instance attaches it by default. Every interesting state change inside the engine is pushed to the calling MCP session as a brand-derived JSON-RPC notification (notifications/spectre/live for Spectre); your client subscribes once on the SSE half of the Streamable HTTP transport and receives them as they happen, with no polling.

Subscribing

Streamable HTTP is one URL with two halves: POST /mcp for request/response and GET /mcp (with Accept: text/event-stream) for server-initiated notifications. Open the GET once after initialize, before any spawn_instance, and keep it open for the life of the scan. Use the same mcp-session-id you got from initialize on both halves — that’s how the server routes the notifications back to the right client.

The exact notification method to listen for is brand-derived; spawn_instance’s response includes live.notification_method so the client doesn’t have to hard-code it. Bare-cuboid builds emit notifications/cuboid/live.

Envelope shape

Each notification’s params is a single envelope:

{
  "jsonrpc": "2.0",
  "method":  "notifications/spectre/live",
  "params": {
    "type":        "issue",            // see type enum below
    "payload":     { … },              // type-specific body, see below
    "timestamp":   "2026-05-05T10:48:01.715Z",
    "status":      "auditing",         // current scan status at emit time
    "running":     true,
    "statistics":  { … },              // engine statistics snapshot at emit time
    "metadata":    { … },              // caller-supplied JSON object, if any (see below)
    "instance_id": "f8cd1a0a…"         // stamped on every event so a single
                                       // session can fan in multiple scans
  }
}

type is one of:

typepayload shapewhen
statusstring — see status payload sequence belowevery status transition + the synthetic started / exited bookends
sitemap_entry{ url: string, code: integer }every newly-crawled URL
issuefull issue Hash (name, severity, vector, proof, digest, …)every new finding (post-deduplication)
errorstring — one or more engine error lines, joined with newlinesrescued exceptions, coalesced over a 200 ms quiet window so a single backtrace becomes one event instead of 30+
reportfull final report Hash (issues + sitemap + statistics + plugins)once during cleanup, before the engine subprocess exits

Status payload sequence

A typical run emits the following status payloads, in order:

started      ← synthetic — fired the moment the live plugin attaches,
                before the engine starts crawling. Useful as an "alive"
                signal: if the client never sees this, the spawn never
                got past plugin load.
preparing    ← engine loading checks/plugins, opening the seed URL
scanning     ← crawl in flight
auditing     ← payload exchange against discovered inputs
cleanup      ← engine finalising state; this is when `report` fires
done         ← terminal lifecycle status (or `aborted`)
exited       ← synthetic — fired from the live plugin's at_exit hook
                when the engine subprocess actually exits.

exited is not automatic at done. The engine subprocess stays alive after done so subsequent scan_report calls keep working. It only exits when the client calls kill_instance (or the host terminates the process). Even then, the hook only fires on a graceful unwind — a hard kill (SIGKILL, host crash, OOM) bypasses Ruby’s at_exit chain entirely, and no exited will ever land. Treat done as “scan finished, results are stable” and exited as a best-effort “engine subprocess is gone too.” Don’t block client teardown on exited arriving.

paused and resumed can appear between scanning/auditing and cleanup if the operator hits scan_pause / scan_resume.

statistics is the live counter snapshot at the moment the event fired — issue totals by severity, page-queue depth, browser-pool status, etc. Receivers can keep a running dashboard without ever calling scan_progress.

Tagging events with caller metadata

spawn_instance may include plugins.live.metadata (a JSON string). At scan-start the plugin parses it once; every envelope thereafter carries the decoded value verbatim under metadata. Use this to correlate when one receiver fans in events from many concurrent scans — e.g. metadata = "{\"scan_id\":\"abc\",\"env\":\"staging\"}". Invalid JSON in metadata aborts the scan at validation time (Component::Options::Error::Invalid) — typos fail fast.

Wire format

The live envelope is encoded in messagepack by default — significantly smaller than JSON for the report payload (which carries the full sitemap and issue set). The MCP server decodes it internally and re-emits it as a normal JSON-RPC notification, so clients see plain JSON. The format is opaque to clients.

When to opt out

Pass live: false to spawn_instance if:

  • You’re driving from a stateless / non-MCP integration (no SSE channel to push to).
  • You want a simpler client implementation that just polls.
  • You’re running under Apex — live is rejected at the application layer (Apex’s sink-trace recon would flood the channel).

In any of those cases the polling cadence section below is still valid.

Polling cadence

Polling via scan_progress is the fallback when live: false (or under Apex). 5 seconds is the default cadence the quick_scan prompt suggests, and it’s a sensible floor:

  • Faster than ~2 s burns context tokens for almost no new state.
  • scan_progress with without_statistics: true is cheap; the statistics block dwarfs the rest of the payload.
  • Pass a stable session token (typically a UUID) on every poll after the first — the engine returns only items not previously emitted under that token, keeping each response small. The token lives for the engine instance’s lifetime; pick a fresh one to start fresh.
  • For very long scans (hours), 30 s is fine.

Instance lifetime

Every spawn_instance forks a daemonised Spectre Scan engine subprocess on the host (or, if a Cuboid Agent is configured, allocates one over the grid). The instance_id is the engine’s RPC token. Things to know:

  • The instance survives a client disconnect. If you forget to call kill_instance, the process keeps running until something kills it (host shutdown, OOM, manual signal). Always wire a kill_instance in your error path.
  • The instance does not survive an MCP-server restart cleanly. The daemonised engine keeps running but the MCP server’s in-memory @@instances map is empty after a restart, so you can’t kill_instance it through MCP any more (you’d need a process-level kill). Don’t restart the MCP server while scans are mid-flight.
  • Each instance reserves about 2 GB RAM and 4 GB disk by default. On a laptop, parallel scans are bounded by RAM; the host won’t proactively refuse a third spawn if the second one is still warming up.
  • start: false is rare in practice. It registers an idle instance that sits there waiting for a run, and MCP’s spawn_instance doesn’t have a separate “start now” tool — driving the run requires out-of-band RPC. Use it when something else is going to drive the run.

Error idiom

Engine exceptions don’t crash the MCP server — MCPProxy.instrumented_call wraps every body with rescue => e. The wire response is:

{
  "result": {
    "isError": true,
    "content": [
      { "type": "text", "text": "error: <ErrorClass>: <message>" }
    ]
  }
}

Common shapes:

  • error: ArgumentError: Invalid options!instance.run(options) rejected the shape. Read spectre://options/reference and try again.
  • error: Toq::Exceptions::RemoteException: … — the inner RPC client to the engine subprocess raised. Usually means the engine itself is in a bad state. Try scan_errors for clues; if that’s empty, kill_instance and respawn.
  • error: JSON::GeneratorError: "\xNN" from ASCII-8BIT to UTF-8 — the engine produced binary bytes that aren’t valid UTF-8 (a response body, HTTP header, etc.). Affects scan_report more than the streaming tools. Skip the report; scan_progress + scan_issues will still work.
  • unknown instance: … — the instance_id you passed isn’t in the server’s local map. Either the MCP server was restarted (which clears @@instances), or the id is stale. Re-spawn_instance.

Validation errors (missing required arg, type mismatch) come back through the JSON-RPC error envelope, not as a tool error:

{ "error": { "code": -32602, "message": "Missing required arguments: instance_id" } }

Options trivia

  • checks: "*" (a single string) is not equivalent to checks: ["*"] (an array containing the wildcard). The string form won’t expand. The preset and the option reference both use the array form.
  • plugins: ["defaults/*"] loads every plugin under the defaults directory. Empty array (or omitted key) loads none.
  • audit.elements defaults to all kinds when the key is omitted, which is what the CLI does. Pass an explicit list to restrict — e.g. ["links", "forms"] skips cookies, headers, JSON/XML bodies, etc.
  • scope.page_limit is baked into the quick-scan preset at 50 — a real-site smoke test that finishes in minutes. Override the page_limit prompt arg (or the JSON directly) for a smaller / larger cap; switch to the full-scan preset (or the full_scan prompt) for an uncapped audit. Sensible explicit values: 30 (smaller smoke test), 200 (representative).
  • authorized_by — set this to the operator’s email; it shows up in the engine’s outbound HTTP From header so target-site admins can identify the scan. Not required, but polite on third-party targets.

Conventions baked into the descriptions

The tool / prompt / resource descriptions are deliberately self-grounding:

  • Per-property descriptions on every tool argument (no buried-in-text args).
  • Cross-references use namespaced names (scan_resume, not resume) so the AI can call them verbatim.
  • Preconditions are stated where they exist (scan_pause “the scan must currently be running”, scan_resume “must have been paused via scan_pause”). Calling out of order returns an MCP tool error rather than a routing failure.
  • Domain terms (sink, mutation, action, vector, digest) are defined in spectre://glossary and cross-referenced from the relevant outputSchema property descriptions, so a model parsing structuredContent can resolve any unknown field name back to the glossary in one hop.

Things the protocol doesn’t expose yet

For honesty — places where you’d still need out-of-band knowledge:

  • Structured error codes. Errors come back as text. If you want to branch on “bad option key” vs “engine crashed” vs “auth failed”, you’re parsing the text.

Each of those is on the roadmap. Until they land, the resources + prompt expansion are the supported way to ground a model.

Connecting an MCP client

Most clients accept a Streamable HTTP server entry verbatim:

{
  "mcpServers": {
    "spectre": {
      "url": "http://127.0.0.1:7331/mcp"
    }
  }
}

That’s all. After initialize, the client sees:

  • 12 tools (4 framework + 8 per-scan), each with input + output schema.
  • 2 prompts (quick_scan, full_scan).
  • 4 resources.

If your client only speaks stdio (older Claude Desktop builds), use any community stdio↔HTTP MCP bridge in front. Cursor, Claude Code, and Continue speak Streamable HTTP natively.

End-to-end example — curl (live)

Initialize, capture the session id, acknowledge:

curl -i -X POST http://127.0.0.1:7331/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  --data '{
    "jsonrpc": "2.0", "id": 1, "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities":    {},
      "clientInfo":      { "name": "curl", "version": "0" }
    }
  }'
# → response header: Mcp-Session-Id: <SID>

curl -X POST http://127.0.0.1:7331/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  --data '{ "jsonrpc": "2.0", "method": "notifications/initialized" }'

Open the SSE channel for live events — keep this connection open for the life of the scan. Run it in another terminal (or backgrounded) so the next POSTs can fire while it’s tailing:

curl -sS -N -X GET http://127.0.0.1:7331/mcp \
  -H 'Accept: text/event-stream' \
  -H "Mcp-Session-Id: $SID"
# stream of `data: { "jsonrpc": "2.0", "method": "notifications/spectre/live", … }`

Spawn a scan against http://testfire.net/ using the quick-scan defaults — live: true is the default so the engine starts streaming events to the SSE channel above immediately:

curl -X POST http://127.0.0.1:7331/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Mcp-Session-Id: $SID" \
  --data '{
    "jsonrpc": "2.0", "id": 2, "method": "tools/call",
    "params": {
      "name": "spawn_instance",
      "arguments": {
        "options": {
          "url":     "http://testfire.net/",
          "checks":  ["*"]
        }
      }
    }
  }'
# → result.structuredContent:
#   { "instance_id": "<IID>",
#     "url":         "127.0.0.1:<engine-port>",
#     "live":        { "notification_method": "notifications/spectre/live" } }

The SSE stream now emits one envelope per event — status transitions, every newly-crawled sitemap_entry, every issue, and a final report when status reaches done.

Tear down once the report event has landed:

curl -X POST http://127.0.0.1:7331/mcp ... \
  --data '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call",
            "params": { "name": "kill_instance",
                        "arguments": { "instance_id": "'$IID'" } } }'

Polling fallback

If you’d rather poll, pass "live": false on spawn_instance and loop with scan_progress / scan_issues:

# spawn with live disabled
curl -X POST http://127.0.0.1:7331/mcp ... \
  --data '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
            "params": { "name": "spawn_instance",
                        "arguments": {
                          "options": { "url": "http://testfire.net/", "checks": ["*"] },
                          "live":    false
                        } } }'

# poll, fetching only items new since the previous call under
# the chosen `session` token (any caller-chosen string)
curl -X POST http://127.0.0.1:7331/mcp ... \
  --data '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
            "params": { "name": "scan_progress",
                        "arguments": {
                          "instance_id":        "'$IID'",
                          "session":            "client-poll-1",
                          "without_statistics": true
                        } } }'

The same loops expressed as a quick_scan prompt expansion are one prompts/get call away.

Web UI

The WebUI allows you to easily run, manage and schedule scans and their results via an intuitive web interface.

Table of contents

Boot-up

To boot the Pro interface please run:

bin/spectre_pro

After boot-up, you can visit the interface via your browser of choice.

Features

Scan management

  • Quick scan. A one-input form in the navbar will scan any URL with sane defaults — useful for ad-hoc spot checks without leaving the current page.
  • Parallel scans. Run multiple scans concurrently against the same site, different sites, or both — bounded only by configured worker capacity.
  • Recurring scans. Re-scan the same target on a schedule and get an automatic review of every finding compared to the previous revision:
    • Fixed — issues that no longer appear.
    • Regressions — fixed issues that re-appeared.
    • New — first-time findings.
    • Trusted / untrusted / reviewed / false-positive — manual states for triage that carry forward across revisions.
  • Scheduled scans. Either pick from preset frequencies (hourly, daily, weekly…) or paste a cron expression. The scheduler surfaces upcoming occurrences and flags conflicts (overlapping start times, parallelism ceiling) before they fire.
  • Suspend / resume / repeat. Pause a long-running scan and resume from the same on-disk session later. One-click repeat re-runs a finished scan with the exact same configuration.

Live monitoring

  • Real-time progress. Coverage, request rate, discovered pages and newly-found issues stream into the UI over Action Cable as the scan runs — no manual refresh.
  • Live event-driven cache busts. Per-user dashboard / navbar caches invalidate the moment a model commits, so counts and badges stay truthful without polling.
  • Scan, revision and site live views. Drill in at the level you need: whole-site activity, a specific scan or a single revision.

Findings & analysis

  • Issue detail with proof, remediation and exploit. Every finding is presented with the captured request / response, normalised proof, a per-check remediation guide and (when available) a working exploit payload.
  • DOM-XSS introspector. For DOM-level findings, follow the data flow from source to sink across the rendered page with captured stack frames and the page snapshot inline.
  • Coverage explorer. Every page the scanner reached, with HTTP status, content-type and a one-click jump to the issues attached to it.
  • Powerful filtering. Stack severity / state / type / scan / revision filters; narrow by site, by check, by URL pattern; permalinks survive reload and live-refresh.
  • Severity-aware sorting. High-impact findings always float to the top, with sibling-grouping so duplicate signatures collapse cleanly.

Configuration

  • Scan profiles. Reusable bundles of checks, scope rules, audit options and plug-ins. Per-user, per-site or shared.
  • Per-site overrides. Override profile scope rules at the site level without forking a whole profile.
  • Device emulation. Scan as a desktop browser, mobile, tablet, or any custom user-agent / viewport / touch combination.
  • Site user roles. Authenticate the scanner as one or more application personas:
    • Form login — declarative URL + form parameters.
    • Script login — drop in Ruby with a prepared browser driver (Watir) or HTTP client.
    • Each role gets its own captured session that persists across revisions.

Operations & audit

  • Server / scanner / network health. Request rate, response times, browser-pool utilisation, error counts, queue depth — surfaced in charts that update live.
  • Full audit log. Every change to sites, scans, revisions, issues and user roles is captured (PaperTrail) with the actor, the diff and a click-through to the affected object — even after the object itself has been deleted.
  • Resilient scheduler. SQLite writer-lock contention, autoloader races and transient RPC errors are handled internally; the UI stays responsive while two or more scans hammer the database.

Reporting & integrations

  • Multi-format export. HTML, JSON, XML, plain-text and the framework-native AFR archive — at the scan, revision or filtered result-set level.
  • Notifications. Per-event email / browser push for scan start / finish / failure / suspension and severity thresholds.
  • OpenAI assist. Optional LLM-backed remediation expansion for individual findings (configurable in Settings).

Djin! — in-app AI assistant

  • Side-dock chat on every page. A right-side panel (with full- page expand and a launcher button bottom-right) that uses your configured AI provider (OpenAI or Claude) — same key as the per-issue OpenAI assist. Streamed replies via Action Cable; conversation history persists per-user.
  • Page-aware grounding. Each turn carries the current (controller, action, resource ids) so questions like “what’s this issue?” or “how bad is this revision?” are answered against what you’re looking at, no need to spell it out.
  • Read tools. Djin! can call list_sites / list_scans / list_revisions / list_issues, drill into a single record (fetch_issue, fetch_revision), free-text search across sites / scans / issues, and return your accessible scope. Every query is scoped to the records you own — Djin! cannot see another operator’s data.
  • Navigation. “Take me there” works — Djin! can call navigate_to({ kind: 'issue', id: <digest> }) and your browser jumps to the issue page (or site / scan / revision).
  • Approval-gated write actions. Djin! can also propose mark_false_positive, mark_fixed, clear_state, pause_scan / resume_scan / abort_scan, and start_scan. Every write tool surfaces an inline yellow card in the dock with the proposed args — nothing happens until you click Approve. Deny instead and Djin! sees the rejection reason and adjusts.
  • Audit trail. Approved Djin!-driven mutations are tagged with whodunnit = "djin:<your_id>" in the PaperTrail log, so the per-user activity feed distinguishes AI-driven actions from hand-driven ones.
  • Cancel & rate-limit. Stop a streaming reply mid-flight; a per-user daily token budget caps spend across conversations.
  • Privacy & opt-in. Disabled by default; flip on in Settings once your AI provider key is configured. The dock surfaces an empty “configure in Settings” hint instead of failing silently if the key isn’t set.

Quality of life

  • Light & dark themes with a one-click toggle that persists across sessions and respects prefers-color-scheme on first visit.
  • Per-page UI state persistence. Severity-section open/closed, collapsed details, table sort and filter selections are remembered per browser without server round-trips.
  • Keyboard-friendly forms and focus-aware live-refresh: an open <select> or focused input is never swapped out from under you.
  • First-run welcome. A guided empty-state experience walks new installs from “no sites yet” to “scanning” without docs.

Screenshots

Welcome

The first-run experience: an empty-state landing page with a quick-scan form and a path to add your first site.

Welcome screen

Dashboard

The dashboard is the home page once you have at least one site. It surfaces running scans, recent activity, aggregate counts and per-site health at a glance.

Light theme

Dashboard — light theme

Dark theme

Dashboard — dark theme

Continued view

Tiles below the fold show recent revisions, performance trends and per-site rollups.

Dashboard — continued

Sub-dashboard: by severity

A focused view that breaks every issue down by severity (high / medium / low / informational), with click-through to the matching issue list.

Dashboard — by severity

Sub-dashboard: by issue type

Rolls every issue up by check / type so you can see at a glance which weakness families dominate the picture.

Dashboard — by issue type

Sites

Add and manage the sites you wish to scan. Each site keeps its own profiles, devices, user roles, scans and audit history.

All sites

Sites index

Site overview

The per-site landing page — a snapshot of recent revisions, severity rollup and currently-running activity for that one site.

Site overview

Site live status

Live progress for the site’s currently-running scan, updated in real-time over Action Cable.

Site — live

Site scans

The full scan list for a site, grouped by Active / Suspended / Finished / Schedule.

Site scans

Site settings

Per-site knobs: protocol, host, port, default profile, default device and inherited scope rules.

Site settings

User roles

Manage authenticated personas the scanner can assume — handy for testing role-gated parts of the application.

Site user roles

Script-based login

When form-based login isn’t enough, drop in a Ruby snippet using a prepared browser driver or HTTP client.

Site user role — login script

Scans

Configure, schedule, monitor and re-run scans.

Quick scan

The navbar’s quick-scan widget — one URL away from a default-profile scan against a brand-new or existing site.

Quick scan

New scan

The full configuration form: profile, device, role, schedule, custom scope, plug-ins.

New scan

Scan live

Live scan progress at the scan level — running revision, queue stats, discovered pages, issues found so far.

Scan — live

Generic live view

Live status surface used both for site-wide and scan-wide live monitoring.

Live status

Revisions

Each scan produces revisions — incremental snapshots of the same target. Revisions are where issues, coverage and reports actually live.

Revision — live

Live monitoring of an individual revision while it scans.

Revision — live

Revision — issues

The issue listing for a single revision, with severity / state / type filters.

Revision — issues

Issues

Drill into individual findings — proof, remediation guidance, exploit, request/response capture, dissected payloads.

Issue detail

Issue — overview

Issue — proof / response

Issue — remediation

DOM XSS

DOM-XSS issue with the introspector view that traces the data flow from source to sink across the rendered page.

Issue — DOM XSS

Recurring scans — regressions & fixes

Recurring scans automatically diff their findings across revisions: fixed issues drop out, regressions get flagged, new findings get their own state.

Recurring scan — review (1)

Recurring scan — review (2)

Recurring scan — review (3)

Introspector

A dedicated view for server-side findings: source → sink data-flow trace, captured stack frames and the rendered page snapshot.

Introspector — flow (1)

Introspector — flow (2)

Introspector — flow (3)

Introspector — flow (4)

Coverage

Every page the scanner reached, with HTTP status, content-type and a click-through to the issues that came out of each one.

Coverage

Health

Server / scanner / network health — request rate, response times, browser-pool utilisation, error counts.

Health

Logs

A full audit log of every change to sites, scans, revisions, issues and user roles. Filterable by event, object type and actor.

Logs

Profiles

Reusable scan configurations: which checks to run, scope rules, plug-ins, audit options.

All profiles

Profiles

Profile editor — checks

Pick exactly which checks should run as part of a profile.

Profile — all checks

Devices

Device emulation lets you scan as a desktop browser, mobile, tablet, or any custom user-agent / viewport combination.

Devices

Settings

Top-level application settings: notifications, OpenAI integration, default scan / HTTP / browser-pool tuning.

Settings

Export

Export scan results in any of the supported report formats (HTML, JSON, XML, plain-text, AFR archive).

Export

Djin! — AI assistant

Side-dock chat that streams via Action Cable, with page-aware grounding, scoped read tools and approval-gated write tools.

Page-aware answers — dashboard

Dock open against the dashboard; “show me high-severity issues” is grounded against the records you actually own, not a free-form recap.

Djin! — dashboard answer

Page-aware answers — issue detail

On an issue page, the dock explains the finding and walks the execution flow against the captured request / response in front of you.

Djin! — issue explainer

Approval-gated write tools

Write tools (here: abort_scan) propose their args inline as a yellow card. Nothing happens until you click Approve; deny to send the rejection reason back to the model.

Djin! — approval card

Setup AI

Spectre Scan integrates with two AI providers — pick whichever account you have credit on:

Both expose the same surface: per-issue analysis (description, remediation, exploit, dissect, insights, patch, report) at scan time, and the Djin! side-dock chat assistant in the Pro web UI.

CLI

OpenAI:

bin/spectre --plugin=openai:apikey=YOUR_OPENAI_KEY [URL]

Claude:

bin/spectre --plugin=claude:apikey=YOUR_ANTHROPIC_KEY [URL]

Optional plugin parameters (both providers accept them):

  • model — override the default model (gpt-4o for OpenAI, claude-opus-4-5 for Claude).
  • min_severity — restrict per-issue analysis to issues at or above this severity (informational, low, medium, high). Default: medium. Lower-severity findings are skipped, saving API tokens.

Example with overrides:

bin/spectre \
    --plugin=claude:apikey=YOUR_KEY,model=claude-haiku-4-5,min_severity=high \
    [URL]

REST API

Drive the same per-issue plugin from POST /instances by adding the provider to the plugins hash on the options body. Same apikey / model / min_severity parameters the CLI accepts.

OpenAI:

{
  "url":    "http://example.com/",
  "checks": ["*"],
  "plugins": {
    "openai": { "apikey": "YOUR_OPENAI_KEY" }
  }
}

Claude:

{
  "url":    "http://example.com/",
  "checks": ["*"],
  "plugins": {
    "claude": { "apikey": "YOUR_ANTHROPIC_KEY" }
  }
}

Plugin parameters layer in as siblings of apikey:

{
  "plugins": {
    "claude": {
      "apikey":       "YOUR_KEY",
      "model":        "claude-haiku-4-5",
      "min_severity": "high"
    }
  }
}

Only one of openai / claude should be present per scan — they both annotate the same issues, so loading both wastes API tokens on duplicate work.

MCP

The MCP server forwards its spawn_instance.options straight to the engine, so the plugins shape is identical to REST — just nested under the tools/call envelope.

OpenAI:

{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {
    "name": "spawn_instance",
    "arguments": {
      "options": {
        "url":    "http://example.com/",
        "checks": ["*"],
        "plugins": {
          "openai": { "apikey": "YOUR_OPENAI_KEY" }
        }
      }
    }
  }
}

Claude:

{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {
    "name": "spawn_instance",
    "arguments": {
      "options": {
        "url":    "http://example.com/",
        "checks": ["*"],
        "plugins": {
          "claude": { "apikey": "YOUR_ANTHROPIC_KEY" }
        }
      }
    }
  }
}

model and min_severity slot in alongside apikey exactly as they do over REST.

The plugins key is documented in full at spectre://options/reference (or inlined under the Options reference on the MCP page).

Web UI

In Settings → Djin! AI assistant:

  1. Pick a provider (OpenAI or Claude) from the dropdown.
  2. Paste your API key. Spectre Pro pings the provider on save to verify the key — invalid keys / billing issues / no model access surface as a precise error rather than a silent failure.
  3. Save.

That alone enables Djin! — the in-app side-dock AI assistant. Two extra toggles in the same section control the rest:

  • Auto-analyze issues during scan — off by default. When on, the scanner attaches the same provider’s per-issue plugin to every scan, expanding each finding’s description / remediation / exploit fields automatically. Costs roughly one provider call per issue.
  • Djin! daily token budget — per-user 24-hour cap on prompt
    • completion tokens summed across every Djin! conversation. 0 disables the cap.

Apex Recon ships with the same Djin! chat assistant, but no per-issue plugin (Apex’s domain is input-vector discovery, not security findings — there’s nothing per-record for the AI to auto-expand). Configuring the AI key in Apex Settings only enables Djin!.

Run air-gapped

In order to run Spectre Scan in an air-gapped environment you need to:

  • Place the encrypted license file as license inside ~/.spectre/ (or $SPECTRE_HOME/ if you’ve overridden the home dir).
    • Either by copying it over from a previous activation on an Internet-enabled machine — copy ~/.spectre/license (and, if present, the plaintext ~/.spectre/license.key next to it), or;
    • by activating on-line.
  • Run an instance of bin/spectre_check_server in your local network – Provides functionality needed for SSRF types of checks.
    • Set the SPECTRE_CHECK_SERVER environment variable to the check server URL – ex. http://10.1.1.1:9292.

Generate reports

Pro

You can export scan results in several formats from the “Export” tab of an aborted or completed revision scan.

CLI

There are 2 reference report format types that you may encounter when using Spectre Scan:

  1. *.crf – Cuboid report file.
  2. *.ser – Spectre Scan report.

Both of these files can be handled by the CLI spectre_reporter utility in order to convert them to a multitude of formats or print the results to STDOUT.

For example, to generate an HTML report:

bin/spectre_reporter --report=html:outfile=my_report.html.zip /home/user/.spectre/reports/report.ser

Or, to just print the report to STDOUT:

bin/spectre_reporter --report=stdout /home/user/.spectre/reports/report.ser

At the time of writing, bin/spectre_reporter --reporters-list yields:

 [~] Available reports:

 [*] ap:
--------------------
Name:           AP
Description:
Awesome prints a scan report hash.

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.1.1
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/ap.rb

 [*] html:
--------------------
Name:           HTML
Description:
Exports the audit results as a compressed HTML report.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.html.zip
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.4.4
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/html.rb

 [*] json:
--------------------
Name:           JSON
Description:
Exports the audit results as a JSON (.json) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.json
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.1.3
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/json.rb

 [*] markdown:
--------------------
Name:           Markdown
Description:
Exports the audit results as a Markdown (.md) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.md
 [~]    Required?:   false

 [~]    ai_friendly - Emit a flatter, compacter Markdown variant tuned for LLM ingestion (no TOC, blobs truncated, explicit section markers).
 [~]    Type:        bool
 [~]    Default:     false
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.1
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/markdown.rb

 [*] marshal:
--------------------
Name:           Marshal
Description:
Exports the audit results as a Marshal (.marshal) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.marshal
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.1.1
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/marshal.rb

 [*] pdf:
--------------------
Name:           PDF
Description:
Exports the audit results as a PDF (.pdf) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.pdf
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.5
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/pdf.rb

 [*] stdout:
--------------------
Name:           Stdout
Description:
Prints the results to standard output.

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.3.3
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/stdout.rb

 [*] txt:
--------------------
Name:           Text
Description:
Exports the audit results as a text (.txt) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.txt
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.2.1
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/txt.rb

 [*] xml:
--------------------
Name:           XML
Description:
Exports the audit results as an XML (.xml) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.xml
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.3.7
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/xml.rb

 [*] yaml:
--------------------
Name:           YAML
Description:
Exports the audit results as a YAML (.yaml) file.

Options:
 [~]    outfile - Where to save the report.
 [~]    Type:        string
 [~]    Default:     2026-05-07_13_45_31_+0300.yaml
 [~]    Required?:   false

Author:         Tasos "Zapotek" Laskos <[email protected]>
Version:        0.2
Path:   /home/zapotek/workspace/scnr/engine/components/reporters/yaml.rb

Optimize scans

Left to its own devices, Spectre Scan will try to optimize itself to match any given circumstance, but there are limitations to what it can do automatically.

If a scan is taking too long, chances are that there are ways to make it go much faster by taking a couple of minutes to configure the system to closer match your needs.

In addition to performance, the following options also affect resource usage so you can experiment with them to better match your available resources as well.

  1. Only enable security checks that concern you
  2. Tailor the audit process to the platforms of the web application
  3. Ensure server responsiveness
  4. Balance RAM consumption and performance
  5. Reduce RAM consumption by avoiding large resources
  6. Don’t follow redundant pages
  7. Adjust the amount of browser workers
  8. Pick the audit mode that suits you best
  9. Scan incrementally

Only enable security checks that concern you

By default, Spectre Scan will load all checks, which may not be what you want.

If you are interested in high severity vulnerabilities or don’t care for things like discovery of common files and directories, and the like, you should disable the superfluous checks.

You can enable only active checks with:

--checks=active/* 

Or skip the inefficient passive ones with:

--checks=*,-common_*,-backup_*,-backdoors

Tailor the audit process to the platforms of the web application

By default, the system will fingerprint the web application in order to deduce what platforms power it, thus enabling it to only send applicable payloads (instead of everything) which results in less server stress and bandwidth usage.

However, it is a good idea to explicitly set the platforms, if you know them, so as to play it safe and get the best possible results – especially since database platforms can’t be fingerprinted prior to the audit and their payloads are a large part of the overall scan.

You can specify platforms with:

--platforms=linux,mysql,php,apache

Ensure server responsiveness

By default, Spectre Scan will monitor the response times of the server and throttle itself down if it detects that the server is getting stressed. This happens in order to keep the server alive and responsive and maintain a stable connection to it.

However, there are times with weak servers when they die before Spectre Scan gets a chance to adjust itself.

You can bring up the scan statistics on the CLI screen by hitting Enter, in which case you’ll see something like:

 [~] Currently auditing          http://testhtml5.vulnweb.com/ajax/popular?offset=0                                 
 [~] Burst response time sum     6.861 seconds                                                                      
 [~] Burst response count        29                                                                                 
 [~] Burst average response time 1.759 seconds                                                                      
 [~] Burst average               0 requests/second                                                              
 [~] Original max concurrency    10                                                                                 
 [~] Throttled max concurrency   2                                                                                                                                             

We can see that the server is having a hard time from the following values:

  • Burst average: 3 requests/second
  • Burst average response time 1.759
  • Burst average: 0 requests/second
  • Throttled max concurrency: 2

The response times were so high (1.75 seconds) that Spectre Scan had to throttle its HTTP request concurrency from 10 requests to 2 requests, which would result in a drastically increased scan time.

You can lower the default HTTP concurrency and try again to make sure that the server at no point gets a stressful load:

--http-request-concurrency=5

Balance RAM consumption and performance

Most excessive RAM consumption issues are caused by large (or a lot of) HTTP requests, which need to be temporarily stored in memory in order for them to later be scheduled in a way that achieves optimal network concurrency.

To cut this short, having a lot of HTTP requests in the queue allows Spectre Scan to be better at performing a lot of them at the same time, and thus makes better use of your available bandwidth. So, a large queue means better network performance.

However, a large queue can lead to some serious RAM consumption, depending on the website and type of audit and a lot of other factors.

As a compromise between preventing RAM consumption issues but still getting decent performance, the default queue size is set to 50. You can adjust this number to better suit your needs depending on the situation.

You can adjust the HTTP request queue size via the --http-request-queue-size option.

Reduce RAM consumption by avoiding large resources

Spectre Scan performs a large number of analysis operations on each web page. This is usually not a problem, except for when dealing with web pages of large sizes.

If you are in a RAM constrained environment, you can configure Spectre Scan to not download and analyze pages which exceed a certain size limit – by default, that limit is 500KB.

You can adjust the maximm allows size of HTTP response via the --http-response-max-size option.

Don’t follow redundant pages

A lot of websites have redundant pages like galleries, calendars, directory listings etc. which are basically the same page with the same inputs but just presenting different data.

Auditing the first (or first few) of such pages is often enough and trying to follow and audit them all can sometimes result in an infinite crawl, as can be the case with calendars.

Spectre Scan provides 2 features to help deal with that:

  • Redundancy filters: Specify pattern and counter pairs, pages matching the pattern will be followed the amount of times specified by the counter.
    • --scope-redundant-path-pattern
  • Auto-redundant: Follow URLs with the same combinations of query parameters a limited amount of times.
    • --scope-auto-redundant – Default is 10.

Adjust the amount of browser workers

Spectre Scan uses real browsers to support technologies such as HTML5, AJAX and DOM manipulation and perform deep analysis of client-side code.

Even though browser operations are performed in parallel using a pool of workers, the default pool size is modest and operations can be time consuming.

By increasing the amount of workers in the pool, scan durations can be dramatically shortened, especially when scanning web applications that make heavy use of client-side technologies.

Finding the optimal pool size depends on the resources of your machine (especially the amount of CPU cores) and will probably require some experimentation; on average, 1-2 browsers for each logical CPU core serves as a good starting point.

However, do keep in mind that more workers may lead to higher RAM consumption as they will also accelerate workload generation.

You can set this option via --dom-pool-size. The default is calculated based on the amount of available CPU cores your system has.

Pick the audit mode that suits you best

This is a Time vs Thoroughness balancing option.

--audit-mode:

  • quick – For a quick scan, complex or rare payloads will be omitted.
  • moderate (default) – Balanced payloads.
  • super – All payloads, more DOM probing, disabled attack optimization heuristics.

Scan incrementally

In order to save valuable time on subsequent scans, Spectre Scan allows you to extract a session file from completed/aborted scans, in order to allow for incremental re-scans.

This means that only newly introduced input vectors will be audited the next time around, which can save immense amounts of time.

The location of the session file is printed at the end of each scan and can be restored via:

./bin/spectre_restore SESSION_FILE

Maintain a valid session

Spectre Scan supports automated logout detection and re-login, as well as improved login procedures.

login_form plugin

The login_form plugin expects for following options:

  • url – The URL containing the login form;
  • parameters – A URL-query-like string of form parameters;
  • check – A pattern to be matched against the response body after requesting the supplied URL in order to verify a successful login.

After a successful login, the plugin will configure the system-wide session manager and let it know of the procedure it needs to follow in order to be able to login automatically, in case it gets logged out during the scan or the session expires.

Hint: If the response of the form submission doesn’t contain the check, you can set a different check URL via the global --session-check-url option, this will also require that a --session-check-pattern be set as well (it can be the same as the autologin check option).

Limitations

This plugin operates a browser just like a regular user would and is thus limited to the same extent.

For example, if the login form is by default hidden and requires a sequence of UI interactions in order to become visible, this plugin will not be able to submit it.

For more complex sequences please use the login_script plugin.

Example

bin/spectre http://testfire.net --plugin=login_form:url=http://testfire.net/bank/login.aspx,parameters="uid=jsmith&passw=Demo1234",check="Sign Off|MY ACCOUNT" --scope-exclude-pattern=logout

The login form found in http://testfire.net/bank/login.aspx which contains the uid and passw inputs will be updated with the given values and submitted.

After that, the response will be matched against the check pattern – which will also be used for the duration of the scan to check whether or not the session is still valid.

(Since the “Sign Off” and “MY ACCOUNT” strings only appear when the user is logged-in, they are a reliable way to check the validity of the session.)

Lastly, we exclude (--scope-exclude-pattern) the logout link from the audit in order to avoid getting logged out.

login_script plugin

The login_script plugin can be used to specify custom login procedures, as simple Ruby or JS scripts, to be executed prior to the scan and each time a logout is detected.

The script will be run under the context of a plugin, which means that it will have access to all system components, allowing you to login in the most optimal way – be that via a real browser, via HTTP requests, by loading an external cookie-jar file and many more.

With browser

If a browser is available, it will be exposed to the script via the browser variable. Otherwise, that variable will have a value of nil.

If you require access to Selenium, browser.wd will provide you access to the appropriate WebDriver.

browser.goto 'http://testfire.net/bank/login.aspx'

form = browser.form( id: 'login' )
form.text_field( name: 'uid' ).set 'jsmith'
form.text_field( name: 'passw' ).set 'Demo1234'

form.submit

# You can also configure the session check from the script, dynamically,
# if you don't want to set static options via the user interface.
SCNR::Engine::Options.session.check_url     = browser.url
SCNR::Engine::Options.session.check_pattern = /Sign Off|MY ACCOUNT/

With HTTP Client

If a real browser environment is not required for the login operation, then using the system-wide HTTP interface is preferable, as it will be much faster and consume much less resources.

response = http.post( 'http://testfire.net/bank/login.aspx',
    parameters:     {
        'uid'   => 'jsmith',
        'passw' => 'Demo1234'
    },
    mode:           :sync,
    update_cookies: true
)

SCNR::Engine::Options.session.check_url     = to_absolute( response.headers.location, response.url )
SCNR::Engine::Options.session.check_pattern = /Sign Off|MY ACCOUNT/

If an external process is used to manage sessions, you can keep Spectre Scan in sync by loading cookies from a shared Netscape-style cookie-jar file.

http.cookie_jar.load 'cookies.txt'

Advanced session check configuration

In addition to just setting the check_url and check_pattern options, you can also set arbitrary HTTP request options for the login check, to cover cases where extra tokens or a method other than GET must be used.

framework.session.check_options = {
    # :get, :post, :put, :delete
    method:     :post,

    # URL query parameters.
    parameters: {
        'param1' => 'value'
    },

    # Request body parameters -- can also be a String instead of Hash.
    body:       {
        'body_param1' => 'value'
    },

    cookies:    {
        'custom_cookie' => 'value'
    },

    headers:    {
        'X-Custom-Header' => 'value'
    }
}

Proxy plugin

The proxy plugin can be used to train the system by inspecting the traffic exchanged between the browser and the web application. From that traffic, it can extract input vectors like links, forms and cookies from both sides – i.e. from server responses as well as browser requests.

Since the proxy can inspect all this traffic, it can be instructed to record a login sequence and then deduce the login form and the values with which it was filled.

Like the form_login plugin, the proxy plugin will configure the system accordingly.

Example

bin/spectre http://testfire.net --plugin=proxy --scope-exclude-pattern=logout

You then need to configure your browser to use this proxy when connecting to the webapp, press the record button just before logging in and the stop button after.

You’ll then be presented with a simple wizard which will guide you through configuring a login check and verifying that the deduced login sequence works properly.

Lastly, we exclude (--scope-exclude-pattern=logout) the logout link from the audit in order to avoid getting logged out.

If the aforementioned techniques don’t work for you, you can pass a cookie-jar and manually configure the login-check using the following options:

  • --http-cookie-jar
  • --session-check-url
  • --session-check-pattern

This way Spectre Scan will still be able to know if it gets logged out (which is helpful to several system components) but won’t be able to log-in automatically.

Of course, you should still exclude any path that can lead to the destruction of the session.

Scan services

At the moment the are no specialized service crawlers, however auditing web services is possible by first training the system via its proxy plugin.

Capturing inputs

The best way to capture web service inputs is by running your service test-suite and having its HTTP requests go through the proxy plugin.

Proxy plugin setup

You can setup the proxy like so:

bin/spectre http://target-url --scope-page-limit=0 --checks=*,-passive/* --plugin=proxy --audit-jsons --audit-xmls

The default proxy URL will be: http://localhost:8282

The --scope-page-limit=0 option tells the system to not do any sort of crawl and only use what has been made visible via the proxy.

The --checks option tells the system to load all but irrelevant checks for service scans – common files and directories and the like don’t really apply in this case.

The --audit-jsons --audit-xmls options restrict the scan to only JSON and XML inputs.

Test-suite setup

Test-suite configurations vary, however you can usually export the proxy setting as an environmental variable, prior to running your test-suite, like so:

export http_proxy=http://localhost:8282

If this global setting is ignored, you will need to explicitly configure your test-suite.

Exporting the input vectors

After running the test-suite, the system will have been trained with the input vectors of the web service. Thus, it would be a good idea to export that data, in order to avoid having to run the training scenarios prior to each scan.

The data can be retrieved with:

http_proxy=http://localhost:8282 curl http://scnr.engine.proxy/panel/vectors.yml -o vectors.yml

Starting the scan

In order for the scan to start you will need to shutdown the proxy:

http_proxy=http://localhost:8282 curl http://scnr.engine.proxy/shutdown

Re-using input vector data

Data exported via the proxy plugin can be imported via the vector_feed plugin, like so:

bin/spectre http://target-url --scope-page-limit=0 --checks=*,-passive/* --plugin=vector_feed:yaml_file=vectors.yml

Thus, you only have to run your test-suite scenarios once, for the initial training and then reuse the exported vector data for subsequent scans.

Debugging

You can debug the proxy manually via simple curl commands, like so:

http_proxy=http://localhost:8282 curl -H "Content-Type: application/json" -X POST -d '{ "input": "value" }' http://target-url/my-resource

Then, in Spectre Scan’s terminal you’ll see something like:

[*] Proxy: Requesting http://target-url/my-resource
[~] Proxy:  *  0 forms
[~] Proxy:  *  0 links
[~] Proxy:  *  0 cookies
[~] Proxy:  *  1 JSON
[~] Proxy:  *  0 XML

If you require further information, you can enable the --output-debug option; acceptable verbosity values range from 1 to 3, 1 being the default.

Scale

Spectre Scan can be configured into a Grid, in order to combine the resources of multiple nodes and thus perform large amounts of scans simultaneously or complete individual scans faster.

Its Grid can distribute workload horizontally and vertically and can also easily scale up and/or down.

In essence, Grids are created by connecting multiple Agents together, at which point a mesh network of Agents is formed. Doing so requires no configuration, other than specifying an already running Agent when booting up a new one.

This allows for creating a private Cloud of scanners, with minimal configuration, that can handle an indefinite amount of workload.

Prior to continuing, it would be best if you took a look at Spectre Scan’s distributed architecture.

Strategies

Scaling strategies can be configured via the --strategy option of Agents, like so:

bin/spectre_agent --strategy=horizonstal
bin/spectre_agent --strategy=vertical

Horizontal (default)

Spectre Scan Instances will be provided by the least burdened Agent, i.e. the Agent with the least utilization of its slots.

This strategy helps to keep the overall Grid health good by spreading the workload across as many nodes as possible.

Vertical

Spectre Scan Instances will be provided by the most burdened Agent, i.e. the Agent with the most utilization of its slots.

This strategy helps to keep the overall Grid size (and thus cost) low by utilizing as few Grid nodes as possible.

It will also let you know if you have over-provisioned as extra nodes will not be receiving any workload.

Creating a Grid

In one terminal run:

bin/spectre_agent

This is the initial Agent.

Scaling up

To scale up just boot more Agents and specify a peer.

So, in another terminal run:

bin/spectre_agent --port=7332 --peer=127.0.0.1:7331

Lastly, in yet another terminal run:

bin/spectre_agent --port=7333 --peer=127.0.0.1:7332

(It doesn’t matter who the peer is as long as it’s part of the Grid.)

Now we have a Grid of 3 Agents.

The point of course is to run each Agent on a different machine in real life, but this will do for now.

Scaling down

You can scale down by unplugging an Agent from its Grid using:

bin/spectre_agent_unplug 127.0.0.1:7332

Running Grid scans

To start a scan that will be load-balanced across the Grid, simply issue a spawn request on any of the Grid members.

Like so:

bin/spectre_spawn --agent-url=127.0.0.1:7331 http://testhtml5.vulnweb.com

The above will run a scan with the default options against http://testhtml5.vulnweb.com, originating from whichever node is optimal at any given time.

If the Grid is out of slots you will see the following message:

[~] Agent is at maximum utilization, please try again later.

In which case you can keep retrying until a slot opens up.

Running multi-Instance scans

The above is useful when you have multiple scans to run and you want to run them at the same time; another cool feature of Spectre Scan though is that it can parallelize individual scans across the Grid thus resulting in huge single-scan performance gains.

For example, this would be useful if you were to scan a site with tens of thousands, hundreds of thousands or even millions of pages.

Even better, doing so is as easy as:

bin/spectre_spawn --agent-url=127.0.0.1:7331 http://testhtml5.vulnweb.com --multi-instances=5

The --multi-instances=5 option will instruct Spectre Scan to use 5 Instances to run this particular scan, with the aforementioned Instances being of course load-balanced across the Grid.

Copyright

Copyright 2024 Ecsypno.

All rights reserved.