Scan Results Analysis

StackHawk provides API endpoints for iterating through the Scan Results created from HawkScan. API access gives savvy security teams the power tools to study their scan findings with ease.

📘

Scan Results API works better with Scans

API responses for scan results aren't particularly meaningful if you haven't first run HawkScan. The findings from HawkScan will be reported back from these endpoints to the API.

Scan Results, Scan Alerts, Scan Uris

When HawkScan is run, It creates a Scan that is associated with an existing Application. Scan results are grouped by application, and can be further filtered by Environment.

A completed scan may have many Alerts. These are triggered from Zap Plugins running in the scan, and can be influenced by the Scan Policy.

Each scan alert may have one or more Findings. These are the request and response pairs that popped for the plugin, triggering the alert.

Calling the List scans endpoint will return paginated applicationScanResults.
This endpoint requires an orgId, and will return paginated results of applicationScanResults for that organization. Each object includes the scan object with details of the underlying point-in-time scan.

Calling the List scan alerts endpoint with a given scanId returns an array of one applicationScanResults object corresponding with that scan.
This applicationScanResults object will also include a populated applicationAlerts field, which is the paginated results of Zap alerts found in that scan. The applicationAlerts also includes the pluginId identifying the Zap alert that triggered the scan.

Calling the Get scan alert findings endpoint with a given scanId and pluginId returns an alert object. This object contains the paginated applicationScanAlertUris, identifying each path in the scanned host that triggered this alert.

Scan results analysis API examples

These are examples of the StackHawk scan results API in action!

Firstly, make sure you've acquired your AUTH_TOKEN from the API auth login endpoint, and you have your organization id handy. You can find this on the account details page or from the current user.

Get the latest scan findings, sorted by severity

Find your latest scan ID, and use that to query for the scan alerts. Inline in code, you can sort the results by severity.

val AUTH_TOKEN= System.getenv("AUTH_TOKEN") ?: throw RuntimeException("AUTH_TOKEN expected")
val ORG_ID=System.getenv("ORG_ID") ?: throw RuntimeException("ORG_ID expected")

val client = OkHttpClient()
val gson = Gson()

// get the latest scan results for the organization
val scansRequest = Request.Builder()
        .url("https://api.stackhawk.com/api/v1/scan/$ORG_ID?pageSize=1&sortField=id&sortDir=desc")
        .get()
        .addHeader("Accept", "application/json")
        .addHeader("Authorization", "Bearer $AUTH_TOKEN")
        .build()

// use gson to serialize scans responseBody into a pojo (defined elsewhere)
val scansResponse = client.newCall(scansRequest).execute()
val listScanResultsResponse = gson.fromJson(responseBody.string(), ListScanResultsResponse::class.java);

// the latest scan id is the first and only paginated result
val latestScanId = listScanResultsResponse.applicationScanResults[0].scan.id

val alertsRequest = Request.Builder()
        .url("https://api.stackhawk.com/api/v1/scan/$latestScanId/alerts?pageSize=100")
        .get()
        .addHeader("Accept", "application/json")
        .addHeader("Authorization", "Bearer $AUTH_TOKEN")
        .build()

// using gson to serialize alerts responseBody into the same pojo, and paginated applicationAlerts
val alertsResponse = client.newCall(alertsRequest).execute()
val applicationAlerts = gson.fromJson(responseBody.string(), ListScanResultsResponse::class.java).applicationScanResults[0].applicationAlerts

// custom map to associate severities into sortable values
val severityMap = mapOf("High" to 3, "Medium" to 2, "Low" to 1)

val alertsBySeverity = applicationAlerts.sortBy { severityMap[it.severity] ?: 0 }

const fetch = require('node-fetch');

// Input API token and org ID
const authToken = "<AUTH_TOKEN>";
const orgId = "<ORG_ID>";

// Fetch options
const options = {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${authToken}`
    }
};

// Get the the latest scan ID    
let response = await fetch(`https://api.stackhawk.com/api/v1/scan/${orgId}?pageSize=10&sortField=id&sortDir=desc`, options).then(res => res.json()).catch(err => console.error('error:' + err));
const scanId = response.applicationScanResults[0].scan.id

// Get scan alerts sorted by severity
response = await fetch(`https://api.stackhawk.com/api/v1/scan/${scanId}/alerts?pageSize=10&sortField=id&sortDir=desc`, options).then(res => res.json()).catch(err => console.error('error:' + err));
const alerts = response.applicationScanResults[0].applicationAlerts
const highAlerts = alerts.filter((alerts) => alerts.severity == "High")
const mediumAlerts = alerts.filter((alerts) => alerts.severity == "Medium")
const lowAlerts = alerts.filter((alerts) => alerts.severity == "Low")
const alertsSorted = [ ...highAlerts, ...mediumAlerts, ...lowAlerts ]

// Print results 
console.log(alertsSorted);
# Input API token and org ID
AUTH_TOKEN=<AUTH_TOKEN>
ORG_ID=<ORG_ID>

# Get the the latest scan ID
LATEST_SCAN_ID=$( \
    curl \
        --request GET \
        --url 'https://api.stackhawk.com/api/v1/scan/'$ORG_ID'?pageSize=10&sortField=id&sortDir=desc' \
        --header 'Accept: application/json' \
        --header 'Authorization: Bearer '$AUTH_TOKEN \
    | jq -r '.applicationScanResults[0].scan.id' \
)
    
# Get scan alerts sorted by severity
ALERTS=$( \
    curl \
        --request GET \
        --url 'https://api.stackhawk.com/api/v1/scan/'$LATEST_SCAN_ID'/alerts?pageSize=10&sortField=id&sortDir=desc' \
        --header 'Accept: application/json' \
        --header 'Authorization: Bearer '$AUTH_TOKEN \
    | jq '.applicationScanResults[].applicationAlerts[]' \
    | jq 'if .severity == "High" then . + { severityPriority: 1 } elif .severity == "Medium" then . + { severityPriority: 2 } else . + { severityPriority: 3 } end' \
    | jq -s 'sort_by(.severityPriority)' \
)
  
# Print results  
echo $ALERTS \
| jq

Get all the vulnerable paths found from the latest scan, for each alert

Once again, you can use the latest SCAN_ID to get alerts for the scan. Then, foreach alert in the scan, you will get the findings of that alert.

val AUTH_TOKEN= System.getenv("AUTH_TOKEN") ?: throw RuntimeException("AUTH_TOKEN expected")
val SCAN_ID=System.getenv("SCAN_ID") ?: throw RuntimeException("SCAN_ID expected")

val client = OkHttpClient()
val gson = Gson()

val alertsRequest = Request.Builder()
        .url("https://api.stackhawk.com/api/v1/scan/$SCAN_ID/alerts?pageSize=100")
        .get()
        .addHeader("Accept", "application/json")
        .addHeader("Authorization", "Bearer $AUTH_TOKEN")
        .build()

// using gson to serialize alerts responseBody into the same pojo, and paginated applicationAlerts
val alertsResponse = client.newCall(alertsRequest).execute()
val applicationAlerts = gson.fromJson(responseBody.string(), ListScanResultsResponse::class.java).applicationScanResults[0].applicationAlerts

// will create a map of each alert name (eg. "Cross Site Scripting" to listOf("/index", "/about"))
val alertToVulnerablePaths = mutableMapOf<String, List<String>>()
applicationAlerts.forEach { alert ->
    
    // forEach alert, request the findings for that alert
    val pluginId = alert.pluginId
    val findingsRequest = Request.Builder()
            .url("https://api.stackhawk.com/api/v1/scan/$SCAN_ID/alert/$pluginId?pageSize=100")
            .get()
            .addHeader("Accept", "application/json")
            .addHeader("Authorization", "Bearer $AUTH_TOKEN")
            .build()

    // using gson to serialize alerts findingsResponse into an AlertResponse pojo, defined elsewhere
    val findingsResponse = client.newCall(findingsRequest).execute()
    val vulnerablePaths : List<String> = gson.fromJson(findingsResponse.string(), AlertResponse::class.java).applicationScanAlertUris.map { it.uri }
    
    // add to the map of alerts to paths 
    alertToVulnerablePaths.put(alert.name, vulnerablePaths)
}
const fetch = require('node-fetch');

// Input API token and org ID
const authToken = "<AUTH_TOKEN>";
const orgId = "<ORG_ID>";

// Get the the latest scan ID    
let url = `https://api.stackhawk.com/api/v1/scan/${orgId}?pageSize=10&sortField=id&sortDir=desc`;
let options = {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${authToken}`
    }
};
let response = await fetch(url, options).then(res => res.json()).catch(err => console.error('error:' + err));
const applicationScanResults = response.applicationScanResults;
let scanId;
let time;
for (let i = 0; i < applicationScanResults.length; i++) {
    if(i == 0) {
        scanId = applicationScanResults[i].scan.id;
        time = applicationScanResults[i].scan.timestamp;
    } else {
        if(applicationScanResults[i].scan.timestamp > time) {
            scanId = applicationScanResults[i].scan.id;
            time = applicationScanResults[i].scan.timestamp;
        }
    }
}

// Get scan alert plugin IDs
url = `https://api.stackhawk.com/api/v1/scan/${scanId}/alerts?pageSize=10&sortField=id&sortDir=desc`;
options = {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${authToken}`
    }
};
response = await fetch(url, options).then(res => res.json()).catch(err => console.error('error:' + err));
const plugins = response.applicationScanResults[0].applicationAlerts;
const plugin_ids = [];
for (let i = 0; i < plugins.length; i++) {
    plugin_ids.push(plugins[i].pluginId);
}

// Get scan alerts by plugin
const pluginPaths = [];
for (let i = 0; i < plugin_ids.length; i++) {
    url = `https://api.stackhawk.com/api/v1/scan/${scanId}/alert/${plugin_ids[i]}?pageSize=100`;
    options = {
        method: 'GET',
        headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${authToken}`
        }
    };
    response = await fetch(url, options).then(res => res.json()).catch(err => console.error('error:' + err));
    const applicationScanAlertUris = response.applicationScanAlertUris;
    const paths = [];
    for (let j = 0; j < applicationScanAlertUris.length; j++) {
        paths.push(applicationScanAlertUris[j].uri);
    }
    const pathsByPlugins = { pluginId: plugin_ids[i], uris: paths };
    pluginPaths.push(pathsByPlugins);
}

// Print results 
console.log(pluginPaths);
# Input API token and org ID
AUTH_TOKEN=<AUTH_TOKEN>
ORG_ID=<ORG_ID>

# Get the the latest scan ID
LATEST_SCAN_ID=$( \
    curl \
        --request GET \
        --url 'https://api.stackhawk.com/api/v1/scan/'$ORG_ID'?pageSize=10&sortField=id&sortDir=desc' \
        --header 'Accept: application/json' \
        --header 'Authorization: Bearer '$AUTH_TOKEN \
    | jq -r '.applicationScanResults[0].scan.id' \
)
  
# Get scan alert plugin IDs
PLUGIN_IDS=$( \
    curl \
        --request GET \
        --url 'https://api.stackhawk.com/api/v1/scan/'$LATEST_SCAN_ID'/alerts' \
        --header 'Accept: application/json' \
        --header 'Authorization: Bearer '$AUTH_TOKEN \
    | jq -r '.applicationScanResults[].applicationAlerts[].pluginId' \
    | jq -s \
)
  
# Get the number of plugin IDs
NUMBER_OF_PLUGINS=$( \
  echo $PLUGIN_IDS \
  | jq '. | length - 1' \
)
  
# Get scan alerts by plugin
for i in `seq 0 $NUMBER_OF_PLUGINS`;
do
    PLUGIN_ID=$( \
        echo $PLUGIN_IDS \
        | jq '.['$i']' \
    )
    PLUGIN_PATHS=$( \
        curl --request GET \
            --url 'https://api.stackhawk.com/api/v1/scan/'$LATEST_SCAN_ID'/alert/'$PLUGIN_ID'?pageSize=100' \
            --header 'Accept: application/json' \
            --header 'Authorization: Bearer '$AUTH_TOKEN \
        | jq -c '.applicationScanAlertUris[] | { pluginId: .pluginId, uri: .uri }' \
    )
    PATHS=$( \
        echo $PLUGIN_PATHS \
        | jq '.uri' \
        | jq -s \
    )
    PATHS_BY_PLUGIN=$( \
        echo '{ "pluginId": "'$PLUGIN_ID'", "uris": '$PATHS' }' \
    )
    PLUGINS_PATHS+=$PATHS_BY_PLUGIN
done

# Print results 
echo $PLUGINS_PATHS \
| jq -s

Get high severity alerts from all recent scans for a given environment

Scan results can be queried for a given ENVIRONMENT. Given the most recent scans filtered for that environment, you can query each Scan's alerts and the highest severity is selected.

val AUTH_TOKEN= System.getenv("AUTH_TOKEN") ?: throw RuntimeException("AUTH_TOKEN expected")
val ORG_ID=System.getenv("ORG_ID") ?: throw RuntimeException("ORG_ID expected")
val ENVIRONMENT=System.getenv("ENVIRONMENT") ?: throw RuntimeException("ENVIRONMENT expected")

val client = OkHttpClient()
val gson = Gson()

// get the latest scan results for the organization
val scansRequest = Request.Builder()
    .url("https://api.stackhawk.com/api/v1/scan/$ORG_ID?envs=$ENVIRONMENT&pageSize=10&sortField=id&sortDir=desc")
    .get()
    .addHeader("Accept", "application/json")
    .addHeader("Authorization", "Bearer $AUTH_TOKEN")
    .build()

// use gson to serialize scans responseBody into a pojo (defined elsewhere)
val scansResponse = client.newCall(scansRequest).execute()
val listScanResultsResponse = gson.fromJson(responseBody.string(), ListScanResultsResponse::class.java);

// get the scan IDs for the scan results
val scanIds = listScanResultsResponse.applicationScanResults.map { it.scan.id }

// Iterate through scan IDs and returning high severity alerts
val highSeverityAlerts = scanIds.flatMap { scanId ->
    val alertsRequest = Request.Builder()
        .url("https://api.stackhawk.com/api/v1/scan/$scanId/alerts?pageSize=100")
        .get()
        .addHeader("Accept", "application/json")
        .addHeader("Authorization", "Bearer $AUTH_TOKEN")
        .build()

    // using gson to serialize alerts responseBody into the same pojo, and paginated applicationAlerts
    val alertsResponse = client.newCall(alertsRequest).execute()
    val applicationAlerts = gson.fromJson(responseBody.string(), ListScanResultsResponse::class.java).applicationScanResults[0].applicationAlerts

    applicationAlerts.filter { it.severity == "High" }
}
const fetch = require('node-fetch');

// Input API token, org ID and environment
const authToken = "<AUTH_TOKEN>";
const orgId = "<ORG_ID>";
const environment = "<ENVIRONMENT>"

// Get scan IDs of all recent scans for an environment
let url = `https://api.stackhawk.com/api/v1/scan/${orgId}?envs=${environment}&pageSize=10&sortField=id&sortDir=desc`;
let options = {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${authToken}`
    }
};
let response = await fetch(url, options).then(res => res.json()).catch(err => console.error('error:' + err));
const scanIds = response.applicationScanResults.map((scans) => scans.scan.id)

// Get high severity alerts from scans
const scanAlerts = []
for (let i = 0; i < scanIds.length; i++) {
    url = `https://api.stackhawk.com/api/v1/scan/${scanIds[i]}/alerts?pageSize=10&sortField=id&sortDir=desc`;
    options = {
        method: 'GET',
        headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${authToken}`
        }
    };
    response = await fetch(url, options).then(res => res.json()).catch(err => console.error('error:' + err));
    const highAlerts = response.applicationScanResults[0].applicationAlerts.filter((alerts) => alerts.severity == "High")
    const scanAlert = { scanId: scanIds[i], highAlerts: highAlerts };
    scanAlerts.push(scanAlert);
}

// Print results 
console.log(scanAlerts);
# Input API token, org ID and environment
AUTH_TOKEN=<AUTH_TOKEN>
ORG_ID=<ORG_ID>
ENVIRONMENT=<ENVIRONMENT>

# Get scan IDs of all recent scans for an environment
SCAN_IDS=$( \
    curl \
        --request GET \
        --url 'https://api.stackhawk.com/api/v1/scan/'$ORG_ID'?envs='$ENVIRONMENT'&pageSize=10&sortField=id&sortDir=desc' \
        --header 'Accept: application/json' \
        --header 'Authorization: Bearer '$AUTH_TOKEN \
    | jq '.applicationScanResults[]' \
    | jq '. | .scan.id' \
    | jq -s \
)
    
# Get the number of scan IDs
NUMBER_OF_SCANS=$( \
    echo $SCAN_IDS \
    | jq '. | length - 1' \
)

# Get high severity alerts from scans
for i in `seq 0 $NUMBER_OF_SCANS`;
do
    SCAN_ID=$( \
        echo $SCAN_IDS \
        | jq -r '.['$i']'
    )
    ALERTS=$( \
        curl \
            --request GET \
            --url 'https://api.stackhawk.com/api/v1/scan/'$SCAN_ID'/alerts?pageSize=100&sortField=id&sortDir=desc' \
            --header 'Accept: application/json' \
            --header 'Authorization: Bearer '$AUTH_TOKEN \
        | jq '.applicationScanResults[].applicationAlerts[]' \
        | jq -s \
    )
    NUMBER_OF_ALERTS=$( \
        echo $ALERTS \
        | jq '. | length - 1' \
    )

    for j in `seq 0 $NUMBER_OF_ALERTS`;
    do
        ALERT=$( \
            echo $ALERTS \
            | jq '.['$j']' \
        )
        HIGH_ALERTS+=$( \
            echo $ALERT \
            | jq 'if .severity == "High" then . else empty end' \
        )
    done
    SCAN_ALERTS+=$( \
        echo $HIGH_ALERTS \
        | jq -s \
        | jq '. | { "scanId": "'$SCAN_ID'", "highAlerts": . }' \
    )
    HIGH_ALERTS=
done

# Print results
echo $SCAN_ALERTS \
| jq -s