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' ) | ||
``` |