From 61bfb92dfaad7aaed26de068037ab48170aa025e Mon Sep 17 00:00:00 2001 From: TechnoSavage Date: Fri, 31 Oct 2025 14:04:03 -0400 Subject: [PATCH 1/3] initial commit for Snow License Manager inbound integration. --- snow_license_manager/README.md | 69 +++++++ snow_license_manager/config.json | 1 + snow_license_manager/snow.star | 310 +++++++++++++++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 snow_license_manager/README.md create mode 100644 snow_license_manager/config.json create mode 100644 snow_license_manager/snow.star diff --git a/snow_license_manager/README.md b/snow_license_manager/README.md new file mode 100644 index 0000000..6095d48 --- /dev/null +++ b/snow_license_manager/README.md @@ -0,0 +1,69 @@ +# Custom Integration: Snow License Manager + +## Getting Started + +- Clone this repository + +``` +git clone https://github.com/runZeroInc/runzero-custom-integrations.git +``` + +## runZero requirements + +- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero + +## Snow License Manager requirements + +**License Manager Instance URL** - The domain or IP of the Snow Atlas web server (defined within the starlark script as `SNOW_BASE_URL`) + +`Snow customer ID` - Numeric customer ID for asset retrieval. Found in `https:///api/customers/` API endpoint. (defined within the starlark script as `SNOW_CUSTOMER_ID`) + +`username` - username for API basic auth (configured in Credentials section of runZero) + +`password` - password for API basic auth (configured in Credentials section of runZero) + +## Snow License Manager API Docs + +NA + +## Steps + +### Snow License Manager configuration + +1. Determine the proper Snow License Manager URL: + - Identify the url for your Snow License Manager instance. + - Assign the URL to `SNOW_BASE_URL` within the starlark script + - Identify the Customer ID to use for asset retrieval + - Assign the Customer ID to `SNOW_CUSTOMER_ID` within the starlark script (multiple scripts can be created to import from different Customer IDs) +2. Create a valid username:password login to be used to authenticate to the API endpoints + - Copy the username; this will be used as the value for `access_key` when creating the Custom Integration credentials in the runZero console (see below) + - Copy the password; this will be used as the value for `access_secret` when creating the Custom Integration credentials in the runZero console (see below) + +### runZero configuration + +1. (OPTIONAL) - make any neccessary changes to the script to align with your environment. + - Modify API calls as needed to filter assets + - Modify datapoints uploaded to runZero as needed +2. [Create the Credential for the Custom Integration](https://console.runzero.com/credentials) + - Select the type `Custom Integration Script Secrets` + - Both `access_key` and `access_secret` are required + - `access_key` corresponds to the username for the Snow License Manager credentials + - `access_secret` corresponds to the password for the Snow License Manager credentials +3. [Create the Custom Integration](https://console.runzero.com/custom-integrations/new) + - Add a Name and Icon + - Toggle `Enable custom integration script` to input your finalized script + - Click `Validate` to ensure it has valide syntax + - Click `Save` to create the Custom Integration +4. [Create the Custom Integration task](https://console.runzero.com/ingest/custom/) + - Select the Credential and Custom Integration created in steps 2 and 3 + - Update the task schedule to recur at the desired timeframes + - Select the Explorer you'd like the Custom Integration to run from + - Click `Save` to kick off the first task + + +### What's next? + +- You will see the task kick off on the [tasks](https://console.runzero.com/tasks) page like any other integration +- The task will update the existing assets with the data pulled from the Custom Integration source +- The task will create new assets for when there are no existing assets that meet merge criteria (hostname, MAC, etc) +- You can search for assets enriched by this custom integration with the runZero search `custom_integration:` \ No newline at end of file diff --git a/snow_license_manager/config.json b/snow_license_manager/config.json new file mode 100644 index 0000000..5488d78 --- /dev/null +++ b/snow_license_manager/config.json @@ -0,0 +1 @@ +{ "name": "Snow License Manager", "type": "inbound" } \ No newline at end of file diff --git a/snow_license_manager/snow.star b/snow_license_manager/snow.star new file mode 100644 index 0000000..457c7ee --- /dev/null +++ b/snow_license_manager/snow.star @@ -0,0 +1,310 @@ +load('runzero.types', 'ImportAsset', 'NetworkInterface', 'Software') +load('base64', base64_encode='encode', base64_decode='decode') +load('flatten_json', 'flatten') +load('http', http_get='get', http_post='post', 'url_encode') +load('json', json_encode='encode', json_decode='decode') +load('net', 'ip_address') +load('uuid', 'new_uuid') + +#Change the URL to match your Snow Software License Manager server +SNOW_BASE_URL = 'https://' +SNOW_CUSTOMER_ID = '' +RUNZERO_REDIRECT = 'https://console.runzero.com/' + +def build_assets(assets, creds): + assets_import = [] + for entry in assets: + item = entry.get('Body', {}) + asset_id = str(item.get('Id', new_uuid)) + hostname = item.get('Name', '') + vendor = item.get('Manufacturer', '') + model = item.get('Model', '') + os = item.get('OperatingSystem', '') + os_version = item.get('OperatingSystemServicePack', '') + + # create the network interfaces + interfaces = [] + adapters = item.get('Hardware', {}).get('NetworkAdapters', []) + for adapter in adapters: + addresses = adapter.get('IpAddress', '').split(';') + if type(addresses) != 'list': + addresses = [addresses] + interface = build_network_interface(ips=addresses, mac=adapter.get('MacAddress', None)) + interfaces.append(interface) + + # Retrieve and map custom attributes + organization = item.get('Organization', '') + org_checksum = item.get('OrgChecksum', '') + is_virtual = item.get('IsVirtual', '') + status = item.get('Status', '') + last_scan_date = item.get('LastScanDate', '') + updated_by = item.get('UpdatedBy', '') + updated_date = item.get('UpdatedData', '') + domain = item.get('Domain', '') + total_disk_space = item.get('TotalDiskSpace', '') + physical_memory = item.get('PhyscialMemory', '') + processor_type = item.get('ProcessorType', '') + processor_count = item.get('ProcessorCount', '') + core_count = item.get('CoreCount', '') + bios_sn = item.get('BiosSerialNumber', '') + hyperv_name = item.get('HypervisorName', '') + is_portable = item.get('IsPortable', '') + is_server = item.get('IsServer', '') + most_freq_user = item.get('MostFrequentUserId', '') + most_recent_user = item.get('MostRecentUserId', '') + logical_disk = hw.get('LogicalDisks', {}).get('LogicalDisk', {}) + disk_name = logical_disk.get('Name', '') + disk_size = logical_disk.get('SizeMb', '') + disk_vol = logical_disk.get('VolumeName', '') + optical_drives = hw.get('OpticalDrives', {}).get('OpticalDrive', {}) + drive_name = optical_drives.get('Name', '') + drive_type = optical_drives.get('Type', '') + + custom_attributes = { + 'organization': organization, + 'orgChecksum': org_checksum, + 'isVirtual': is_virtual, + 'status': status, + 'lastScanDate': last_scan_date, + 'updatedBy': updated_by, + 'updatedDate': updated_date, + 'domain': domain, + 'totalDiskSpace': total_disk_space, + 'physicalMemory': physical_memory, + 'processorType': processor_type, + 'processorCount': processor_count, + 'coreCount': core_count, + 'biosSerialNumber': bios_sn, + 'hypervisorName': hyperv_name, + 'isPortable': is_portable, + 'isServer': is_server, + 'mostFrequentUserId': most_freq_user, + 'mostRecentUserId': most_recent_user, + 'logicalDisk.name': disk_name, + 'logicalDisk.sizeMb': disk_size, + 'logicalDisk.volumeName': disk_vol, + 'opticalDrive.name': drive_name, + 'opticalDrive.type': drive_type + } + + hw = item.get('Hardware', {}) + custom_attributes['hardware.biosSerialNumber'] = hw.get('BiosSerialNumber', '') + custom_attributes['hardware.biosVersion'] = hw.get('BiosVersion', '') + custom_attributes['hardware.biosDate'] = hw.get('BiosDate', '') + custom_attributes['hardware.processorType'] = hw.get('ProcessorType', '') + custom_attributes['hardware.numberOfProcessors'] = hw.get('NumberOfProcessors', '') + custom_attributes['hardware.coresPerProcessor.'] = hw.get('CoresPerProcessor', '') + custom_attributes['hardware.physicalMemoryMb'] = hw.get('PhysicalMemoryMb', '') + custom_attributes['hardware.memorySlots'] = hw.get('MemorySlots', '') + custom_attributes['hardware.memorySlotsAvailable'] = hw.get('MemorySlotsAvailable', '') + custom_attributes['hardware.systemDiskSpaceMb'] = hw.get('SystemDiskSpaceMb', '') + custom_attributes['hardware.systemDiskSpaceAvailableMb'] = hw.get('SystemDiskSpaceAvailableMb', '') + custom_attributes['hardware.totalDiskSpaceMb'] = hw.get('TotalDiskSpaceMb', '') + custom_attributes['hardware.totalDiskSpaceAvailableMb'] = hw.get('TotalDiskSpaceAvailableMb', '') + + display_adapters = hw.get('DisplayAdapters', {}).get('DisplayAdapter', []) + for adapter in display_adapters: + for k, v in adapter: + custom_attributes['displayAdapter.' + str(display_adapters.index[adapter]) + '.' + k] = v + monitors = hw.get('Monitors', {}).get('Monitor', []) + for monitor in monitors: + for k, v in monitor: + custom_attributes['monitor.' + str(monitors.index[monitor]) + '.' + k] = v + + + # Retrieve software information for asset + # create software entries + software = [] + applications = get_apps(asset_id, creds) + for app in applications: + software_entry = build_app(app) + software.append(software_entry) + + # Build assets for import + assets_import.append( + ImportAsset( + id=asset_id, + hostnames=[hostname], + manufacturer=vendor, + model=model, + os=os, + os_version=os_version, + networkInterfaces=interfaces, + customAttributes=custom_attributes, + software=software + ) + ) + return assets_import + +def build_network_interface(ips, mac): + ip4s = [] + ip6s = [] + for ip in ips[:99]: + ip_addr = ip_address(ip) + if ip_addr.version == 4: + ip4s.append(ip_addr) + elif ip_addr.version == 6: + ip6s.append(ip_addr) + else: + continue + if not mac: + return NetworkInterface(ipv4Addresses=ip4s, ipv6Addresses=ip6s) + else: + return NetworkInterface(macAddress=mac, ipv4Addresses=ip4s, ipv6Addresses=ip6s) + +def build_app(software_entry): + app = software_entry.get('Body', {}) + app_id = app.get('Id', None) + # if app_id: + # software_details = get_app_details(app_id) + #installed = app.get('InstallDate', '') + product = app.get('FamilyName', '') + vendor = app.get('ManufacturerName', '') + + # Map custom attributes from software + name = app.get('Name', '') + manufacturer_id = app.get('ManufacturerId', '') + manufacturer_name = app.get('ManufacturerName', '') + family_id = app.get('FamilyId', '') + family_name = app.get('FamilyName', '') + bundled_app_id = app.get('BundleApplicationId', '') + bundled_app_name = app.get('BundleApplicationName', '') + last_used = app.get('LastUsed', '') + first_used = app.get('FirstUsed', '') + install_date = app.get('InstallDate', '') + discvovered_date = app.get('DiscoveredDate', '') + run = app.get('Run', '') + avg_usage_time = app.get('AvgUsageTime', '') + users = app.get('Users', '') + license_reqd = app.get('LicenseRequired', '') + is_installed = app.get('IsInstalled', '') + is_blacklisted = app.get('IsBlacklisted', '') + is_whitelisted = app.get('IsWhitelisted', '') + is_virtual = app.get('IsVirtual', '') + is_oem = app.get('IsOEM', '') + is_msdn = app.get('IsMSDN', '') + is_webapp = app.get('IsWebApplication', '') + app_cost = app.get('ApplicationItemCost', '') + custom_attributes = { + 'name': name, + 'manufacturer.id': manufacturer_id, + 'manufacturer.name': manufacturer_name, + 'family.id': family_id, + 'family.name': family_name, + 'bundled.application.id': bundled_app_id, + 'bundled.application.name': bundled_app_name, + 'last.used': last_used, + 'first.used': first_used, + 'install.date': install_date, + 'discovered.date': discvovered_date, + 'run': run, + 'average.usage.time': avg_usage_time, + 'users': users, + 'license.required': license_reqd, + 'is.installed': is_installed, + 'is.blacklisted': is_blacklisted, + 'is.whitelisted': is_whitelisted, + 'is.virtual': is_virtual, + 'is.oem': is_oem, + 'is.msdn': is_msdn, + 'is.web.application': is_webapp, + 'application.item.cost': app_cost + } + + return Software( + id=app_id, + product=product, + vendor=vendor, + customAttributes=custom_attributes + ) + +def get_computers(creds): + items_returned = 0 + total_items = 10000 + assets_all = [] + + while True: + url = SNOW_BASE_URL + '/api/customers/' + SNOW_CUSTOMER_ID + '/computers?' + headers = {'Accept': 'application/json', + 'Authorization': 'Basic ' + creds} + params = {'$inlinecount': 'allpages', + '$skip': str(items_returned)} + response = http_get(url, headers=headers, params=params) + if response.status_code != 200: + print('failed to retrieve assets at $skip=' + str(items_returned), 'status code: ' + str(response.status_code)) + else: + data = json_decode(response.body) + meta = data['Meta'] + has_page_size = False + for item in meta: + if item['Name'] == 'Count': + total_items = item.get('Value') + if item['Name'] == 'PageSize': + has_page_size = True + items_returned += item.get('Value') + computers = data['Body'] + assets_all.extend(computers) + if not has_page_size: # The last page lacks the page size meta value + break + print(str(items_returned) + ' computers of ' + str(total_items) + ' returned from API') + + return assets_all + +def get_apps(asset_id, creds): + items_returned = 0 + total_items = 10000 + applications_all = [] + + while True: + url = SNOW_BASE_URL + '/api/customers/' + SNOW_CUSTOMER_ID + '/computers/' + str(asset_id) + '/applications?' + headers = {'Accept': 'application/json', + 'Authorization': 'Basic ' + creds} + params = {'$inlinecount': 'allpages', + '$skip': str(items_returned)} + response = http_get(url, headers=headers, params=params) + if response.status_code != 200: + print('failed to retrieve application for ' + str(asset_id) + ' at $skip=' + str(items_returned), 'status code: ' + str(response.status_code)) + else: + data = json_decode(response.body) + meta = data['Meta'] + has_page_size = False + for item in meta: + if item['Name'] == 'Count': + total_items = item.get('Value') + if item['Name'] == 'PageSize': + has_page_size = True + items_returned += item.get('Value') + applications = data['Body'] + applications_all.extend(applications) + if not has_page_size: # The last page lacks the page size meta value + break + print(str(items_returned) + ' applications of ' + str(total_items) + ' returned from API') + + return applications_all + +def get_app_details(app_id, creds): + url = SNOW_BASE_URL + '/api/customers/' + SNOW_CUSTOMER_ID + '/applications/' + str(app_id) + headers = {'Accept': 'application/json', + 'Authorization': 'Basic ' + creds} + response = http_get(url, headers=headers) + if response.status_code != 200: + print('failed to retrieve application details for ' + str(app_id), 'status code: ' + str(response.status_code)) + else: + data = json_decode(response.body) + details = data['Body'] + + return details + +def main(*args, **kwargs): + username = kwargs['access_key'] + password = kwargs['access_secret'] + b64_creds = base64_encode(username + ":" + password) + assets = get_computers(b64_creds) + + # Format asset list for import into runZero + import_assets = build_assets(assets, b64_creds) + if not import_assets: + print('no assets') + return None + + return import_assets \ No newline at end of file From 8868b6f9c930fa62bca0788d8c12c6c8d9ca4453 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 31 Oct 2025 18:05:56 +0000 Subject: [PATCH 2/3] Auto: update integrations JSON and README --- README.md | 1 + docs/integrations.json | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c8862a..fe3253b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ If you need help setting up a custom integration, you can create an [issue](http - [runZero Task Sync](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/task-sync/) - [Scale Computing](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/scale-computing/) - [Snipe-IT](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snipe-it/) +- [Snow License Manager](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snow_license_manager/) - [Stairwell](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/) - [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/) ## Export from runZero diff --git a/docs/integrations.json b/docs/integrations.json index 190bb59..d883433 100644 --- a/docs/integrations.json +++ b/docs/integrations.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2025-10-29T19:27:39.971854Z", - "totalIntegrations": 25, + "lastUpdated": "2025-10-31T18:05:56.263755Z", + "totalIntegrations": 26, "integrationDetails": [ { "name": "Lima Charlie", @@ -151,6 +151,12 @@ "type": "inbound", "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/proxmox/README.md", "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/proxmox/custom-integration-proxmox.star" + }, + { + "name": "Snow License Manager", + "type": "inbound", + "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snow_license_manager/README.md", + "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snow_license_manager/snow.star" } ] } \ No newline at end of file From 1176367b968680d88030a8000393546ee4cf4e50 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 16:33:34 +0000 Subject: [PATCH 3/3] Auto: update integrations JSON and README --- README.md | 1 + docs/integrations.json | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe3253b..0682637 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If you need help setting up a custom integration, you can create an [issue](http - [Snow License Manager](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snow_license_manager/) - [Stairwell](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/) - [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/) +- [Ubiquiti Unifi Network](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ubiquiti-unifi-network/) ## Export from runZero - [Audit Log to Webhook](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/audit-events-to-webhook/) - [runZero Vunerability Workflow](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/) diff --git a/docs/integrations.json b/docs/integrations.json index d883433..003d760 100644 --- a/docs/integrations.json +++ b/docs/integrations.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2025-10-31T18:05:56.263755Z", - "totalIntegrations": 26, + "lastUpdated": "2025-11-03T16:33:29.642322Z", + "totalIntegrations": 27, "integrationDetails": [ { "name": "Lima Charlie", @@ -56,6 +56,12 @@ "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/cortex-xdr/README.md", "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/cortex-xdr/custom-integration-cortex-xdr.star" }, + { + "name": "Ubiquiti Unifi Network", + "type": "inbound", + "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ubiquiti-unifi-network/README.md", + "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ubiquiti-unifi-network/custom-integration-ubiquiti-unifi-network.star" + }, { "name": "Automox", "type": "inbound",