In the two previous installments of this series, we have shown you how to manage Acunetix scans using Bash and PowerShell together with the Acunetix API. In this article, you will learn how to do the same using Python. As an example, we will create a Python script that uses the requests library to make requests to the Acunetix REST API.
Every API call must be authenticated by providing the API key in the HTTP request headers, hence each request is made with the following headers:
Content-Type: application/json
X-Auth: <my_api_key>
The API Key can be retrieved from the Acunetix profile page.
Anatomy of the Script
The script has the following structure:
- First, we declare the cleanup function, which is a recovery function to delete the scan and target that were created by the script if an invalid scan status is detected:
- The remove_scan API call requires us to make a DELETE request to the /scans/{scan_id} endpoint; the scan ID is retrieved by the main part of the script described below
- The remove_target API call requires us to make a DELETE request to the /targets/{target_id} endpoint; the target ID is retrieved by the main part of the script described below
- Then, we declare the starting global variables to be used throughout the script:
- MyAXURL is the base URL for your Acunetix API, which is typically:
- On-premises: https://<your_fqdn>:3443/api/v1
- Online: https://online.acunetix.com/api/v1
- MyAPIKEY is the API key, which can be retrieved from the profile page
- MyTargetURL is the URL of the target to eventually be scanned
- MyTargetDESC is a friendly description for the target
- FullScanProfileID is the profile ID for the default Full Scan; this default scan profile always has a value of 11111111-1111-1111-1111-111111111111
- MyRequestHeaders is the array of HTTP request headers (including authentication with the Acunetix API key)
- MyAXURL is the base URL for your Acunetix API, which is typically:
- Next, we create the target:
- The API documentation for the add_target function shows that:
- We have to make a POST request to the /targets endpoint;
- The body should be in JSON format and contain at least 4 keys: address, description, type, and criticality; in our example:
{ "address": "http://testphp.vulnweb.com/", "description": "Test PHP Site - created via ax-python-api.ps1", "type": "default", "criticality": 10 }
- The response will be in JSON format; the critical information we want to extract from the response is the value of the target_id key
- The API documentation for the add_target function shows that:
- As a next step, we schedule the scan:
- The API documentation for the schedule_scan function shows that:
- We have to make a POST request to the /scans endpoint;
- The body should be in JSON format; in our example:
{ "profile_id": "11111111-1111-1111-1111-111111111111", "incremental": false, "schedule": { "disable": false, "start_date": null, "time_sensitive": false }, "user_authorized_to_scan": "yes", "target_id": "TargetID_from_previous_step" }
- The response will be in JSON format; however, the critical information (the scan ID) we want to extract is not delivered in the response but in the HTTP response header called Location
- The API documentation for the schedule_scan function shows that:
- Then, we create a loop that checks the status of the scan every 30 seconds and waits for the scan status to become completed:
- The API documentation for the get_scan function shows that:
- We have to make a GET request to the /scans/{scan_id} endpoint, where the scan_id is obtained from the previous step
- The response will be in JSON format; the critical information (the scan status) we want to extract is the value of the status key inside the nested JSON object with key current_session
- If the scan status is processing or scheduled, the script continues to wait; when the scan status changes to completed, the script continues processing; if the scan status is not one of these 3 values, it is considered invalid, the cleanup function is called, and the script ends
- The API documentation for the get_scan function shows that:
- Next, we need to obtain the scan session ID; the response to the get_scan API call also contains the scan session ID; the critical information we want to extract is the value of the scan_session_id key inside the nested JSON object with key current_session
- Then, we need to obtain the scan result ID; to obtain it, we need to do the following:
- The API documentation for the get_scan_result_history API call shows that:
- We have to make a GET request to the /scans/{scan_id}/results endpoint, where the scan_id was obtained from a previous step
- The response is in JSON format; the critical information (the scan result ID) we want to extract is the value of the result_id key inside one of the arrays of JSON objects forming the value for the results key; since the script creates a single scan for a single target, the situation is simplified such that we only expect to get a single JSON object nested inside the results key
- The API documentation for the get_scan_result_history API call shows that:
- Finally, we need to obtain the list of vulnerabilities generated by the scan:
- The API documentation for the get_scan_vulnerabilities API call shows that:
- We have to make a GET request to the /scans/{scan_id}/results/{result_id}/vulnerabilities endpoint, where the scan_id and the result_id were obtained from previous steps
- The response is in JSON format and contains 2 keys:
- The vulnerabilities key contains an array of all the vulnerabilities (up to 100) found for the scan scheduled previously
- The pagination key contains information about the number of pages and how to retrieve subsequent pages in the case that the number of vulnerabilities is indeed more than 100
- The API documentation for the get_scan_vulnerabilities API call shows that:
Python Script
# Import required libraries
import json, requests, ssl, time, urllib3
​
def cleanup():
# Delete the scan
dummy = requests.delete(MyAXURL + '/scans/' + MyScanID, headers = MyRequestHeaders, verify=False)
# Delete the target
dummy = requests.delete(MyAXURL + '/targets/' + MyTargetID, headers = MyRequestHeaders, verify=False)
​
# Declare variables
MyAXURL = "https://qgen-004.qgengroup.local:3443/api/v1"
MyAPIKEY = "1986ad8c0a5b3df4d7028d5f3c06e936c2a54cb301c8342b8b047b25985b4205f"
MyTargetURL = "http://testphp.vulnweb.com/"
MyTargetDESC = "Test PHP Site - created via ax-python-api.py"
FullScanProfileID = "11111111-1111-1111-1111-111111111111"
MyRequestHeaders = {'X-Auth':MyAPIKEY, 'Content-Type':'application/json'}
​
# Create our intended target - target ID is in the JSON response
MyRequestBody = {"address":MyTargetURL,"description":MyTargetDESC,"type":"default","criticality":10}
MyTargetIDResponse = requests.post(MyAXURL + '/targets', json=MyRequestBody, headers = MyRequestHeaders, verify=False)
MyTargetIDjson=json.loads(MyTargetIDResponse.content)
MyTargetID=MyTargetIDjson["target_id"]
​
# Trigger a scan on the target - scan ID is in the HTTP response headers
MyRequestBody = {"profile_id":FullScanProfileID,"incremental":False,"schedule":{"disable":False,"start_date":None,"time_sensitive":False},"user_authorized_to_scan":"yes","target_id":MyTargetID}
MyScanIDResponse = requests.post(MyAXURL + '/scans', json=MyRequestBody, headers = MyRequestHeaders, verify=False)
MyScanID = MyScanIDResponse.headers["Location"].replace("/api/v1/scans/","")
​
LoopCondition=True
while LoopCondition :
MyScanStatusResponse = requests.get(MyAXURL + '/scans/' + MyScanID, headers = MyRequestHeaders, verify=False)
MyScanStatusjson = json.loads(MyScanStatusResponse.content)
MyScanStatus = MyScanStatusjson["current_session"]["status"]
if (MyScanStatus=="processing"):
print("Scan Status: Processing - waiting 30 seconds...")
elif (MyScanStatus=="scheduled"):
print("Scan Status: Scheduled - waiting 30 seconds...")
elif (MyScanStatus=="completed"):
LoopCondition=False
else:
print("Invalid Scan Status: Aborting")
cleanup
exit()
MyScanStatus=""
time.sleep(30)
​
# Obtain the scan session ID
MyScanSessionResponse = requests.get(MyAXURL + '/scans/' + MyScanID, headers = MyRequestHeaders, verify=False)
MyScanSessionjson = json.loads(MyScanSessionResponse.content)
MyScanSessionID = MyScanSessionjson["current_session"]["scan_session_id"]
​
# Obtain the scan result ID
MyScanResultResponse = requests.get(MyAXURL + '/scans/' + MyScanID + "/results", headers = MyRequestHeaders, verify=False)
MyScanResultjson = json.loads(MyScanResultResponse.content)
MyScanResultID = MyScanResultjson["results"][0]["result_id"]
​
# Obtain scan vulnerabilities
MyScanVulnerabilitiesResponse = requests.get(MyAXURL + '/scans/' + MyScanID + '/results/' + MyScanResultID + '/vulnerabilities', headers = MyRequestHeaders, verify=False)
​
print ""
print "Target ID: " + MyTargetID
print "Scan ID: " + MyScanID
print "Scan Session ID: " + MyScanSessionID
print "Scan Result ID: " + MyScanResultID
print ""
print ""
print "Scan Vulnerabilities"
print "===================="
print ""
print MyScanVulnerabilitiesResponse.content
Get the latest content on web security
in your inbox each week.