|
|
|
# REST API
|
|
|
|
|
|
|
|
* [Server](#server)
|
|
|
|
* [API](#api)
|
|
|
|
* [List scans](#list-scans)
|
|
|
|
* [Perform a new scan](#perform-a-new-scan)
|
|
|
|
* [Monitor scan progress](#monitor-scan-progress)
|
|
|
|
* [Pause a scan](#pause-a-scan)
|
|
|
|
* [Resume a scan](#resume-a-scan)
|
|
|
|
* [Retrieve a scan report](#retrieve-a-scan-report)
|
|
|
|
* [Abort or shutdown a scan](#abort-or-shutdown-a-scan)
|
|
|
|
* [Example](#example-client)
|
|
|
|
|
|
|
|
## <a id="server" href="#server">Server</a>
|
|
|
|
|
|
|
|
For server configuration please consult the [[relevant page | REST-Server]].
|
|
|
|
|
|
|
|
## <a id="api" href="#api">API</a>
|
|
|
|
|
|
|
|
### <a id="list-scans" href="#list-scans">List scans</a>
|
|
|
|
|
|
|
|
Scans are identified by their IDs, a list of which can be retrieved with:
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
GET /scans
|
|
|
|
|
|
|
|
#### Response
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"ids": [
|
|
|
|
"b72154c25c82aef00fbf9a16d04c1894",
|
|
|
|
"34e9ba35a3d793ae6e2562303d1a5e87"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### <a id="perform-a-new-scan" href="#perform-a-new-scan">Perform a new scan</a>
|
|
|
|
|
|
|
|
Scans will run in parallel, each in its own process.
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
POST /scans
|
|
|
|
|
|
|
|
Default options are:
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"url" : null,
|
|
|
|
"http" : {
|
|
|
|
"user_agent" : "Arachni/v2.0dev",
|
|
|
|
"request_timeout" : 10000,
|
|
|
|
"request_redirect_limit" : 5,
|
|
|
|
"request_concurrency" : 20,
|
|
|
|
"request_queue_size" : 100,
|
|
|
|
"request_headers" : {},
|
|
|
|
"response_max_size" : 500000,
|
|
|
|
"cookies" : {}
|
|
|
|
},
|
|
|
|
"audit" : {
|
|
|
|
"parameter_values" : true,
|
|
|
|
"exclude_vector_patterns" : [],
|
|
|
|
"include_vector_patterns" : [],
|
|
|
|
"link_templates" : []
|
|
|
|
},
|
|
|
|
"input" : {
|
|
|
|
"values" : {},
|
|
|
|
"default_values" : {
|
|
|
|
"(?i-mx:name)" : "arachni_name",
|
|
|
|
"(?i-mx:user)" : "arachni_user",
|
|
|
|
"(?i-mx:usr)" : "arachni_user",
|
|
|
|
"(?i-mx:pass)" : "5543!%arachni_secret",
|
|
|
|
"(?i-mx:txt)" : "arachni_text",
|
|
|
|
"(?i-mx:num)" : "132",
|
|
|
|
"(?i-mx:amount)" : "100",
|
|
|
|
"(?i-mx:mail)" : "arachni@email.gr",
|
|
|
|
"(?i-mx:account)" : "12",
|
|
|
|
"(?i-mx:id)" : "1"
|
|
|
|
},
|
|
|
|
"without_defaults" : false,
|
|
|
|
"force" : false
|
|
|
|
},
|
|
|
|
"browser_cluster" : {
|
|
|
|
"wait_for_elements" : {},
|
|
|
|
"pool_size" : 6,
|
|
|
|
"job_timeout" : 25,
|
|
|
|
"worker_time_to_live" : 100,
|
|
|
|
"ignore_images" : false,
|
|
|
|
"screen_width" : 1600,
|
|
|
|
"screen_height" : 1200
|
|
|
|
},
|
|
|
|
"scope" : {
|
|
|
|
"redundant_path_patterns" : {},
|
|
|
|
"dom_depth_limit" : 5,
|
|
|
|
"exclude_path_patterns" : [],
|
|
|
|
"exclude_content_patterns" : [],
|
|
|
|
"include_path_patterns" : [],
|
|
|
|
"restrict_paths" : [],
|
|
|
|
"extend_paths" : [],
|
|
|
|
"url_rewrites" : {}
|
|
|
|
},
|
|
|
|
"session" : {},
|
|
|
|
"checks" : [],
|
|
|
|
"platforms" : [],
|
|
|
|
"plugins" : {},
|
|
|
|
"no_fingerprinting" : false,
|
|
|
|
"authorized_by" : null
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
* Only the `url` option is mandatory.
|
|
|
|
* These options are passed to [Arachni::Options#update](http://www.rubydoc.info/github/Arachni/arachni/Arachni/Options#update-instance_method).
|
|
|
|
* Options expecting a type of `Regexp` (unavailable in JSON) can be specified as
|
|
|
|
type `String`.
|
|
|
|
* The string should not be enclosed in `/`; for example, use `test.*` instead of `/test.*/`.
|
|
|
|
|
|
|
|
#### Response
|
|
|
|
|
|
|
|
Currently, the response only contains the scan's ID.
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"id" : "1a34b6aca50bcf0065aca0a06a8d21ba"
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
##### Errors
|
|
|
|
|
|
|
|
A `500` error will be returned on invalid options:
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"error" : "Arachni::RPC::Exceptions::RemoteException: undefined method `stuff=' for #<Arachni::Options:0x00000001fa0dd8>",
|
|
|
|
"backtrace" : [
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/options.rb:276:in `block in update'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/options.rb:271:in `each'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/options.rb:271:in `update'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/rpc/server/active_options.rb:34:in `set'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/rpc/server/instance.rb:584:in `scan'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-rpc-0.2.1.2/lib/arachni/rpc/server.rb:207:in `call'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-rpc-0.2.1.2/lib/arachni/rpc/server/handler.rb:57:in `receive_request'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-rpc-0.2.1.2/lib/arachni/rpc/server/handler.rb:96:in `receive_object'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-rpc-0.2.1.2/lib/arachni/rpc/protocol.rb:52:in `on_read'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor/connection.rb:255:in `block in _read'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor/connection/error.rb:26:in `call'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor/connection/error.rb:26:in `translate'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor/connection.rb:254:in `_read'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor/connection/tls.rb:115:in `_read'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:574:in `each'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:574:in `block in process_connections'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:574:in `each'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:574:in `process_connections'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:316:in `block in run'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:307:in `loop'",
|
|
|
|
"/home/zapotek/.rvm/gems/ruby-2.2.0/gems/arachni-reactor-0.1.0/lib/arachni/reactor.rb:307:in `run'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/rpc/server/instance.rb:152:in `initialize'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/processes/executables/instance.rb:13:in `new'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/processes/executables/instance.rb:13:in `<top (required)>'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/processes/executables/base.rb:9:in `load'",
|
|
|
|
"/home/zapotek/workspace/arachni/lib/arachni/processes/executables/base.rb:9:in `<main>'"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### <a id="monitor-scan-progress" href="#monitor-scan-progress">Monitor scan progress</a>
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
GET /scans/:id
|
|
|
|
|
|
|
|
#### Response
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"status": "scanning",
|
|
|
|
"busy": true,
|
|
|
|
"seed": "c0c039750bef4f5688da4fba929b06ac",
|
|
|
|
"statistics": {
|
|
|
|
"http": {
|
|
|
|
"request_count": 1312,
|
|
|
|
"response_count": 1208,
|
|
|
|
"time_out_count": 0,
|
|
|
|
"total_responses_per_second": 145.55173283136,
|
|
|
|
"burst_response_time_sum": 0,
|
|
|
|
"burst_response_count": 0,
|
|
|
|
"burst_responses_per_second": 0,
|
|
|
|
"burst_average_response_time": 0,
|
|
|
|
"total_average_response_time": 0.12118887582781,
|
|
|
|
"max_concurrency": 20,
|
|
|
|
"original_max_concurrency": 20
|
|
|
|
},
|
|
|
|
"browser_cluster": {
|
|
|
|
"seconds_per_job": 1.6666666666667,
|
|
|
|
"total_job_time": 25,
|
|
|
|
"queued_job_count": 31,
|
|
|
|
"completed_job_count": 15
|
|
|
|
},
|
|
|
|
"runtime": 9.251885252,
|
|
|
|
"found_pages": 10,
|
|
|
|
"audited_pages": 2,
|
|
|
|
"current_page": "http:\/\/testhtml5.vulnweb.com\/ajax\/popular?offset=0"
|
|
|
|
},
|
|
|
|
"errors": [],
|
|
|
|
"messages": [],
|
|
|
|
"issues": [],
|
|
|
|
"sitemap": {}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
* `status` can be:
|
|
|
|
* `ready` -- Initialised and waiting for instructions.
|
|
|
|
* `preparing` -- Getting ready to start (i.e. initializing plugins etc.).
|
|
|
|
* `scanning` -- The instance is currently scanning the webapp.
|
|
|
|
* `pausing` -- The instance is being paused.
|
|
|
|
* `paused` -- The instance has been paused.
|
|
|
|
* `cleanup` -- The scan has completed and the instance is cleaning up
|
|
|
|
after itself (i.e. waiting for plugins to finish etc.).
|
|
|
|
* `aborted` -- The scan has been aborted, you can grab the report and shutdown.
|
|
|
|
* `done` -- The scan has completed, you can grab the report and shutdown.
|
|
|
|
* `busy`
|
|
|
|
* `true` -- The scan is still in progress.
|
|
|
|
* `false` -- The scan has finished, it is safe to grab the report and shutdown.
|
|
|
|
* `errors` -- Recoverable runtime errors.
|
|
|
|
* `issues` -- Identified issues.
|
|
|
|
* `sitemap` -- Scanned pages by URL and their HTTP status code.
|
|
|
|
* `url`: `code`
|
|
|
|
* `messages` -- Status messages.
|
|
|
|
|
|
|
|
So long as the client maintains a session with the service, only new `issues`, `sitemap`
|
|
|
|
entries and `errors` will be returned.
|
|
|
|
If no session is being maintained, each call will always return all data.
|
|
|
|
|
|
|
|
### <a id="pause-a-scan" href="#pause-a-scan">Pause a scan</a>
|
|
|
|
|
|
|
|
This is a soft pause, it will not kill the scanner process but merely make it
|
|
|
|
wait for a resume signal in order to continue the scan.
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
PUT /scans/:id/pause
|
|
|
|
|
|
|
|
### <a id="resume-a-scan" href="#resume-a-scan">Resume a scan</a>
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
PUT /scans/:id/resume
|
|
|
|
|
|
|
|
### <a id="retrieve-a-scan-report" href="#retrieve-a-scan-report">Retrieve a scan report</a>
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
GET /scans/:id/report
|
|
|
|
GET /scans/:id/report.json
|
|
|
|
GET /scans/:id/report.xml
|
|
|
|
GET /scans/:id/report.yaml
|
|
|
|
GET /scans/:id/report.html.zip
|
|
|
|
|
|
|
|
When the extension is missing, it will default to `json`.
|
|
|
|
|
|
|
|
#### Response
|
|
|
|
|
|
|
|
- [JSON](http://www.arachni-scanner.com/reports/report.json) (`json`)
|
|
|
|
- [XML](http://www.arachni-scanner.com/reports/report.xml) (`xml`).
|
|
|
|
- [YAML](http://www.arachni-scanner.com/reports/report.yml) (`yaml`)
|
|
|
|
- [HTML](http://www.arachni-scanner.com/reports/report.html/)
|
|
|
|
([zip](http://www.arachni-scanner.com/reports/report.html.zip)) (`html`).
|
|
|
|
|
|
|
|
### <a id="abort-or-shutdown-a-scan" href="#abort-or-shutdown-a-scan">Abort or shutdown a scan</a>
|
|
|
|
|
|
|
|
#### Request
|
|
|
|
|
|
|
|
DELETE /scans/:id
|
|
|
|
|
|
|
|
This call needs to take place after each scan is done in order to prevent zombie processes.
|
|
|
|
|
|
|
|
Once that call is made, the scan process will be killed and removed from the
|
|
|
|
service, if you wish to retrieve the report you will need to do so prior to performing
|
|
|
|
this call.
|
|
|
|
|
|
|
|
## <a id="example-client" href="#example-client">Example client</a>
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
require 'ap'
|
|
|
|
require 'typhoeus'
|
|
|
|
require 'json'
|
|
|
|
|
|
|
|
# Base URL of the REST service.
|
|
|
|
URL = 'http://localhost:7331'
|
|
|
|
|
|
|
|
# Cookie-Jar for the session.
|
|
|
|
#
|
|
|
|
# Allows for optimizations such as scan progress calls only returning issues not
|
|
|
|
# previously seen by previous calls, instead of all issues every single time.
|
|
|
|
COOKIE_JAR = '/tmp/arachni_rest_client.cookiejar'
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# HTTP helpers
|
|
|
|
###############
|
|
|
|
|
|
|
|
def request( path, options = {} )
|
|
|
|
if (data = options.delete(:data))
|
|
|
|
options[:body] = JSON.dump( data )
|
|
|
|
end
|
|
|
|
|
|
|
|
response = Typhoeus::Request.new(
|
|
|
|
"#{URL}/#{path}",
|
|
|
|
options.merge(
|
|
|
|
# Maintain a session.
|
|
|
|
cookiefile: COOKIE_JAR,
|
|
|
|
cookiejar: COOKIE_JAR,
|
|
|
|
|
|
|
|
# Enable compression.
|
|
|
|
accept_encoding: 'gzip, deflate',
|
|
|
|
)
|
|
|
|
).run
|
|
|
|
|
|
|
|
fail response.return_message if response.code == 0
|
|
|
|
|
|
|
|
JSON.load( response.body )
|
|
|
|
end
|
|
|
|
|
|
|
|
def get( path )
|
|
|
|
request( path )
|
|
|
|
end
|
|
|
|
|
|
|
|
def post( path, data = nil )
|
|
|
|
request( path, data: data, method: :post )
|
|
|
|
end
|
|
|
|
|
|
|
|
def delete( path )
|
|
|
|
request( path, method: :delete )
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Usage example
|
|
|
|
###############
|
|
|
|
|
|
|
|
# Start a new scan with the given options.
|
|
|
|
#
|
|
|
|
# The `id` included in the response data will allow us to manage it.
|
|
|
|
id = post( '/scans',
|
|
|
|
url: 'http://testhtml5.vulnweb.com',
|
|
|
|
|
|
|
|
# Only scan a few pages.
|
|
|
|
scope: {
|
|
|
|
page_limit: 10
|
|
|
|
},
|
|
|
|
|
|
|
|
# Load all checks.
|
|
|
|
checks: ['*']
|
|
|
|
)['id']
|
|
|
|
|
|
|
|
# Poll until the scan is finished.
|
|
|
|
loop do
|
|
|
|
print '.'
|
|
|
|
|
|
|
|
# Get the scan's progress.
|
|
|
|
#
|
|
|
|
# Status messages, runtime statistics, new issues, new errors etc.
|
|
|
|
progress = get( "/scans/#{id}" )
|
|
|
|
|
|
|
|
# Status messages (initializing something, pausing in a bit, etc.).
|
|
|
|
if progress['messages'].any?
|
|
|
|
puts
|
|
|
|
puts 'Messages:'
|
|
|
|
puts progress['messages'].join( "\n" )
|
|
|
|
end
|
|
|
|
|
|
|
|
# Because we're maintaining a session with the REST server, only issues
|
|
|
|
# which have not been previously seen will be returned by each call.
|
|
|
|
if progress['issues'].any?
|
|
|
|
puts
|
|
|
|
puts 'Issues:'
|
|
|
|
progress['issues'].each do |issue|
|
|
|
|
summary = "#{issue['name']} in '#{issue['vector']['type']}'"
|
|
|
|
|
|
|
|
# Passive issues don't have this.
|
|
|
|
if issue['affected_input_name']
|
|
|
|
summary << " input '#{issue['affected_input_name'].inspect}'"
|
|
|
|
summary << " using #{issue['affected_input_value'].inspect}"
|
|
|
|
end
|
|
|
|
|
|
|
|
summary << " at: #{issue['page']['dom']['url']}"
|
|
|
|
|
|
|
|
puts summary
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Same thing as before, only new sitemap entries will be returned for each call.
|
|
|
|
if progress['sitemap'].any?
|
|
|
|
puts
|
|
|
|
puts 'Scanned pages:'
|
|
|
|
progress['sitemap'].each do |url, code|
|
|
|
|
puts "[#{code}] #{url}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Same thing as before, only new errors will be returned for each call.
|
|
|
|
if progress['errors'].any?
|
|
|
|
puts
|
|
|
|
puts 'Errors:'
|
|
|
|
puts progress['errors'].join( "\n" )
|
|
|
|
end
|
|
|
|
|
|
|
|
# When `busy` is set to `false`, it means that the scan has completed.
|
|
|
|
break if !progress['busy']
|
|
|
|
sleep 1
|
|
|
|
end
|
|
|
|
|
|
|
|
puts
|
|
|
|
|
|
|
|
# Get the full scan report.
|
|
|
|
ap get( "/scans/#{id}/report" )
|
|
|
|
|
|
|
|
# Get a list of all living scan processes, of course, it will include ours.
|
|
|
|
puts 'Scan list:'
|
|
|
|
ap get( '/scans' )
|
|
|
|
|
|
|
|
# Since we're done with this one, kill it.
|
|
|
|
#
|
|
|
|
# Don't forget this, we don't want any zombie processes laying around.
|
|
|
|
print 'Shutting down scan...'
|
|
|
|
delete "/scans/#{id}"
|
|
|
|
puts '... done!'
|
|
|
|
|
|
|
|
# Lo and behold, our scan is no longer listed.
|
|
|
|
puts 'Scan list:'
|
|
|
|
ap get( '/scans' )
|
|
|
|
``` |