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.gateway.horizon3ai.com/v1/graphql
export H3_API_URL=https://api.gateway.horizon3ai.eu/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
h3 pentest <op-id>
The command above runs the GraphQL query defined in h3-cli/queries/pentest.graphql.
You can also run your own custom GraphQL queries and mutations using h3-cli.
Save the following query to a file named my_pentest.graphql.
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
}
}
Then run the query using the following command:
h3 gql ./my_pentest.graphql '{"op_id":"<op-id>"}'
{
"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 > create_op.
curl \
-X POST $H3_API_URL \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $H3_API_JWT" \
-d @- <<HERE
{
"query": "
mutation CreatePentest(
\$op_name: String!
\$op_type: OpType!
) {
create_op(
schedule_op_form: {
op_name: \$op_name
op_type: \$op_type
}
) {
op {
...OpFragment
}
}
}
fragment OpFragment on Op {
op_id
op_name
op_state
op_type
scheduled_at
launched_at
nodezero_script_url
}
",
"variables": {
"op_name": "Pentest created via API",
"op_type": "NodeZero"
}
}
HERE
import os
import requests
query = '''
mutation CreatePentest(
$op_name: String!
$op_type: OpType!
) {
create_op(
schedule_op_form: {
op_name: $op_name
op_type: $op_type
}
) {
op {
...OpFragment
}
}
}
fragment OpFragment on Op {
op_id
op_name
op_state
op_type
scheduled_at
launched_at
nodezero_script_url
}
'''
variables = {
"op_name": "Pentest created via API",
"op_type": "NodeZero"
}
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": {
"create_op": {
"op": {
"op_id": "df087532-b6ca-48b1-bcd3-d8b1968a197f",
"op_name": "Pentest created via API",
"op_state": "scheduled",
"op_type": "NodeZero",
"scheduled_at": "2023-03-04T16:19:42",
"launched_at": 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 > create_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_at 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¶
See the Setup -> Schedules section of the API Reference to learn how to set up schedules for running pentests on a regular basis, for example weekly or monthly.