Skip to content

Commit bcaa503

Browse files
authored
Merge pull request #36 from runZeroInc/update/automox
Update custom-integration-automox.star
2 parents 76d0935 + 663b546 commit bcaa503

2 files changed

Lines changed: 295 additions & 196 deletions

File tree

automox/custom-integration-automox.star

Lines changed: 205 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -6,160 +6,259 @@ load('net', 'ip_address')
66
load('http', http_get='get')
77
load('uuid', 'new_uuid')
88

9-
AUTOMOX_API_URL = "https://console.automox.com/api/servers"
9+
AUTOMOX_BASE_URL = "https://console.automox.com/api"
10+
AUTOMOX_SERVERS_URL = AUTOMOX_BASE_URL + "/servers"
11+
AUTOMOX_ORGS_URL = AUTOMOX_BASE_URL + "/orgs"
1012

11-
def get_automox_devices(headers):
12-
"""Retrieve all devices from Automox using pagination"""
13+
def looks_numeric(v):
14+
if v == None:
15+
return False
16+
s = str(v)
17+
if s == "":
18+
return False
19+
return s.isdigit()
1320

14-
query = {
15-
"limit": "500",
16-
"page": "0"
17-
}
21+
def normalize_list(decoded):
22+
if decoded == None:
23+
return []
24+
t = type(decoded)
25+
if t == "list":
26+
return decoded
27+
if t == "dict":
28+
if "data" in decoded and type(decoded["data"]) == "list":
29+
return decoded["data"]
30+
if "results" in decoded and type(decoded["results"]) == "list":
31+
return decoded["results"]
32+
if "items" in decoded and type(decoded["items"]) == "list":
33+
return decoded["items"]
34+
if "records" in decoded and type(decoded["records"]) == "list":
35+
return decoded["records"]
36+
fail("Unexpected dict response (no data/results/items/records list field).")
37+
fail("Unexpected response type: " + t)
1838

39+
def get_automox_devices(headers, org_hint):
1940
devices = []
41+
page = 0
42+
limit = 500
43+
use_o = looks_numeric(org_hint)
2044

2145
while True:
22-
response = http_get(AUTOMOX_API_URL, headers=headers, params=query)
46+
params = {"limit": str(limit), "page": str(page), "include_details": "1"}
47+
if use_o:
48+
params["o"] = str(org_hint)
2349

24-
if response.status_code != 200:
25-
print("Failed to fetch devices from Automox. Status:", response.status_code)
26-
return devices
50+
resp = http_get(AUTOMOX_SERVERS_URL, headers=headers, params=params)
2751

28-
batch = json_decode(response.body)
52+
if resp.status_code == 404 and use_o:
53+
use_o = False
54+
devices = []
55+
page = 0
56+
continue
57+
58+
if resp.status_code != 200:
59+
fail("Failed to fetch devices from Automox: " + str(resp.status_code))
2960

61+
batch = normalize_list(json_decode(resp.body))
3062
if not batch:
31-
break # Stop fetching if no more results are returned
63+
break
64+
65+
for d in batch:
66+
devices.append(d)
3267

33-
devices.extend(batch)
34-
query["page"] = str(int(query["page"]) + 1)
68+
page = page + 1
3569

36-
print("Loaded", len(devices), "devices")
3770
return devices
3871

39-
def build_assets(api_token, org_id=None):
40-
"""Convert Automox device data into runZero asset format"""
41-
headers = {
42-
"Authorization": "Bearer " + api_token,
43-
"Content-Type": "application/json"
44-
}
45-
all_devices = get_automox_devices(headers)
46-
assets = []
72+
def get_orgs(headers):
73+
orgs = []
74+
page = 0
75+
limit = 500
4776

48-
for device in all_devices:
49-
device_id = device.get("id", new_uuid())
50-
custom_attrs = {
51-
"os_version": device.get("os_version", ""),
52-
"os_name": device.get("os_name", ""),
53-
"os_family": device.get("os_family", ""),
54-
"agent_version": device.get("agent_version", ""),
55-
"compliant": str(device.get("compliant", "")),
56-
"last_logged_in_user": device.get("last_logged_in_user", ""),
57-
"serial_number": device.get("serial_number", ""),
58-
"agent_status": device.get("status", {}).get("agent_status", "")
59-
}
77+
while True:
78+
params = {"limit": str(limit), "page": str(page)}
79+
resp = http_get(AUTOMOX_ORGS_URL, headers=headers, params=params)
6080

61-
mac_address = ""
62-
if device.get("detail", {}).get("NICS"):
63-
mac_address = device["detail"]["NICS"][0].get("MAC", "")
81+
if resp.status_code != 200:
82+
fail("Failed to fetch orgs from Automox: " + str(resp.status_code))
6483

65-
# Collect IPs
66-
ips = device.get("ip_addrs", []) + device.get("ip_addrs_private", [])
84+
batch = normalize_list(json_decode(resp.body))
85+
if not batch:
86+
break
6787

68-
# Append software if org_id is passed
69-
if org_id:
70-
software_list = build_software_list(org_id, device_id, headers)
88+
for o in batch:
89+
orgs.append(o)
7190

72-
assets.append(
73-
ImportAsset(
74-
id=str(device_id),
75-
networkInterfaces=[build_network_interface(ips, mac_address)],
76-
hostnames=[device.get("name", "")],
77-
os_version=device.get("os_version", ""),
78-
os=device.get("os_name", ""),
79-
customAttributes=custom_attrs
80-
)
81-
)
82-
return assets
91+
page = page + 1
92+
93+
return orgs
94+
95+
def choose_org_id(headers, org_hint):
96+
if looks_numeric(org_hint):
97+
return str(org_hint)
98+
99+
orgs = get_orgs(headers)
100+
if not orgs:
101+
fail("No organizations returned from Automox; cannot determine org_id.")
102+
103+
oid = orgs[0].get("id", None)
104+
if oid == None:
105+
fail("Automox /orgs response missing 'id'.")
106+
return str(oid)
107+
108+
def fetch_org_packages(headers, org_id):
109+
url = AUTOMOX_BASE_URL + "/orgs/" + str(org_id) + "/packages"
110+
packages = []
111+
page = 0
112+
limit = 500
113+
114+
while True:
115+
params = {"limit": str(limit), "page": str(page), "o": str(org_id)}
116+
resp = http_get(url, headers=headers, params=params)
117+
118+
if resp.status_code != 200:
119+
fail("Failed to fetch org packages from Automox: " + str(resp.status_code))
120+
121+
batch = normalize_list(json_decode(resp.body))
122+
if not batch:
123+
break
124+
125+
for p in batch:
126+
packages.append(p)
127+
128+
page = page + 1
129+
130+
return packages
83131

84-
def build_software_list(org_id, device_id, headers):
85-
# Fetch software inventory from Automox API
86-
automox_software_url = "https://console.automox.com/api/servers/" + str(device_id) + "/packages?o=" + str(org_id)
87-
88-
software_response = http_get(automox_software_url, headers=headers)
89-
90-
if software_response.status_code != 200:
91-
fail("Failed to fetch software inventory: " + str(software_response.status_code))
92-
93-
software_inventory = json_decode(software_response.body)
94-
95-
software_list = []
96-
for soft in software_inventory:
97-
transformed_software = Software (
98-
id = str(soft.get("id", "")),
99-
installedFrom = str(soft.get("repo", "")),
100-
product = str(soft.get("display_name", "")),
101-
version = str(soft.get("version", "")),
102-
customAttributes = {
132+
def index_software_by_server(packages):
133+
by_server = {}
134+
135+
for soft in packages:
136+
sid = soft.get("server_id", None)
137+
if sid == None:
138+
continue
139+
140+
sw = Software(
141+
id=str(soft.get("id", "")),
142+
installedFrom=str(soft.get("repo", "")),
143+
product=str(soft.get("display_name", "")),
144+
version=str(soft.get("version", "")),
145+
customAttributes={
103146
"server_id": str(soft.get("server_id", "")),
104147
"package_id": str(soft.get("package_id", "")),
105148
"software_id": str(soft.get("software_id", "")),
106-
"installed": soft.get("installed", ""),
107-
"ignored": soft.get("ignored", ""),
108-
"group_ignored": soft.get("group_ignored", ""),
109-
"deferred_until": soft.get("deferred_until", ""),
110-
"group_deferred_until": soft.get("group_deferred_until", ""),
149+
"installed": str(soft.get("installed", "")),
150+
"ignored": str(soft.get("ignored", "")),
151+
"group_ignored": str(soft.get("group_ignored", "")),
152+
"deferred_until": str(soft.get("deferred_until", "")),
153+
"group_deferred_until": str(soft.get("group_deferred_until", "")),
111154
"name": str(soft.get("name", "")),
112155
"cves": str(soft.get("cves", "")),
113-
"cve_score": soft.get("cve_score", ""),
156+
"cve_score": str(soft.get("cve_score", "")),
114157
"agent_severity": str(soft.get("agent_severity", "")),
115158
"severity": str(soft.get("severity", "")),
116159
"package_version_id": str(soft.get("package_version_id", "")),
117160
"os_name": str(soft.get("os_name", "")),
118161
"os_version": str(soft.get("os_version", "")),
119162
"os_version_id": str(soft.get("os_version_id", "")),
120163
"create_time": str(soft.get("create_time", "")),
121-
"requires_reboot": soft.get("requires_reboot", ""),
164+
"requires_reboot": str(soft.get("requires_reboot", "")),
122165
"patch_classification_category_id": str(soft.get("patch_classification_category_id", "")),
123166
"patch_scope": str(soft.get("patch_scope", "")),
124-
"is_uninstallable": soft.get("is_uninstallable", ""),
167+
"is_uninstallable": str(soft.get("is_uninstallable", "")),
125168
"secondary_id": str(soft.get("secondary_id", "")),
126-
"is_managed": soft.get("is_managed", ""),
169+
"is_managed": str(soft.get("is_managed", "")),
127170
"impact": str(soft.get("impact", "")),
128-
"organization_id": str(soft.get("organization_id", ""))
129-
},
130-
)
131-
# Only append if not empty
132-
if transformed_software:
133-
software_list.append(transformed_software)
134-
135-
return software_list
171+
"organization_id": str(soft.get("organization_id", "")),
172+
},
173+
)
174+
175+
key = str(sid)
176+
if key not in by_server:
177+
by_server[key] = []
178+
by_server[key].append(sw)
179+
180+
return by_server
136181

137182
def build_network_interface(ips, mac=None):
138-
"""Convert IPs and MAC addresses into a NetworkInterface object"""
139183
ip4s = []
140184
ip6s = []
141185

142186
for ip in ips[:99]:
143-
if ip:
144-
ip_addr = ip_address(ip)
145-
if ip_addr.version == 4:
146-
ip4s.append(ip_addr)
147-
elif ip_addr.version == 6:
148-
ip6s.append(ip_addr)
149-
else:
187+
if not ip:
150188
continue
189+
addr = ip_address(ip)
190+
if addr.version == 4:
191+
ip4s.append(addr)
192+
elif addr.version == 6:
193+
ip6s.append(addr)
151194

152195
return NetworkInterface(macAddress=mac, ipv4Addresses=ip4s, ipv6Addresses=ip6s)
153196

197+
def build_network_interfaces_from_device(device):
198+
details = device.get("details", device.get("detail", {}))
199+
if type(details) == "dict":
200+
nics = details.get("NICS", None)
201+
if type(nics) == "list" and nics:
202+
out = []
203+
for nic in nics[:99]:
204+
mac = nic.get("MAC", "")
205+
ips = nic.get("IPS", [])
206+
out.append(build_network_interface(ips, mac))
207+
if out:
208+
return out
209+
210+
ips = device.get("ip_addrs", []) + device.get("ip_addrs_private", [])
211+
return [build_network_interface(ips, "")]
212+
213+
def build_assets(api_token, org_hint):
214+
headers = {"Authorization": "Bearer " + api_token, "Content-Type": "application/json"}
215+
216+
devices = get_automox_devices(headers, org_hint)
217+
org_id = choose_org_id(headers, org_hint)
218+
219+
packages = fetch_org_packages(headers, org_id)
220+
sw_by_server = index_software_by_server(packages)
221+
222+
assets = []
223+
for device in devices:
224+
device_id = device.get("id", new_uuid())
225+
226+
custom_attrs = {
227+
"os_version": device.get("os_version", ""),
228+
"os_name": device.get("os_name", ""),
229+
"os_family": device.get("os_family", ""),
230+
"agent_version": device.get("agent_version", ""),
231+
"compliant": str(device.get("compliant", "")),
232+
"last_logged_in_user": device.get("last_logged_in_user", ""),
233+
"serial_number": device.get("serial_number", ""),
234+
"agent_status": device.get("status", {}).get("agent_status", ""),
235+
}
236+
237+
assets.append(
238+
ImportAsset(
239+
id=str(device_id),
240+
networkInterfaces=build_network_interfaces_from_device(device),
241+
hostnames=[device.get("name", "")],
242+
os_version=device.get("os_version", ""),
243+
os=device.get("os_family", "") + " " + device.get("os_name", ""),
244+
software=sw_by_server.get(str(device_id), []),
245+
customAttributes=custom_attrs,
246+
trust_device_type=True,
247+
trust_os=True,
248+
trust_os_version=True
249+
)
250+
)
251+
252+
return assets
253+
154254
def main(**kwargs):
155-
"""Main function to retrieve and return Automox asset data"""
156-
org_id = kwargs.get("access_key", None)
157-
api_token = kwargs['access_secret'] # Use API token from runZero credentials
255+
org_hint = kwargs.get("access_key", None)
256+
api_token = kwargs.get("access_secret", None)
158257

159-
assets = build_assets(api_token, org_id)
160-
258+
if not api_token:
259+
fail("Missing access_secret (Automox API token).")
260+
261+
assets = build_assets(api_token, org_hint)
161262
if not assets:
162-
print("No assets retrieved from Automox")
163263
return None
164-
165264
return assets

0 commit comments

Comments
 (0)