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:
- curl - useful for command line applications
- Python requests - useful for building Python applications
- Altair GraphQL Client - useful for testing and building API requests
- Postman API Client - useful for testing and building API requests
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.