Skip to content

Feature Request / Enhancement - Download icon(s) (plus additional changes) #5

@wewenttothemoon

Description

@wewenttothemoon

Hi @jorgeasaurus,

I wanted to share with you some functionalities I added to further enhance this (already) great utility and receive some feedback from you regarding some areas.

JamfBackupRestoreFunctions.ps1

Download-JamfObject function

  • Fixed subfolder creation when site exists and group types can be smart/static
  • Added $hasSites variable to determine folder structure (no site name folders for environments with no configured sites)
  • Added additional resources to $siteBasedResources array (users & usergroups)
  • Added additional resource to grouptype check (usergroups)
  • If environment has existing sites, but resources are not assigned to one, default sitename to GLOB (as it is a global configuration - can be user defined)
  • Included support for icon exporting/downloading (@jorgeasaurus this is one of the areas I'm not to happy with. There is no endpoint that lists all the possible object ID's for icons; so I created a 'brute-force' method to loop through a user defined amount of ID's and break loop after 20 HTTP 404's). The alternative is to only backup the icons that are assigned to a policy via self service, but that leaves a gap for icons that have been uploaded to jamf but are not assigned to a policy via self service. There is also an endpoint (/v1/icon/download/{id} that supports downloading the icons in original scale/resolution, but I was not able to get this working (continued to get HTTP 500). Let me know if there is a better method to scrape object IDs for the icons.
function Download-JamfObject {
    # Downloads and saves Jamf objects with their associated files
    param (
        [string]$Id,
        [string]$Resource,
        [string]$DownloadDirectory
    )

    try {
        # Validate token status before proceeding
        Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token
        
        # Get object details from Jamf
        $jamfObject = Get-JamfObject -Id $Id -Resource $Resource
        $extension = if ($Resource -eq "computer-prestages") { "json" } else { "xml" }
        $displayName = Get-SanitizedDisplayName -Id $Id -Name $jamfObject.name
							

        # Define resources that can be organized by site
        $siteBasedResources = @(
            "computergroups",
            "computers",
            "macapplications",
            "mobiledeviceapplications",
            "mobiledeviceconfigurationprofiles",
            "mobiledevicegroups",
            "osxconfigurationprofiles",
            "policies",
            "restrictedsoftware",
            "users",
            "usergroups"
        )

        $groupType = $null
        $siteName  = ""
        $subfolder = ""
        $targetDir = $DownloadDirectory  # default target directory

        if ($jamfObject.plist) {
            # Parse XML content
            [xml]$xml = $jamfObject.plist

            # Set subfolder based on group type (smart vs static)
            if ($Resource -in @("computergroups", "mobiledevicegroups", "usergroups")) {
                $groupType = if ($xml.SelectSingleNode("//is_smart").InnerText -eq 'true') { "smart" } else { "static" }
            }

            # Organize by site if applicable
            if ($hasSites -and ($siteBasedResources -contains $Resource)) {
                $siteName = switch ($Resource) {
                    "computergroups"                  { $xml.computer_group.site.name }
                    "computers"                       { $xml.computer.general.site.name }
                    "macapplications"                 { $xml.mac_application.general.site.name }
                    "mobiledeviceapplications"        { $xml.mobile_device_application.general.site.name }
                    "mobiledeviceconfigurationprofiles" { $xml.mobile_device_configuration_profile.general.site.name }
                    "mobiledevicegroups"              { $xml.mobile_device_group.site.name }
                    "osxconfigurationprofiles"        { $xml.os_x_configuration_profile.general.site.name }
                    "policies"                        { $xml.policy.general.site.name }
                    "restrictedsoftware"              { $xml.restricted_software.general.site.name }
                    "users"                           { $xml.user.sites.site.name }
                    "usergroups"                      { $xml.user_group.site.name }
                }

                #  Set site-based subfolder "GLOB" if site exists and site name is 'NONE'
                if ([string]::IsNullOrWhiteSpace($siteName) -or $siteName -eq 'NONE') {
                    $siteName = "GLOB" # Default to 'GLOB' if no site is specified for this resource (can be user defined)
                }
            }

            # Build nested subfolder path based on group type and site name for applicable resources
            if ($groupType) {
                if ($hasSites -and $siteName) {
                    # Organize by site name and group type for applicable resources
                    $subfolder = Join-Path -Path $groupType -ChildPath $siteName
                } else {
                    # Organize by group type only for applicable resources with no site names
                    $subfolder = $groupType
                }
            }
            elseif ($siteName) {
                # Organize by site name when group type not applicable and site exists
                $subfolder = $siteName
            }

            # Build final target directory path
            if ($subfolder) {
                $targetDir = Join-Path -Path $DownloadDirectory -ChildPath $subfolder
            } else {
                $targetDir = $DownloadDirectory
            }

            # Save plist file
            Ensure-DirectoryExists -DirectoryPath $targetDir
            $plistFilePath = Join-Path -Path $targetDir -ChildPath "$displayName.plist"
            $jamfObject.plist | Out-File -FilePath $plistFilePath -Encoding utf8
            Format-XML -FilePath $plistFilePath
        }

        if ($jamfObject.payload) {
            if ($Resource -eq "icon") {
                # Handle icon resource
                $iconObject = $jamfObject.payload | ConvertFrom-Json
                $downloadFileName = "$($iconObject.id)_$($iconObject.name)"
                $downloadPath = Join-Path -Path $targetDir -ChildPath $downloadFileName
                Invoke-WebRequest -Uri $iconObject.url -OutFile $downloadPath -ErrorAction Stop
            } else {
                # All other resources
                Ensure-DirectoryExists -DirectoryPath $targetDir
                $payloadFilePath = Join-Path -Path $targetDir -ChildPath "$displayName.$extension"
                $jamfObject.payload | Out-File -FilePath $payloadFilePath -Encoding utf8
                if ($extension -eq "xml") {
                    Format-XML -FilePath $payloadFilePath
                }
            }
        }

        # Save script content if it exists
        if ($jamfObject.script) {
            # Remove .sh extension if present in display name
            if ($displayName -like "*.sh") {
                $displayName = $displayName -replace '\.sh$', ''
            }
            $scriptFilePath = Join-Path -Path $DownloadDirectory -ChildPath "$displayName.sh"
            $jamfObject.script | Out-File -FilePath $scriptFilePath -Encoding utf8
        }
    } catch {
        Write-Error "Error downloading $Resource : ID $Id - $_"
    }

}

Get-JamfObject function

  • Added support for handling icon resource type
function Get-JamfObject {
    param (
        [string]$Id, # Unique identifier of the Jamf object
        [string]$Resource   # Type of resource (e.g., policies, scripts, computer-prestages)
    )

    # Determine API version - computer-prestages uses v3, icon uses v1, others use classic API
    switch ($Resource) {
    "computer-prestages" { $apiVersion = "v3" ; break }
    "icon"               { $apiVersion = "v1" ; break }
    default              { $apiVersion = "classic" }
    }

    # Build endpoint URL - computer-prestages and icon have different API formats than other resources
    $endpoint = if ($Resource -in @("computer-prestages", "icon")) { 
        "$Resource/$Id" 
    } else { 
        "$Resource/id/$Id" 
    }

    # Make API call - use XML format for classic API, JSON for v2/v3
    $response = Invoke-JamfApiCall -Endpoint $endpoint -Method "GET" -ApiVersion $apiVersion -XML:($apiVersion -eq "classic")

    # Handle icon resource and modern API responses (v2/v3) 
    if ( $Resource -eq "icon" -or $apiVersion -match "v2|v3" ) { 
        return @{
            name    = $response.displayName  # or $response.name if you prefer for icons
            payload = $response | ConvertTo-Json -Depth 5
        }
    }

    # Handle classic API responses
    else {
        $xml = [xml]$response  # Convert response to XML object
        $payload = $xml.DocumentElement.FirstChild.payloads  # Extract payloads
        $name = $xml.SelectSingleNode("//name").InnerText   # Get object name

        # Extract script content if it's a script-related resource
        $script = if ($Resource -eq "scripts") { 
            $xml.SelectSingleNode("//script_contents").InnerText 
        } elseif ($Resource -eq "computerextensionattributes") { 
            $xml.SelectSingleNode("//input_type/script").InnerText 
        } else { 
            $null 
        }

        # Return structured data including name, payload, original XML, and script content
        return @{
            name    = $name
            payload = $payload
            plist   = $response 
            script  = $script
        }
    }
}

Download-JamfObjects function

  • Created $jamfSites and $hasSites to use in Download-JamfObject function for determining target directories
function Download-JamfObjects {
    param(
        [string]$Id, # Optional: Specific object ID to download
        [string]$Resource, # Required: Type of Jamf resource (e.g., policies, scripts)
        [switch]$ClearExports       # Optional: Clear existing exports before downloading
    )

    # Verify token is valid before querying sites
    Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token

    # Get site details from Jamf
    $jamfSites = Invoke-JamfApiCall -Endpoint "sites" -Method "GET" -ApiVersion "classic" -XML:$true
    $hasSites = ([int]([xml]$jamfSites).sites.size) -ne 0 # Variable used in Download-JamfObject function to determine folder structure

    # Construct the download directory path using the resource type
    $downloadDirectory = Join-Path -Path $Config.DataFolder -ChildPath $Resource

    # If ClearExports is specified, remove existing directory and its contents
    if ($ClearExports -and (Test-Path $downloadDirectory)) { Remove-Item $downloadDirectory -Recurse -Force }
    # Create the download directory if it doesn't exist
    Ensure-DirectoryExists -DirectoryPath $downloadDirectory

    if ($Id) {
        # Download a single object if ID is provided
        Download-JamfObject -Id $Id -Resource $Resource -DownloadDirectory $downloadDirectory
    } else {
        # Download all objects of the specified resource type
        Write-Host "Exporting all [$Resource] objects" -NoNewline -ForegroundColor Cyan
        # Get all object IDs for the specified resource
        $objectIds = Get-JamfObjectIds -Resource $Resource
        try {
            foreach ($objectId in $objectIds) {
                # Verify token is valid before each download
                Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token
            
                # Download each object individually
                Download-JamfObject -Id $objectId -Resource $Resource -DownloadDirectory $downloadDirectory
            }
            Write-Host " - ✅" -ForegroundColor Green
        } catch {
            Write-Host " - ❌" -ForegroundColor Red
            Write-Error "Failed to download objects for resource '$Resource': $_"
        }

    }
}

Get-JamfObjectIds function

  • Added support for scraping icon object IDs
  • Added parameter defined in config.ps1 to define how many icon IDs end-user wants to discover (hopefully better method exists)
function Get-JamfObjectIds {
    param (
        [string]$Resource,
        [int]$IconMaxId = $Config.IconMaxId
    )

    # Invoke the Jamf API call to get the response
    $apiVersion = switch ($Resource) {
        "computer-prestages" { "v3" }
        "patch-software-title-configurations" { "v2" }
        "icon" { "v1" }
        default { "classic" }
    }
    # testing (limitation: static iconmaxid count)
    # special-case for icon, because there is no list endpoint
    # alternative is to export only icons that are tied to self service policies, but that does not export all uploaded icons
    # was unable to find a way to use the /v1/icon/download/{id} endpoint, which, as per documentation, allows to download icon in original scale/resolution (https://developer.jamf.com/jamf-pro/reference/get_v1-icon-download-id)
    # No API role required (https://developer.jamf.com/jamf-pro/docs/privileges-and-deprecations)
    if ($Resource -eq "icon") {
        $validIds = @()
        $consecutive404s = 0

        foreach ($id in 1..$IconMaxId) {
            $endpoint = "icon/$id"
            try {
                $iconResponse = Invoke-JamfApiCall -Endpoint $endpoint -Method "GET" -ApiVersion $apiVersion
                if ($iconResponse -and $iconResponse.id) {
                    $validIds += $iconResponse.id
                    $consecutive404s = 0  # reset counter if success
                }
                $percentComplete = [math]::Round( ($id / $IconMaxId) * 100 )
                Write-Progress -Activity "Getting icon IDs" -Status "Checking ID $id" -PercentComplete $percentComplete
            }
            catch {
                # 404s are expected for invalid icon IDs
                $consecutive404s++
                if ($consecutive404s -ge 20) {
                    break # Break from loop after 20 consecutive 404s
                }
            }
        }

        # Break the progress bar
        Write-Progress -Activity "Getting icon IDs" -Completed
        return $validIds
    }
    # For all other resources, proceed normally
    $response = Invoke-JamfApiCall -Endpoint $Resource -Method "GET" -ApiVersion $ApiVersion

    if (-not $response) {
        Write-Host "No response received for resource: $Resource" -ForegroundColor Red
        return $null
    }

    # Get the first NoteProperty from the response
    $firstProperty = $response | Get-Member -MemberType NoteProperty | Select-Object -First 1

    if (-not $firstProperty) {
        Write-Host "No NoteProperties found in response for resource: $Resource" -ForegroundColor Yellow
        return $null
    }

    # Extract the value of the first NoteProperty and get the IDs
    $objects = $response.$($firstProperty.Name)

    # Uncomment if you want to only export static groups
    # if ($Resource -in "computergroups", "mobiledevicegroups") {
    #     return ($objects | Where-Object { -not $_.is_smart }).id
    # }

    # For computer-prestages, the property is 'results' with a nested structure
    if ($Resource -eq "computer-prestages") {
        return $objects.id
    }

    # Default case: return the IDs directly from the first NoteProperty
    return $objects.id
}

Config.ps1

  • Added support (optional) to create subdirectory with date that script is ran
  • Added IconMaxId (used in Get-JamfObjectIDs) for icon ID scraping
[hashtable]$script:Config = @{
    BaseUrl    = "https://instance.jamfcloud.com" # URL of your Jamf Pro server
    Username   = "username" # Leave empty if using API credentials
    Password   = 'password' # Leave empty if using API credentials
    #clientId     = "00000000-0000-0000-0000-000000000000" # Fill if using API credentials
    #clientSecret = "your-client-secret-here" # Fill if using API credentials
    DataFolder = Join-Path (Get-Location) "JAMF_Backup_Production\$($(Get-Date -Format 'MM-dd-yyyy'))"
    ApiVersion = "classic" # Use 'v1','v2' or 'classic'
    IconMaxId  = 300 # Maximum IDs for icons to scan/discover, adjust as needed
    Token      = $null
}

JamfBackupRestoreSampleUsage.ps1

  • Added additional supported resource types
# Download multiple resource objects from Jamf Pro
@(
    "osxconfigurationprofiles",
    "mobiledeviceconfigurationprofiles",
    "scripts",
    "restrictedsoftware", 
    "policies", 
    "computer-prestages", 
    "computerextensionattributes",
    "computergroups",
    "mobiledevicegroups", 
    "mobiledeviceextensionattributes", 
    "departments",
    "buildings",
    "categories",
    "computers",
    "mobiledevices",
    "mobiledeviceapplications",
    "macapplications",
    "users",
    "usergroups",
    "icon",
    "sites",
    "printers",
    "dockitems"
) | ForEach-Object {
    Download-JamfObjects -resource $_ -ClearExports
}

README.md

  • Added additional supported resource types
# Download multiple resource objects from Jamf Pro
@(
    "osxconfigurationprofiles",
    "mobiledeviceconfigurationprofiles",
    "scripts",
    "restrictedsoftware", 
    "policies", 
    "computer-prestages", 
    "computerextensionattributes",
    "computergroups",
    "mobiledevicegroups", 
    "mobiledeviceextensionattributes", 
    "departments",
    "buildings",
    "categories",
    "computers",
    "mobiledevices",
    "mobiledeviceapplications",
    "macapplications",
    "users",
    "usergroups",
    "icon",
    "sites",
    "printers",
    "dockitems"
) | ForEach-Object {
    Download-JamfObjects -resource $_ -ClearExports
}

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions