Converting Jamf Custom Schema JSON for Workspace ONE UEM


Picking out Custom Settings XML - Demystifying the Schema

Periodically, I see app vendors providing custom JSON schema files to help build app-specific configuration profiles for MDM (specifically Jamf). Workspace ONE UEM supports app-specific configuration, but currently via Custom Settings in an XML format. While many vendors also suppply a custom mobileconfig file or Custom Settings dictionary that can be used with Workspace ONE UEM, I hope in this post to show how any Workspace ONE admin can manually convert a Custom Schema JSON file to Custom Settings XML.

Example Custom JSON Schema

First, let’s start with an example of a typical custom JSON schema. The example below comes from cmdSecurity, the company that makes cmdReporter (recently purchased by Jamf). While the actual JSON content is lengthy and scary-looking, the bits you need to actually create Custom Settings XML are easy to pick out.

Custom Schema JSON for cmdReporter

{
    "title": "cmdReporter Settings",
    "description": "Domain: com.cmdsec.cmdreporter",
    "options": {
        "remove_empty_properties": true
    },
    "properties":
    {
        "AuditLevel":
        {
            "type": "integer",
            "title": "AuditLevel",
            "description": "Log Verbosity Level. Recommended: 1 or 2",
            "enum": [1, 2, 3],
            "enum_titles": [1, 2, 3],
            "default": 1
        },
        "AuditEventLogVerboseMessages":
        {
            "type": "boolean",
            "title": "AuditEventLogVerboseMessages",
            "default": false,
            "description": "Log messages cmdReporter deems non-critical. Recommended: false"
        },
        "AuditEventExcludedUsers":
        {
            "type": "array",
            "description": "Users to exclude from audit logging. Recommended: None",
            "title": "AuditEventExcludedUsers",
            "options":
            {
                "infoText": "Key: AllowedEmailDomains"
            },
            "items":
            {
                "type": "string",
                "title": "Excluded User",
                "options":
                {
                    "inputAttributes":
                    {
                        "placeholder": "dan"
                    }
                }
            }
        },
        "AuditEventExcludedProcesses":
        {
            "type": "array",
            "title": "AuditEventExcludedProcesses",
            "description": "Applications or processes to exclude from logging. Recommended: Only other security software",
            "items":
            {
                "type": "string",
                "title": "Excluded Processes",
                "options":
                {
                    "inputAttributes":
                    {
                        "placeholder": "/Applications/Security Tool.app"
                    }
                }
            }
        },

        "LogRemoteEndpointEnabled":
        {
            "type": "boolean",
            "title": "LogRemoteEndpointEnabled",
            "description": "Master switch for sending logs to remote destinations. Recommended: true",
            "default": true
        },
        "LogRemoteEndpointURL":
        {
            "type": "string",
            "title": "LogRemoteEndpointURL",
            "description": "Full URL with port number where logs will be sent.",
            "options":
            {
                "inputAttributes":
                {
                    "placeholder": "https://company.splunk.server:443/services/collector/raw"
                }
            }
        },
        "LogRemoteEndpointType":
        {
            "type": "string",
            "title": "LogRemoteEndpointType",
            "description": "Switch which configuration is used for remote transmission. LogRemoteEndpoint(value) are additional configurations. Recommended: Splunk",
            "enum": ["Syslog", "REST", "Kafka", "Splunk", "TLS", "AWSKinesis"],
            "enum_titles": ["Syslog", "REST", "Kafka", "Splunk", "TLS", "AWSKinesis"],
            "default": "Splunk"
        },
        "LogRemoteEndpointAWSKinesis":
        {
            "type": "object",
            "title": "LogRemoteEndpointAWSKinesis",
            "description": "Send logs directly to an AWS Kinesis stream",
            "properties":
            {
                "AccessKeyID":
                {
                    "type": "string",
                    "title": "AWS Access Key ID",
                    "description": "(Required) AWS Access Key ID",
                    "options":
                    {
                        "inputAttributes":
                        {
                            "placeholder": "EJNPQUNWGIJ..."
                        }
                    }
                },
                "SecretKey":
                {
                    "type": "string",
                    "title": "AWS Secret Key",
                    "description": "(Required) AWS Secret Key",
                    "options":
                    {
                        "inputAttributes":
                        {
                            "placeholder": "vOQd2pqNMyNR3..."
                        }
                    }
                },
                "StreamName":
                {
                    "type": "string",
                    "title": "AWS Kinesis Stream Name",
                    "description": "(Required) AWS Kinesis Stream Name (NOT ARN)",
                    "options":
                    {
                        "inputAttributes":
                        {
                            "placeholder": "cmdReporter"
                        }
                    }
                },
                "Region":
                {
                    "type": "string",
                    "title": "Region",
                    "description": "(Required)",
                    "default": "us-east-1"
                }
            }
        },
        "LogRemoteEndpointKafka":
        {
            "type": "object",
            "title": "LogRemoteEndpointKafka",
            "description": "Configure certificate and topic for Apache Kafka",
            "properties":
            {
                "TopicName":
                {
                    "type": "string",
                    "title": "TopicName",
                    "description": "(Required) Kafka topic cmdReporter will publish to",
                    "default": "cmdReporter"
                },
                "TLSServerCertificate":
                {
                    "type": "array",
                    "title": "TLSServerCertificate",
                    "description": "(Required) Common names for server certificate trust chain.",
                    "items":
                    {
                        "type": "string",
                        "title": "Certificate Common Name",
                        "options":
                        {
                            "infoText": "Maps to Kafka setting ssl.ca.location",
                            "inputAttributes":
                            {
                                "placeholder": "Apple Root CA - G2"
                            }
                        }
                    }

                },
                "TLSClientCertificate":
                {
                    "type": "array",
                    "title": "TLSClientCertificate",
                    "description": "(Optional) common name of client certificate in system keychain.",
                    "options":
                    {
                        "infoText": "Maps to Kafka setting ssl.certificate.location",
                        "inputAttributes":
                        {
                            "placeholder": "server_name.company.com"
                        }
                    }
                },
                "TLSClientPrivateKey":
                {
                    "type": "string",
                    "title": "TLSClientPrivateKey",
                    "description": "(Optional) PEM formatted client private key",
                    "options":
                    {
                        "infoText": "Maps to Kafka setting ssl.key.location",
                        "inputAttributes":
                        {
                            "placeholder": "-----BEGIN CERTIFICATE-----..."
                        }
                    }
                }
            }
        },
        "LogRemoteEndpointREST":
        {
            "type": "object",
            "title": "LogRemoteEndpointREST",
            "description": "REST or Splunk HEC settings. PublicKeyHash applies to both methods.",
            "properties":
            {
                "PublicKeyHash":
                {
                    "type": "string",
                    "title": "PublicKeyHash",
                    "description": "(Required) REST or Splunk HEC API key"
                }
            }
        },
        "LogRemoteEndpointTLS":
        {
            "type": "object",
            "title": "LogRemoteEndpointTLS",
            "description": "",
            "properties":
            {
                "TLSServerCertificate":
                {
                    "type": "array",
                    "title": "TLSServerCertificate",
                    "description": "(Required) Common names for server certificate trust chain.",
                    "items":
                    {
                        "type": "string",
                        "title": "Certificate Common Name",
                        "options":
                        {
                            "inputAttributes":
                            {
                                "placeholder": "Apple Root CA - G2"
                            }
                        }
                    }

                }
            }
        },
        "UnifiedLogPredicates":
        {
            "type": "array",
            "title": "UnifiedLogPredicates",
            "description": "Search terms that will be collected from the unified log systems.",
            "items":
            {
                "type": "string",
                "title": "Predicates",
                "description": "Search terms that will be collected from the unified log systems.",
                "options":
                {
                    "inputAttributes":
                    {
                        "placeholder": "(subsystem == \"com.apple.securityd\")"
                    }
                }
            }
        },
        "LicenseEmail":
        {
            "type": "string",
            "title": "LicenseEmail",
            "description": "(Required)",
            "placeholder": "name@company.com"
        },
        "LicenseExpirationDate":
        {
            "type": "string",
            "title": "LicenseExpirationDate",
            "description": "(Required) Format: 01/20/2020",
            "placeholder": "01/20/2020"
        },
        "LicenseKey":
        {
            "type": "string",
            "title": "LicenseKey",
            "description": "(Required)",
            "placeholder": "asdfh38chdj..."
        },
        "LicenseType":
        {
            "type": "string",
            "title": "LicenseType",
            "enum": ["Trial", "Annual"],
            "enum_titles": ["Trial", "Annual"],
            "default": "Annual",
            "description": "(Required)"
        },
        "LicenseVersion":
        {
            "type": "string",
            "title": "LicenseVersion",
            "description": "(Required) Always leave at 1",
            "default": "1"
        },
        "LogFileMaxNumberBackups":
        {
            "type": "integer",
            "title": "LogFileMaxNumberBackups",
            "description": "Maximum number of archived backups to keep before deleting oldest.",
            "default": "10"
        },
        "LogFileMaxSizeMegaBytes":
        {
            "type": "integer",
            "title": "LogFileMaxSizeMegaBytes",
            "description": "Maximum log file size before rotating.",
            "default": "50"
        },
        "LogFileOwnership":
        {
            "type": "string",
            "title": "LogFileOwnership",
            "description": "User and group ownership of log files",
            "default": "root:wheel"
        },
        "LogFilePermission":
        {
            "type": "string",
            "title": "LogFilePermission",
            "description": "Octal permissions for live and archived log files.",
            "default": "644"
        },
        "SecurityBaseline":
        {
            "type": "string",
            "title": "SecurityBaseline",
            "description": "Name of the security baseline to report on",
            "enum": ["800-53_high","800-53_low","800-53_moderate","cnssi-1253","all_rules"],
            "enum_titles": ["NIST 800-53_high","NIST 800-53_low","NIST 800-53_moderate","cnssi-1253","all_rules"],
            "default": "all_rules"
        },
        "SecurityBaselineReportingInterval":
        {
            "type": "integer",
            "title": "SecurityBaselineReportingInterval",
            "description": "Number of minutes between security baseline reports.",
            "default": "720"
        },
        "ProhibitedApps":
        {
            "type": "object",
            "title": "ProhibitedApps",
            "description": "Configure applications to be blocked from user sessions.",
            "properties":
            {
                "PAExecutableNames":
                {
                    "type": "array",
                    "title": "PAExecutableNames",
                    "description": "Process names to block from launching from a user session.",
                    "items":
                    {
                        "type": "string",
                        "title": "Executable Name",
                        "options":
                        {
                            "inputAttributes":
                            {
                                "placeholder": "fdesetup"
                            }
                        }
                    }

                },
                "PASigningIdentifiers":
                {
                    "type": "array",
                    "title": "PASigningIdentifiers",
                    "description": "Process signing IDs to block from launching from a user session.",
                    "items":
                    {
                        "type": "string",
                        "title": "Executable App Signing ID",
                        "options":
                        {
                            "inputAttributes":
                            {
                                "placeholder": "com.apple.fdesetup"
                            }
                        }
                    }

                },
                "PATeamIdentifiers":
                {
                    "type": "array",
                    "title": "PATeamIdentifiers",
                    "description": "Process team IDs to block from launching from a user session",
                    "items":
                    {
                        "type": "string",
                        "title": "Executable App Signing ID",
                        "options":
                        {
                            "inputAttributes":
                            {
                                "infoText": "This is the team ID for Wireshark as an example.",
                                "placeholder": "7Z6EMTD2C6"
                            }
                        }
                    }
                }
            }
        }
    }
}

Yeah, there’s a lot to unpack there. Or is there?

Step 1 - The Custom Settings Template

First, we start with the NSUserDefaults Custom Settings template. To this, we must modify the following:

  • String Value for PayloadDisplayName (this can be whatever you want to show up in the Profiles preference pane)
  • Sting Value for PayloadType (from the “Domain: " line in the schema - this is the App’s defaults domain)
  • String Value for PayloadUUID (any UUID created by typing uuidgen in Terminal.app)
  • String Value for PayloadIdentifier (a combination of the PayloadType.UUID)
<dict>
    <key>PayloadDisplayName</key>
    <string>com.cmdsec.cmdreporter Settings</string>
    <key>PayloadEnabled</key>
    <true/>
    <key>PayloadIdentifier</key>
    <string>com.cmdsec.cmdreporter.A216A1AD-6B96-497A-B6CF-3E167318B127</string>
    <key>PayloadType</key>
    <string>com.cmdsec.cmdreporter</string>
    <key>PayloadUUID</key>
    <string>A216A1AD-6B96-497A-B6CF-3E167318B127</string>
    <key>PayloadVersion</key>
    <integer>1</integer>

Step 2 - Adding the Custom Settings Key-Value Pairs

Each Key-Value Pair in the Custom Settings XML results from a JSON Property and Type. Proceeding down the schema file, you’ll use these JSON objects to build a list of key-value pairs. A few instructions to note:

  • The "properties":{ key means you’re about to see a list of <key> names. In other words, substitute "properties":{ for the XML <dict></dict> nodes.
  • An object type with multiple properties results in a <key> where the value is a <dict> of multiple key-value pairs.
  • An array type with an options key and items types results in a <key> where the value is an <array> of <type> (e.g. one or more values).
  • A string type with enum values requires the use of one of the specific enumerated values. You cannot use an arbitrary string value.

Converting Object Types

For example, take the property LogRemoteEndpointKafka:

        "LogRemoteEndpointKafka":
        {
            "type": "object",
            "title": "LogRemoteEndpointKafka",
            "description": "Configure certificate and topic for Apache Kafka",
            "properties":
            {
                "TopicName":
                {
                    "type": "string",
                    "title": "TopicName",
                    "description": "(Required) Kafka topic cmdReporter will publish to",
                    "default": "cmdReporter"
                },
                "TLSServerCertificate":
                {
                    "type": "array",
                    "title": "TLSServerCertificate",
                    "description": "(Required) Common names for server certificate trust chain.",
                    "items":
                    {
                        "type": "string",
                        "title": "Certificate Common Name",
                        "options":
                        {
                            "infoText": "Maps to Kafka setting ssl.ca.location",
                            "inputAttributes":
                            {
                                "placeholder": "Apple Root CA - G2"
                            }
                        }
                    }

                },
                "TLSClientCertificate":
                {
                    "type": "array",
                    "title": "TLSClientCertificate",
                    "description": "(Optional) common name of client certificate in system keychain.",
                    "options":
                    {
                        "infoText": "Maps to Kafka setting ssl.certificate.location",
                        "inputAttributes":
                        {
                            "placeholder": "server_name.company.com"
                        }
                    }
                },
                "TLSClientPrivateKey":
                {
                    "type": "string",
                    "title": "TLSClientPrivateKey",
                    "description": "(Optional) PEM formatted client private key",
                    "options":
                    {
                        "infoText": "Maps to Kafka setting ssl.key.location",
                        "inputAttributes":
                        {
                            "placeholder": "-----BEGIN CERTIFICATE-----..."
                        }
                    }
                }
            }
        },

The <key> comes from the JSON name (e.g. "LogRemoteEndpointKafka":) but with leading and ending punctuation removed. As mentioned earlier, the "properties":{ key is changed out for the XML <dict></dict> nodes. Finally, we build the remaining key-value pairs within the dictionary using each JSON name (as the key), and the placeholder value in each type (optionally as arrays).

To make it easier to read, here’s the pieces that you need to be picking out:

        "LogRemoteEndpointKafka":
        {
            <snip>
            "properties":
            {
                "TopicName":
                {
                    "type": "string",
                    <snip>
                    "default": "cmdReporter"
                },
                "TLSServerCertificate":
                {
                    "type": "array",
                    <snip>
                        "type": "string",
                            <snip>
                                "placeholder": "Apple Root CA - G2"
                            }
                        }
                    }
                },
                "TLSClientCertificate":
                {
                    "type": "array",
                    <snip>
                        <snip>
                            "placeholder": "server_name.company.com"
                        }
                    }
                },
                "TLSClientPrivateKey":
                {
                    "type": "string",
                        <snip>
                            "placeholder": "-----BEGIN CERTIFICATE-----..."
                        }
                    }
                }
            }
        },

The Custom Settings XML output would look similar to:

<key>LogRemoteEndpointKafka</key>
<dict>
    <key>TopicName</key>
    <string>cmdReporter</string>
    <key>TLSServerCertificate</key>
    <array>
        <string>Apple Root CA - G2</string>
    </array>
    <key>TLSClientCertificate</key>
    <array>
        <string>server_name.company.com</string>
    </array>
    <key>TLSClientPrivateKey</key>
    <string>-----BEGIN CERTIFICATE-----...</string>
</dict>

Converting Array Types

For example, take the property AuditEventExcludedUsers:

"AuditEventExcludedUsers":
        {
            "type": "array",
            "description": "Users to exclude from audit logging. Recommended: None",
            "title": "AuditEventExcludedUsers",
            "options":
            {
                "infoText": "Key: AllowedEmailDomains"
            },
            "items":
            {
                "type": "string",
                "title": "Excluded User",
                "options":
                {
                    "inputAttributes":
                    {
                        "placeholder": "dan"
                    }
                }
            }
        },

The resulting custom settings XML would be as follows:

    <key>AuditEventExcludedUsers</key>
    <array>
        <string>dan</string>
    </array>

Putting It All Together

In the interest of time, I don’t plan to convert the whole JSON Schema, but you can see how I’ve come up with a small snippet of the appropriate Custom Settings XML:

<dict>
    <key>AuditLevel</key>
    <integer>1</integer>
    <key>AuditEventLogVerboseMessages</key>
    <false />
    <key>AuditEventExcludedUsers</key>
    <array>
        <string>dan</string>
    </array>
    <key>AuditEventExcludedProcesses</key>
    <array>
        <string>/Applications/Security Tool.app</string>
    </array>
    <key>LogRemoteEndpointEnabled</key>
    <false />
    <key>LogRemoteEndpointKafka</key>
    <dict>
        <key>TopicName</key>
        <string>cmdReporter</string>
        <key>TLSServerCertificate</key>
        <array>
            <string>Apple Root CA - G2</string>
        </array>
        <key>TLSClientCertificate</key>
        <array>
            <string>server_name.company.com</string>
        </array>
        <key>TLSClientPrivateKey</key>
        <string>-----BEGIN CERTIFICATE-----...</string>
    </dict>
    <key>AuditEventExcludedUsers</key>
    <array>
        <string>dan</string>
    </array>
</dict>

Final Thoughts

Converting the Schema by hand is tedious and potentially error-prone. Hopefully, converting these JSON Schema documents via automation will soon be made an easy (or not at all required) task.

Additional Resources

macOS  Apple  XML 

See also