Skip to content

Getting Started with GraphQL

The motivation for using GraphQL is explained, and examples of common scenarios are provided to help get started with using the Horizon3.ai API. This will help develop an understanding of how to use the GraphQL API to interact with your data.

Authentication

Before getting start with the examples here, be sure to review the section on API Authentication.

Leverage the full power

Once you are comfortable interacting with the GraphQL API, head over to our API Reference for a complete list of queries and mutations available.

Questions or comments

To provide feedback, please use the form at the bottom of this page.

Why GraphQL?

The ITOps, SecOps, and automations engineering worlds are filled with REST APIs. So why did we choose a GraphQL API?

Data is power. We have a good idea how you might be able to use it, but it would be wrong of us to believe that we know better than you. GraphQL gives our developers far more control and flexibility through the ability to shape API queries to meet their needs. We wanted to provide the same power to our API users.

GraphQL.org

If you are new to GraphQL, we highly recommended reviewing the learning material at GraphQL.org.

HTTP Client

To send requests to the GraphQL API, an HTTP client is needed. This is mostly a personal choice and/or dependent on the systems you may be integrating with. For example, if you are trying to build your own HTML interface, you will likely need a client that runs in JavaScript to be compatible with internet browsers.

Here are a few examples of useful HTTP clients:

Need a specific programming language?

HTTP clients exist for every major programming language. A quick internet search should reveal the most well-known HTTP client(s) in any programming language.

Command Line Interface

Also be sure to check out the Horizon3.ai CLI for a ready-to-use API client.

Status codes

HTTP status codes returned by the /graphql endpoint are summarized below.

Code Title Description
200 Success Response contains the requested data.
400 Bad Request Malformed request, see errors field in the response data.
401 Unauthorized Not authenticated due to an invalid or expired JWT.
403 Forbidden Not authorized to access the requested resource.
5xx Internal Server Error An unexpected error was encountered.

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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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.