Last active
March 24, 2025 05:08
-
-
Save fred/ddf413ec58690f04db9c9b69769996cc to your computer and use it in GitHub Desktop.
Kandji Export Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'json' | |
require 'net/http' | |
require 'uri' | |
require 'csv' | |
require 'logger' | |
class KandjiDeviceReport | |
BATCH_SIZE = 300 | |
def initialize(api_token, sub_domain) | |
@api_token = api_token | |
@sub_domain = sub_domain | |
@base_url = "https://#{@sub_domain}.api.kandji.io/api/v1" | |
@logger = Logger.new($stdout) | |
@logger.level = Logger::INFO | |
end | |
def generate_report(output_file = 'device_report.csv') | |
@logger.info("Starting device report generation...") | |
CSV.open(output_file, 'w') do |csv| | |
csv << [ | |
'Device ID', | |
'Serial Number', | |
'Device Name', | |
'Battery Health', | |
'State of Charge', | |
'Total Disk Space (GB)', | |
'Free Disk Space (GB)', | |
'Used Disk Space (%)', | |
'ICloudDriveDesktop', | |
'ICloudDriveDocuments', | |
'ICloudDriveEnabled', | |
'ICloudDriveFirstSyncDownComplete', | |
'ICloudLoggedIn', | |
'Default Browser', | |
'Locale', | |
'Region' | |
] | |
offset = 0 | |
loop do | |
devices = fetch_devices(offset) | |
puts "devices_count: #{devices.size}" | |
break if devices.empty? | |
process_devices(devices, csv) | |
offset += BATCH_SIZE | |
@logger.info("Processed #{offset} devices...") | |
end | |
end | |
@logger.info("Report generation completed: #{output_file}") | |
rescue StandardError => e | |
@logger.error("Error generating report: #{e.message}") | |
@logger.error(e.backtrace.join("\n")) | |
raise | |
end | |
private | |
def fetch_devices(offset) | |
url = "#{@base_url}/devices" | |
url += "?offset=#{offset}" if offset > 0 | |
make_request(url) | |
end | |
def fetch_device_status(device_id) | |
make_request("#{@base_url}/devices/#{device_id}/status") | |
end | |
def fetch_device_details(device_id) | |
make_request("#{@base_url}/devices/#{device_id}/details") | |
end | |
def make_request(url) | |
uri = URI(url) | |
request = Net::HTTP::Get.new(uri) | |
request['Authorization'] = "Bearer #{@api_token}" | |
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| | |
http.request(request) | |
end | |
handle_response(response) | |
rescue StandardError => e | |
@logger.error("API request failed for #{url}: #{e.message}") | |
raise | |
end | |
def handle_response(response) | |
case response | |
when Net::HTTPSuccess | |
JSON.parse(response.body) | |
else | |
@logger.error("API request failed with status #{response.code}: #{response.body}") | |
raise "API request failed: #{response.message}" | |
end | |
end | |
def process_devices(devices, csv) | |
devices.each do |device| | |
device_id = device['device_id'] | |
@logger.info("processing: #{device_id}") | |
# Fetch both status and details from Library Items | |
status = fetch_device_status(device_id) | |
details = fetch_device_details(device_id) | |
battery_health = extract_battery_health(status) | |
state_of_charge = extract_state_of_charge(status) | |
disk_info = extract_disk_info(details) | |
icloud_info = extract_icloud_info(status) | |
default_browser = extract_default_browser_info(status) | |
default_locale = extract_locale_info(status) | |
csv_row = [ | |
device_id, | |
device['serial_number'], | |
device['device_name'], | |
battery_health, | |
state_of_charge, | |
disk_info[:total_gb], | |
disk_info[:free_gb], | |
disk_info[:used_percentage], | |
icloud_info[:desktop].to_s, | |
icloud_info[:documents].to_s, | |
icloud_info[:drive_enabled].to_s, | |
icloud_info[:drive_sync_done].to_s, | |
icloud_info[:icloud_logged_in].to_s, | |
default_browser.to_s, | |
default_locale[:locale], | |
default_locale[:region] | |
] | |
csv << csv_row | |
@logger.debug("#{status}") | |
@logger.info("Processed device: #{csv_row}") | |
end | |
end | |
def extract_battery_health(status) | |
if match = status['library_items'].to_s.match(/Maximum Capacity: ([0-9]{2,3})\%/) | |
match[1] | |
end | |
end | |
def extract_state_of_charge(status) | |
if match = status['library_items'].to_s.match(/State of Charge \(\%\)\: ([0-9]{2,3})/) | |
match[1] | |
end | |
end | |
def extract_default_browser_info(status) | |
if match = status['library_items'].to_s.match(/Default Browser\: ([a-zA-Z.]+)/) | |
match[1] | |
end | |
end | |
def extract_locale_info(status) | |
locale_match = status['library_items'].to_s.match(/AppleLocale:\s*([a-z_@=]+)/i) | |
region_match = status['library_items'].to_s.match(/Country\/Region:\s*([a-z]{2})/i) | |
locale = locale_match ? locale_match[1] : 'unknown' | |
region = region_match ? region_match[1] : 'Unknown' | |
return { locale: locale, region: region } | |
end | |
def extract_icloud_info(status) | |
icloud_info = { | |
desktop: nil, | |
documents: nil, | |
drive_enabled: nil, | |
drive_sync_done: nil, | |
icloud_logged_in: nil | |
} | |
if s = status.to_s.match(/FXICloudDriveDesktop \= ([0-1]{1})/) | |
icloud_info[:desktop] = s[1] | |
end | |
if s = status.to_s.match(/FXICloudDriveDocuments \= ([0-1]{1})/) | |
icloud_info[:documents] = s[1] | |
end | |
if s = status.to_s.match(/FXICloudDriveEnabled \= ([0-1]{1})/) | |
icloud_info[:drive_enabled] = s[1] | |
end | |
if s = status.to_s.match(/FXICloudDriveFirstSyncDownComplete \= ([0-1]{1})/) | |
icloud_info[:drive_sync_done] = s[1] | |
end | |
if s = status.to_s.match(/FXICloudLoggedIn \= ([0-1]{1})/) | |
icloud_info[:icloud_logged_in] = s[1] | |
end | |
icloud_info | |
end | |
def find_right_disc(volumes) | |
volumes.find { |volume| volume['encrypted'] == 'Yes' && volume['capacity'].to_f > 100} || volumes.first | |
end | |
def extract_disk_info(details) | |
disk_info = { | |
total_gb: nil, | |
free_gb: nil, | |
used_percentage: nil | |
} | |
volumes = details["volumes"] | |
if !volumes.empty? && (disc = find_right_disc(volumes)) && !disc.nil? | |
disk_info[:total_gb] = disc['capacity'].to_s | |
disk_info[:free_gb] = disc['available'].to_s | |
disk_info[:used_percentage] = disc['percent_used'].to_s | |
end | |
disk_info | |
end | |
end | |
# Usage example: | |
begin | |
api_token = ENV['KANDJI_API_TOKEN'] | |
sub_domain = ENV['KANDJI_SUBDOMAIN'] | |
if api_token.nil? || sub_domain.nil? | |
raise "Please set both KANDJI_API_TOKEN and KANDJI_SUBDOMAIN environment variables" | |
end | |
reporter = KandjiDeviceReport.new(api_token, sub_domain) | |
reporter.generate_report('device_report.csv') | |
rescue StandardError => e | |
puts "Failed to generate report: #{e.message}" | |
exit 1 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment