feat(axonius): new Axonius asset-management integration

Axonius REST API, 5 commands: list/get devices, device count, list users (AQL
filters). API-key + API-secret auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-07-12 14:41:43 +02:00
parent 088694fdc2
commit df8a2c1187
6 changed files with 347 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
id: axonius
name: Axonius
version: 1.0.0
description: "Axonius (REST API) — cybersecurity asset management: query devices and users with AQL filters, get a device by ID, and count devices matching a filter. API-key + API-secret authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: list/get devices, device count, list users."
category: asset_management
# Per-instance configuration. Auth uses the 'api-key' and 'api-secret' headers.
config_schema:
properties:
base_url:
type: string
description: "Axonius instance URL (e.g. https://axonius.example.com)"
api_key:
type: string
description: "API key"
x-soar-sensitive: true
api_secret:
type: string
description: "API secret"
x-soar-sensitive: true
required:
- base_url
- api_key
- api_secret
commands:
- id: list_devices
name: axonius-list-devices
description: "Query devices with an optional AQL filter."
risk: read
inputs_schema:
properties:
filter: { type: string, description: "AQL filter (e.g. specific_data.data.hostname == \"host01\")" }
limit: { type: number, description: "Max devices (default 50)" }
offset: { type: number, description: "Offset (default 0)" }
required: []
outputs_schema: { properties: {} }
- id: get_device
name: axonius-get-device
description: "Get a single device by its internal Axonius ID."
risk: read
inputs_schema:
properties:
device_id: { type: string, description: "Internal Axonius device ID" }
required: [device_id]
outputs_schema: { properties: {} }
- id: device_count
name: axonius-device-count
description: "Count devices matching an AQL filter."
risk: read
inputs_schema:
properties:
filter: { type: string, description: "AQL filter (empty = all devices)" }
required: []
outputs_schema: { properties: {} }
- id: list_users
name: axonius-list-users
description: "Query users with an optional AQL filter."
risk: read
inputs_schema:
properties:
filter: { type: string, description: "AQL filter" }
limit: { type: number, description: "Max users (default 50)" }
offset: { type: number, description: "Offset (default 0)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: axonius-test-connection
description: "Verify connectivity and credentials (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,52 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def request(method, path, cfg, body=None):
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"api-key": str(cfg.get("api_key", "")),
"api-secret": str(cfg.get("api_secret", "")),
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
filter_ = inputs.get("filter")
body = {"data": {}}
if filter_:
body["data"]["filter"] = filter_
return request("POST", "/devices/count", cfg, body=body)
_run(main)
@@ -0,0 +1,50 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def request(method, path, cfg, body=None):
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"api-key": str(cfg.get("api_key", "")),
"api-secret": str(cfg.get("api_secret", "")),
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
device_id = inputs.get("device_id")
if not device_id:
raise Exception("device_id is required")
return request("GET", "/devices/" + q(device_id), cfg)
_run(main)
@@ -0,0 +1,61 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def request(method, path, cfg, body=None):
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"api-key": str(cfg.get("api_key", "")),
"api-secret": str(cfg.get("api_secret", "")),
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
filter_ = inputs.get("filter")
limit = inputs.get("limit")
offset = inputs.get("offset")
body = {
"data": {
"page": {
"limit": int(limit or 50),
"offset": int(offset or 0),
}
}
}
if filter_:
body["data"]["filter"] = filter_
return request("POST", "/devices", cfg, body=body)
_run(main)
@@ -0,0 +1,61 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def request(method, path, cfg, body=None):
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"api-key": str(cfg.get("api_key", "")),
"api-secret": str(cfg.get("api_secret", "")),
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
filter_ = inputs.get("filter")
limit = inputs.get("limit")
offset = inputs.get("offset")
body = {
"data": {
"page": {
"limit": int(limit or 50),
"offset": int(offset or 0),
}
}
}
if filter_:
body["data"]["filter"] = filter_
return request("POST", "/users", cfg, body=body)
_run(main)
@@ -0,0 +1,47 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def request(method, path, cfg, body=None):
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {
"api-key": str(cfg.get("api_key", "")),
"api-secret": str(cfg.get("api_secret", "")),
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
request("POST", "/devices/count", cfg, body={"data": {}})
return {"ok": True}
_run(main)