Skip to content

GraphQL Examples

The examples here send HTTP requests to the Horizon3.ai GraphQL API endpoint, which gets referenced in the commands below via environment variable H3_API_URL:

export H3_API_URL=https://api.horizon3ai.com/v1/graphql

Before proceeding, first perform API authentication and store the JWT in environment variable H3_API_JWT:

export H3_API_JWT=<token-returned-from-auth>

Hello World

Let's start with the simple hello (world) query.

For documentation of this query, see API Reference for Queries > hello.

curl \
  -X POST $H3_API_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $H3_API_JWT" \
  -d '{"query": "query HelloWorld { hello }"}'
import os
import requests

query = '''
    query HelloWorld {
      hello
    }
'''
url = os.environ["H3_API_URL"]
headers = {"Authorization": f"Bearer {os.environ['H3_API_JWT']}"}
response = requests.post(url, headers=headers, json={"query": query})
result = response.json() if response.status_code == 200 else None
Result
    {
      "data": {
        "hello": "world!"
      }
    }

Pentest data

Let's review how to fetch data associated with a given pentest, such as start/stop times, number of weaknesses found, credentials discovered, and hosts scanned. A lot more data can be queried than what is shown in this example, see the API Reference for more info.

For documentation of this query, see API Reference for Queries > pentest.

curl \
  -X POST $H3_API_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $H3_API_JWT" \
  -d @- <<HERE
{
  "query": "
    query GetPentest(\$op_id: String!) {
      pentest(op_id: \$op_id) {
        op_id
        op_type
        name
        state
        launched_at
        completed_at
        min_scope
        max_scope
        exclude_scope
        weaknesses_count
        credentials_count
        cred_access_count
      }
    }",
  "variables": {
    "op_id": "12341234-1234-1234-1234-123412341234"
  }
}
HERE
import os
import requests

query = '''
    query GetPentest($op_id: String!) {
      pentest(op_id: $op_id) {
        op_id
        op_type
        name
        state
        launched_at
        completed_at
        min_scope
        max_scope
        exclude_scope
        weaknesses_count
        credentials_count
        cred_access_count
      }
    }
'''
variables = {
    "op_id": "12341234-1234-1234-1234-123412341234"
}
url = os.environ["H3_API_URL"]
headers = {"Authorization": f"Bearer {os.environ['H3_API_JWT']}"}
response = requests.post(url, headers=headers, json={"query": query, "variables": variables})
result = response.json() if response.status_code == 200 else None
Result
    {
      "data": {
        "pentest": {
          "op_id": "12341234-1234-1234-1234-123412341234",
          "op_type": "NodeZero",
          "name": "Sample Pentest",
          "state": "done",
          "launched_at": "2022-04-25T14:41:18",
          "completed_at": "2022-04-25T15:23:21",
          "min_scope": null,
          "max_scope": null,
          "exclude_scope": [],
          "weaknesses_count": 53,
          "credentials_count": 33,
          "cred_access_count": 142
        }
      }
    }

Need more or less data?

One of the benefits of GraphQL is the ability to only query the data you need. In this example, add or remove fields of the Pentest type to suite your needs. This avoids the issue of over-fetching and eliminates the difficulty of parsing the returned data.

Pentest reports

Let's review how to download a zip containing CSV data files and PDF reports associated with a given pentest. This approach to fetching data from the API is in contrast to the granular approach demonstrated in the previous section on querying pentest data.

For documentation of this query, see API Reference for Queries > pentest_reports_zip_url.

curl \
  -X POST $H3_API_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $H3_API_JWT" \
  -d @- <<HERE
{
  "query": "
    query GetPentestReports(\$input: OpInput!) {
      pentest_reports_zip_url(input: \$input)
    }",
  "variables": {
    "input": {
      "op_id": "12341234-1234-1234-1234-123412341234"
    }
  }
}
HERE
import os
import requests

query = '''
    query GetPentestReports($input: OpInput!) {
      pentest_reports_zip_url(input: $input)
    }
'''
variables = {
    "input": {
        "op_id": "12341234-1234-1234-1234-123412341234"
    }
}
url = os.environ["H3_API_URL"]
headers = {"Authorization": f"Bearer {os.environ['H3_API_JWT']}"}
response = requests.post(url, headers=headers, json={"query": query, "variables": variables})
result = response.json() if response.status_code == 200 else None
Result
    {
      "data": {
        "pentest_reports_zip_url": "<temporary-url-to-download-zip>"
      }
    }

Paginate action logs

Some queries fetch lists of data, e.g. users, pentests, actions logs, etc., but the amount of data can be substantial. Pagination allows us to limit the quantity of data retrieved. Let's take a look at how this works by querying the two (2) most recent action logs for a given pentest.

For documentation of this query, see API Reference for Queries > action_logs_page.

curl \
  -X POST $H3_API_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $H3_API_JWT" \
  -d @- <<HERE
{
  "query": "
    query GetActionLogs(\$input: OpInput!, \$page_input: PageInput) {
      action_logs_page(input: \$input, page_input: \$page_input) {
        page_info {
          ...PageInfoFragment
        }
        action_logs {
          ...ActionLogFragment
        }
      }
    }

    fragment PageInfoFragment on PageInfo {
      page_num
      page_size
    }

    fragment ActionLogFragment on ActionLog {
      uuid
      endpoint_ip
      start_time
      end_time
      cmd
      module_id
      module_name
      module_description
    }
  ",
  "variables": {
    "input": {
      "op_id": "12341234-1234-1234-1234-123412341234"
    },
    "page_input": {
      "page_num": 1,
      "page_size": 2,
      "order_by": "start_time",
      "sort_order": "DESC"
    }
  }
}
HERE
import os
import requests

query = '''
    query GetActionLogs($input: OpInput!, $page_input: PageInput) {
      action_logs_page(input: $input, page_input: $page_input) {
        page_info {
          ...PageInfoFragment
        }
        action_logs {
          ...ActionLogFragment
        }
      }
    }

    fragment PageInfoFragment on PageInfo {
      page_num
      page_size
    }

    fragment ActionLogFragment on ActionLog {
      uuid
      endpoint_ip
      start_time
      end_time
      cmd
      module_id
      module_name
      module_description
    }
'''
variables = {
    "input": {
        "op_id": "12341234-1234-1234-1234-123412341234"
    },
    "page_input": {
        "page_num": 1,
        "page_size": 2,
        "order_by": "start_time",
        "sort_order": "DESC"
    }
}
url = os.environ["H3_API_URL"]
headers = {"Authorization": f"Bearer {os.environ['H3_API_JWT']}"}
response = requests.post(url, headers=headers, json={"query": query, "variables": variables})
result = response.json() if response.status_code == 200 else None
Result
    {
      "data": {
        "action_logs_page": {
          "page_info": {
            "page_num": 1,
            "page_size": 2
          },
          "action_logs": [
            {
              "uuid": "12341234-1234-1234-1234-123412341234/20307",
              "endpoint_ip": "10.0.255.10",
              "start_time": "2020-09-30T21:41:22",
              "end_time": "2020-09-30T21:41:22",
              "cmd": "rpcclient 10.0.255.10 -c enumdomusers -U xadmin%L********n",
              "module_id": "List Users Over RPC",
              "module_name": "List Users Over RPC",
              "module_description": "The List Users Over Remote Procedure Call (RPC) utilizes rpcclient and a user credential discovered during an operation to dump all of the domain users."
            },
            {
              "uuid": "12341234-1234-1234-1234-123412341234/20308",
              "endpoint_ip": "10.0.255.10",
              "start_time": "2020-09-30T21:41:22",
              "end_time": "2020-09-30T21:41:23",
              "cmd": "net rpc group members Administrators -I 10.0.255.10 -U SMOKE.NET\\xadmin%L********n",
              "module_id": "Enumerate Admins Over RPC",
              "module_name": "Enumerate Admins Over RPC",
              "module_description": "The Enumerate Admins Over RPC module uses network Remote Procedure Call (RPC) to determine the administrative accounts available on an endpoint and also, if the host is a Domain Controller (DC), will try to gather the Domain Admin accounts."
            }
          ]
        }
      }
    }

GraphQL fragments

You may have noticed this example used fragments to succinctly define fields to query on a given type. To learn more, see GraphQL fragments from graphql.org.

Create internal pentest

Let's consider how we can create a new internal pentest via the API. This will require us to send a mutation to the GraphQL API, instead of a query. Mutations are used when creating, updating, or deleting resources, whereas queries are intended only for fetching existing resources.

Creating vs launching pentest

Creating a pentest is the process of configuring and preparing a pentest to be launched. The process of launching a pentest is handled separately, after creating the pentest, by deploying NodeZero in your environment. This is explained more in a later section.

For documentation of this query, see API Reference for Mutations > schedule_op_template.

curl \
  -X POST $H3_API_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $H3_API_JWT" \
  -d @- <<HERE
{
  "query": "
    mutation CreatePentest(
      \$op_template_name: String!
      \$op_name: String
    ) {
      schedule_op_template(
        op_template_name: \$op_template_name
        op_name: \$op_name
      ) {
        op {
          ...OpFragment
        }
      }
    }

    fragment OpFragment on Op {
      op_id
      op_name
      op_state
      op_type
      scheduled_timestamp_iso
      launched_timestamp_iso
      nodezero_script_url
    }
  ",
  "variables": {
    "op_template_name": "Default 1 - Recommended",
    "op_name": "Pentest created via API"
  }
}
HERE
import os
import requests

query = '''
    mutation CreatePentest(
      $op_template_name: String!
      $op_name: String
    ) {
      schedule_op_template(
        op_template_name: $op_template_name
        op_name: $op_name
      ) {
        op {
          ...OpFragment
        }
      }
    }

    fragment OpFragment on Op {
      op_id
      op_name
      op_state
      op_type
      scheduled_timestamp_iso
      launched_timestamp_iso
      nodezero_script_url
    }
'''
variables = {
    "op_template_name": "Default 1 - Recommended",
    "op_name": "Pentest created via API"
}
url = os.environ["H3_API_URL"]
headers = {"Authorization": f"Bearer {os.environ['H3_API_JWT']}"}
response = requests.post(url, headers=headers, json={"query": query, "variables": variables})
result = response.json() if response.status_code == 200 else None
Result
    {
      "data": {
        "schedule_op_template": {
          "op": {
            "op_id": "df087532-b6ca-48b1-bcd3-d8b1968a197f",
            "op_name": "Pentest created via API",
            "op_state": "scheduled",
            "op_type": "NodeZero",
            "scheduled_timestamp_iso": "2023-03-04T16:19:42",
            "launched_timestamp_iso": null,
            "nodezero_script_url": <url-to-download-launch-script>
          }
        }
      }
    }

In example above, the default template, Default 1 - Recommended, was used to define the pentest configuration. To create your own op template, it is recommended to do so from the Horizon.ai Portal, but you may also use the API with Mutations > save_op_template.

What is an op template?

An "op template" refers to a predefined pentest configuration, including settings for hosts to scan, passwords to spray, and other attack config. You may create op templates for various use cases and environment(s). Each op template gets stored in your client account and can be used when creating pentests.

Ready for launch!

Continue reading to see how our newly created pentest can be launched. It is evident that the pentest has yet be launched based on the null value for launched_timestamp_iso in the result above.

Launch NodeZero®

Once an internal pentest has been created, it is ready for NodeZero to deploy in the intended environment. To do so, simply copy the value returned in nodezero_script_url from the mutation above, then pass it to curl and pipe the downloaded NodeZero script to bash:

NodeZero host only

The launch script must be run from the NodeZero host inside your intended environment. For more information on configuring this host, see Setup NodeZero Host.

curl <nodezero_script_url> | bash

Once the command above successfully runs, the autonomous pentest has officially deployed. To monitor its progress and notable events, go to the pentest Real-Time View in the Horizon3.ai Portal.

Scheduling pentests

Despite the mutation name used above, i.e. schedule_op_template, it does not allow a pentest to be scheduled at a specific time. It only creates the pentest, preparing NodeZero for deployment in your environment.

CLI Tool

Our ready-to-use CLI Tool provides the ability to schedule recurring pentests and much more.

Next Step

description API Reference chevron_right
Explore the graphql schema and create your own queries to run.