@@ -6,160 +6,259 @@ load('net', 'ip_address')
66load ('http' , http_get = 'get' )
77load ('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
137182def 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+
154254def 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