Syncro / Hudu - Improved Asset Dash Integration

Hudu's native integration for Syncro is great for creating & updating assets in Hudu for each Syncro RMM device.

One thing it doesn't do, is show data from Asset Custom Fields in Syncro.
These custom fields are great to track extra data against devices, like asset numbers (asset tags) and installed printers.

Below is a PowerShell script designed to run on devices, through Syncro's background scheduled scripting module, that creates a visually pleasing dashboard of custom field data on assets in Hudu.


The script has been built to work with data we specifically collect and store in custom fields (using a standardised data structure), so may need some tweaking to work in other's environments.

Please note we do not have a professional in-house developer, so there may be a more efficient way to design this automation, and our comments / notes can definitely be improved upon!

To get this to work you need:

  1. Customer names to exactly match in Syncro and Hudu.

  2. Custom fields on devices within Syncro.

  3. A rich-text field called 'API Info' on your Asset Layout for Syncro device in Hudu.

  4. Standardised data structure used in the custom fields (for example, to store printer information, you might store the text like this -

    1. --
      Printer Name: Office Printer
      Printer Port: 192.168.1.10
      Printer Driver: OfficePrinterDriver
      --
      Printer Name: Home Printer
      Printer Port: 192.168.1.11
      Printer Device: HomePrinterDriver

    By storing the text in a standardised structure, we can then easily parse through the field in PowerShell to extra the relevant data for the HTML layout.

  5. Syncro platform variables assigned in the script, pointing to -

    1. SyncroAssetName = asset_name

    2. CustomerName = customer_business_name

    3. SyncroAssetID = asset_id

    4. All custom fields (i.e. FieldAssetNumber = asset_custom_field_asset_number)

  6. Syncro password variable assigned in the script called HuduKey with the password value as your Hudu API key. This prevents the API key being stored on the device at-rest.

  7. To update the following script sections -

    1. Read through the script and replace any use of YOUR_SUBDOMAIN with your Hudu and / or Syncro subdomain(s).

    2. Read through the script and replace the existing Asset Layout ID with the Asset Layout ID for your Syncro devices in Hudu.

Security Considerations

Please remember this script runs directly on devices and therefore is a potential vector for exploitation.

It is important you do not store your Hudu API key in the script itself, and instead use a password environment variable that will be passed to the script at runtime.

It is recommended to restrict the API key to not have password access, or destructive action rights.

A further security improvement would be to create an API key for each Hudu customer, restricted to their Hudu company, and store it as a custom field for the Syncro customer (customer custom field, not asset custom field). You can then pass the customer_custom_field_HuduAPIKey platform value as the password in the script. This would allow you to utilise one script, but separate credentials, for every customer.

# --- Declare Asset Fields ---
$AssetName = "$($SyncroAssetName)" 
$SyncroCustomField1 = "$($FieldAssetNumber)"
$SyncroCustomField2 = "$($FieldOwnerUser)"
$SyncroCustomField3 = "$($FieldLocalAdmins)"
$SyncroCustomField4 = "$($FieldLocalUsers)"
$SyncroCustomField5 = "$($FieldProfileSizes)"
$SyncroCustomField6 = "$($FieldWindows11)"
$SyncroCustomField7 = "$($FieldPrinters)"
$SyncroCustomField8 = "$($FieldBattery)"
$SyncroCustomField9 = "$($FieldPurchaseNotes)"
$SyncroCustomField10 = "$($FieldDomainUsers)"
$SyncroCustomField11 = "$($FieldMonitors)"
$SyncroCustomField12 = "$($FieldBitlockerKey)"
$SyncroCustomField13 = "$($FieldMappedDrives)"
$CompanyName = "$($CustomerName)"
$AssetID = "$($SyncroAssetID)"

# --- Hudu API Settings ---
$HuduApiKey = "$($HuduKey)"
$HuduBaseUrl = "https://YOUR_SUBDOMAIN.huducloud.com"

$headers = @{
    "x-api-key"    = $HuduApiKey
    "Content-Type" = "application/json"
}

# --- Helper Function to Get Hudu Company ID ---
function Get-HuduCompanyId {
    param ($CompanyName)
    $encodedName = [uri]::EscapeDataString($CompanyName)
    $url = "$HuduBaseUrl/api/v1/companies?name=$encodedName"

    try {
        $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
        $company = $response.companies | Where-Object { $_.name -eq $CompanyName }
        if ($company) {
            return $company.id
        } else {
            Write-Error "❌ Company '$CompanyName' not found in Hudu."
            exit 1
        }
    } catch {
        Write-Error "❌ Error querying company ID: $_"
        exit 1
    }
}

# --- Get Company ID ---
$CompanyId = Get-HuduCompanyId -CompanyName $CompanyName
Write-Host "✅ Retrieved Company ID: $CompanyId"

# --- Step 1: Try to fetch the asset ---
$encodedName = [uri]::EscapeDataString($AssetName)
$assetUrl = "$HuduBaseUrl/api/v1/assets?name=$encodedName"

try {
    $response = Invoke-RestMethod -Uri $assetUrl -Headers $headers -Method Get
    $asset = $response.assets | Where-Object { $_.name -eq $AssetName -and $_.company_id -eq $CompanyId }
} catch {
    Write-Error "❌ Failed to query Hudu asset: $_"
    exit 1
}

# --- Step 2: Format the HTML ---
function Format-Empty {
    param ($value)
    if ([string]::IsNullOrWhiteSpace($value)) {
        return "<span style='color:#888; font-style:italic;'>(empty)</span>"
    } else {
        return $value
    }
}

function Format-BatteryBar {
    param ($batteryText)

    if ($batteryText -match '(\d+)\s*%') {
        $percentage = [int]$matches[1]
        $color = if ($percentage -ge 50) {
            "#4CAF50"  # Green
        } elseif ($percentage -ge 30) {
            "#FFC107"  # Yellow
        } else {
            "#F44336"  # Red
        }

        return @"
<div style='width:100%; background-color:#eee; border-radius:5px; overflow:hidden; height:20px;'>
  <div style='width:${percentage}%; background-color:$color; height:100%; text-align:center; color:#fff; font-weight:bold;'>
    $percentage%
  </div>
</div>
"@
    } else {
        return "<span style='color:#888; font-style:italic;'>(no battery data)</span>"
    }
}

function Format-SectionTable {
    param (
        [string]$rawText,
        [string]$sectionTitle
    )

    if ([string]::IsNullOrWhiteSpace($rawText)) {
        return "<p><strong>${sectionTitle}:</strong> <span style='color:#888; font-style:italic;'>(empty)</span></p>"
    }

    $entries = $rawText -split '[-]{2,}'
    $tableHtml = "<h4 style='margin-bottom:8px; font-size:16px; color:#003366;'>$sectionTitle</h4>"

    foreach ($entry in $entries) {
        $lines = $entry -split "`r?`n"
        $rows = foreach ($line in $lines) {
            if ($line -match '^\s*(.+?):\s*(.+?)\s*$') {
                "<tr>
                    <td style='padding:6px 12px; border:1px solid #ddd; background-color:#003366; color:white; font-weight:bold; width:35%; vertical-align:top;'>$($matches[1])</td>
                    <td style='padding:6px 12px; border:1px solid #ddd; background-color:#fff; vertical-align:top;'>$($matches[2])</td>
                </tr>"
            }
        }

        if ($rows.Count -gt 0) {
            $tableHtml += "<table style='width:100%; border-collapse:collapse; margin-bottom:15px; font-size:14px;'>"
            $tableHtml += ($rows -join "")
            $tableHtml += "</table>"
        }
    }

    return $tableHtml
}

function Format-MappedDrivesTable {
    param (
        [string]$rawText
    )

    if ([string]::IsNullOrWhiteSpace($rawText)) {
        return "<p><strong>Mapped Drives:</strong> <span style='color:#888; font-style:italic;'>(empty)</span></p>"
    }

    # Extract the first line as-is (the header)
    $lines = $rawText -split "`r?`n"
    $headerLine = $lines[0].Trim()

    # Remove the first line from the rest of the content
    $rawText = ($lines | Select-Object -Skip 1) -join "`n"

    # Split entries by dashed lines
    $entries = $rawText -split '[-]{2,}'

    # Start building the HTML
    $tableHtml = "<p style='font-size:14px; margin-bottom:8px;'>$headerLine</p>"
    $tableHtml += "<h4 style='margin-bottom:8px; font-size:16px; color:#003366;'>Mapped Drives</h4>"
    $tableHtml += "<table style='width:100%; border-collapse:collapse; font-size:14px;'>"
    $tableHtml += "<thead><tr style='background-color:#003366; color:white;'><th style='padding:8px; text-align:left;'>Drive Letter</th><th style='padding:8px; text-align:left;'>Network Path</th></tr></thead>"
    $tableHtml += "<tbody>"

    foreach ($entry in $entries) {
        $driveLetter = ""
        $networkPath = ""

        $lines = $entry -split "`r?`n"
        foreach ($line in $lines) {
            if ($line -match 'Drive Letter\s*->\s*(.+)') {
                $driveLetter = $matches[1].Trim()
            } elseif ($line -match 'Network Path:\s*(.+)') {
                $networkPath = $matches[1].Trim()
            }
        }

        if ($driveLetter -and $networkPath) {
            $tableHtml += "<tr><td style='padding:6px; border:1px solid #ddd;'>$driveLetter</td><td style='padding:6px; border:1px solid #ddd;'>$networkPath</td></tr>"
        }
    }

    $tableHtml += "</tbody></table>"

    return $tableHtml
}

$html = @"
<div style='font-family:Segoe UI, sans-serif; color:#333;'>

  <!-- Action Buttons -->
  <h2 style='color:#003366; border-bottom:2px solid #003366; padding-bottom:5px;'>Syncro Actions</h2>
  <div style='margin-bottom: 20px;'>
    <a href='https://YOUR_SUBDOMAIN.syncromsp.com/customer_assets/$AssetID' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>View Asset</button></a>
    <a href='https://syncro-live.syncromsp.com/tasks?asset_id=$AssetID' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>Background Tools</button></a>
    <a href='https://YOUR_SUBDOMAIN.syncromsp.com/customer_assets/$AssetID#systeminfotab' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>System Info</button></a>
    <a href='https://YOUR_SUBDOMAIN.syncromsp.com/customer_assets/$AssetID#procedurestab' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>Scripts</button></a>
    <a href='https://YOUR_SUBDOMAIN.syncromsp.com/customer_assets/$AssetID#installedappstab' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>Installed Apps</button></a>
    <a href='https://YOUR_SUBDOMAIN.syncromsp.com/customer_assets/$AssetID#patchestab' target='_blank'><button style='background-color:#003366; color:white; border:none; padding:10px 20px; border-radius:5px;'>Patches</button></a>
  </div>

  <!-- Info Sections -->
  <h2 style='color:#003366; border-bottom:2px solid #003366; padding-bottom:5px;'>Asset Overview</h2>
  <div style='margin-bottom: 20px;'>
      <!-- Device Basics -->
      <div style='background:#f9f9f9; border:1px solid #ccc; padding:15px; margin-bottom:20px; border-radius:8px;'>
        <h3 style='color:#003366;'>Device Info</h3>
        <p><strong>Asset Number:</strong> $SyncroCustomField1</p>
        <p><strong>Owner User:</strong> $SyncroCustomField2</p>
        <p><strong>Windows 11 Compatible?</strong> $SyncroCustomField6</p>
        <p><strong>Bitlocker Key:</strong> $(Format-Empty $SyncroCustomField12)</p>
      </div>
    
      <!-- Setup / Purchase notes -->
      <div style='background:#f9f9f9; border:1px solid #ccc; padding:15px; margin-bottom:20px; border-radius:8px;'>
        <h3 style='color:#003366;'>Setup & Purchase Info</h3>
        <p><strong>Setup / Notes: </strong> $(Format-Empty $SyncroCustomField9)</p>
      </div>
      
      <!-- Battery Info -->
      <div style='background:#f9f9f9; border:1px solid #ccc; padding:15px; margin-bottom:20px; border-radius:8px;'>
        <h3 style='color:#003366;'>Battery Capacity</h3>
        <p><em>Device battery capacity (full charge capacity in KWh) as a percentage of the out of the box maximum.</em></p>
        <p>$(Format-BatteryBar $SyncroCustomField8)</p>
      </div>
    
      <!-- User Accounts -->
      <div style='background:#f9f9f9; border:1px solid #ccc; padding:15px; margin-bottom:20px; border-radius:8px;'>
        <h3 style='color:#003366;'>Accounts and Access</h3>
        <p><strong>Local Admin Accounts:</strong> $SyncroCustomField3</p>
        <p><strong>Local User Accounts:</strong> $SyncroCustomField4</p>
        <p><strong>Domain User Accounts:</strong> $(Format-Empty $SyncroCustomField10)</p>
        <p><strong>User Profile Sizes:</strong> $(Format-Empty $SyncroCustomField5)</p>
      </div>
    
      <!-- System Configuration -->
      <div style='background:#f9f9f9; border:1px solid #ccc; padding:15px; margin-bottom:20px; border-radius:8px;'>
        <h3 style='color:#003366;'>System Configuration</h3>
        <details style='margin-bottom: 15px;'>
            <summary style='font-size:16px; font-weight:bold; color:#003366; cursor:pointer;'>Installed Printers</summary>
            $(Format-SectionTable -rawText $SyncroCustomField7 -sectionTitle "")
        </details>
        <details style='margin-bottom: 15px;'>
            <summary style='font-size:16px; font-weight:bold; color:#003366; cursor:pointer;'>Connected Monitors</summary>
            $(Format-SectionTable -rawText $SyncroCustomField11 -sectionTitle "")
        </details>
        <details style='margin-bottom: 15px;'>
            <summary style='font-size:16px; font-weight:bold; color:#003366; cursor:pointer;'>Mapped Drives</summary>
            $(Format-MappedDrivesTable -rawText $SyncroCustomField13)
        </details>
      </div>
    </div>
</div>
"@

# --- Step 3: Send HTTPS Request ---
if ($asset) {
    Write-Host "✅ Asset '$AssetName' found. Updating..."

    # Prepare request body for update (correct custom_fields format)
    $updateBody = @{
        asset = @{
            name            = $asset.name
            asset_layout_id = $asset.asset_layout_id
            company_id      = $asset.company_id
            custom_fields   = @(
                @{
                    "API Info" = $html
                }
            )
        }
    } | ConvertTo-Json -Depth 5

    $updateUrl = "$HuduBaseUrl/api/v1/companies/$CompanyId/assets/$($asset.id)"
    try {
        $updateResponse = Invoke-RestMethod -Uri $updateUrl -Method Put -Headers $headers -Body $updateBody
        Write-Host "✅ Asset '$AssetName' successfully updated."

        # Print full response
        Write-Host "🔍 Full JSON response from Hudu API:"
        $updateResponse | ConvertTo-Json -Depth 10 | Write-Host
    } catch {
        Write-Error "❌ Failed to update asset '$AssetName': $_"
    }

} else {
    Write-Host "ℹ️ Asset '$AssetName' not found. Creating new asset..."

    # If asset doesn't exist, create it
    $createBody = @{
        asset = @{
            name            = $AssetName
            asset_layout_id = 5  # Replace with correct layout ID if needed
            company_id      = $CompanyId
            custom_fields   = @(
                @{
                    "API Info" = $html
                }
            )
        }
    } | ConvertTo-Json -Depth 5

    $createUrl = "$HuduBaseUrl/api/v1/assets"
    try {
        $createResponse = Invoke-RestMethod -Uri $createUrl -Method Post -Headers $headers -Body $createBody
        Write-Host "✅ Asset '$AssetName' successfully created."

        # Print full response
        Write-Host "🔍 Full JSON response from Hudu API:"
        $createResponse | ConvertTo-Json -Depth 10 | Write-Host
    } catch {
        Write-Error "❌ Failed to create asset '$AssetName': $_"
    }
}
3
1 reply