guides/developer/REST-API.md
0 → 100644
# 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)" : "[email protected]", | |||
"(?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' ) | |||
``` |