Skip to main content

Cross-platform utility package for MDM deployment scripts (Jamf Pro, Intune).

Project description

pymdm

A Python utility package for macOS MDM deployment scripts, built for MacAdmins Python (#!/usr/local/bin/managed_python3) and Jamf Pro workflows. Windows/Intune support is also available for teams managing mixed-platform fleets.

Features

  • ParamParser: Safe parsing of Jamf Pro script parameters 4-11 (macOS)
  • Dialog: swiftDialog integration for user-facing dialogs and notifications (macOS)
  • CommandRunner: Secure subprocess execution with credential sanitization, platform-aware run-as-user, and check=False mode for raw CompletedProcess access
  • SystemInfo: System information helpers — serial number, console user, hostname
  • MdmLogger: Structured logging with file output, rotation, and multiple log levels
  • WebhookSender: Send logs and metadata to webhooks with optional custom headers
  • IntuneParamProvider: Env var and argv parameter parsing for Intune scripts (Windows)
  • DarwinDefaults: Read, write, and delete macOS defaults plist values (macOS)
  • DarwinServiceManager: Manage launchd services — is_loaded, bootout, bootstrap (macOS)
  • Win32Registry: Read, write, and delete Windows registry values via winreg (Windows)
  • Win32ServiceManager: Manage Windows services via sc.exe (Windows)

Platform Support

Feature macOS (Jamf) Windows (Intune)
ParamParser (Jamf) Yes
Dialog (swiftDialog) Yes Graceful no-op
CommandRunner Yes Yes
SystemInfo Yes Yes
MdmLogger Yes Yes
WebhookSender Yes Yes
IntuneParamProvider Yes
DarwinDefaults Yes
DarwinServiceManager Yes
Win32Registry Yes
Win32ServiceManager Yes

Installation

From Source

uv pip install -e .

Development

make install     # Install with dev dependencies
make test        # Run tests
make format      # Format code with ruff

Quick Start

Logging

from pymdm import MdmLogger

logger = MdmLogger(
    debug=True,
    output_path="/var/log/my_script.log"
)

logger.info("Script started")
logger.debug("Detailed information")
logger.warn("Warning message")
logger.error("Error occurred", exit_code=1)

Jamf Parameters (macOS)

from pymdm import ParamParser

# Get string parameter
webhook_url = ParamParser.get(4)  # $4 in Jamf policy

# Get boolean parameter
debug_mode = ParamParser.get_bool(5)  # "true", "1", "yes" -> True

# Get integer parameter
timeout = ParamParser.get_int(6, default=30)

Intune Parameters (Windows)

from pymdm.mdm import IntuneParamProvider

provider = IntuneParamProvider()

# Get from sys.argv
value = provider.get(1)

# Get from environment variable
webhook_url = provider.get("WEBHOOK_URL")

# Boolean from env var
debug = provider.get_bool("DEBUG_MODE")

Auto-Detect MDM Provider

from pymdm.mdm import get_provider

# Automatically selects Jamf on macOS, Intune on Windows
# Override with PYMDM_MDM_PROVIDER env var
provider = get_provider()

value = provider.get(4)  # Jamf: sys.argv[4], Intune: sys.argv[4]
debug = provider.get_bool("DEBUG")  # Intune: env var lookup

Command Execution

from pymdm import CommandRunner

runner = CommandRunner(logger=logger)

# Safe execution (list form) — returns str
output = runner.run(["/usr/bin/id", "-u", username])

# check=False returns subprocess.CompletedProcess
result = runner.run(["/usr/bin/some_tool", "--check"], check=False)
if result.returncode != 0:
    logger.warn(f"Tool exited {result.returncode}: {result.stderr}")

# Pass kwargs through to subprocess.run
output = runner.run(["ls", "-la"], cwd="/tmp")

# Run as logged-in user (platform-aware)
runner = CommandRunner(logger=logger, username="jsmith", uid=501)
output = runner.run_as_user(["/usr/bin/open", "-a", "Safari"])

System Information

from pymdm import SystemInfo

# Get serial number
# macOS: system_profiler | Windows: PowerShell/wmic
serial = SystemInfo.get_serial_number()

# Get console user info
user_info = SystemInfo.get_console_user()
if user_info:
    username, uid, home_path = user_info

# Get hostname
hostname = SystemInfo.get_hostname()

# Get full name
full_name = SystemInfo.get_user_full_name("jsmith")

Webhook Integration

from pymdm import WebhookSender, MdmLogger

logger = MdmLogger(output_path="/var/log/script.log")
webhook = WebhookSender(
    url="https://hooks.tray.io/...",
    logger=logger,
    headers={"Authorization": "Bearer <token>"},
)

# Send log with metadata
webhook.send(
    hostname=SystemInfo.get_hostname(),
    serial=SystemInfo.get_serial_number(),
    script_name="my_deployment_script",
    status="success"
)

macOS Defaults (plist)

from pymdm.platforms.darwin import DarwinDefaults

# Read a preference value
val = DarwinDefaults.read("com.apple.finder", "ShowHardDrivesOnDesktop")

# Write a string value
DarwinDefaults.write("com.example.app", "Setting", "value")

# Write a boolean value
DarwinDefaults.write("com.example.app", "Enabled", "true", "-bool")

# Delete a key
DarwinDefaults.delete("com.example.app", "Setting")

macOS Service Management

from pymdm.platforms.darwin import DarwinServiceManager

# Check if a launchd service is loaded
if DarwinServiceManager.is_loaded("system/com.example.daemon"):
    DarwinServiceManager.bootout("system/com.example.daemon")

# Load a service from a plist
DarwinServiceManager.bootstrap("system", "/Library/LaunchDaemons/com.example.daemon.plist")

Windows Registry

from pymdm.platforms.win32 import Win32Registry

# Read a registry value
product = Win32Registry.read(
    Win32Registry.HKLM,
    r"SOFTWARE\Microsoft\Windows NT\CurrentVersion",
    "ProductName",
)

# Write a string value (auto-detects REG_SZ)
Win32Registry.write(Win32Registry.HKLM, r"SOFTWARE\MyApp", "Setting", "value")

# Write an integer (auto-detects REG_DWORD)
Win32Registry.write(Win32Registry.HKLM, r"SOFTWARE\MyApp", "Count", 42)

# Delete a value
Win32Registry.delete(Win32Registry.HKLM, r"SOFTWARE\MyApp", "Setting")

Windows Service Management

from pymdm.platforms.win32 import Win32ServiceManager

if Win32ServiceManager.is_running("CrowdStrike Falcon"):
    Win32ServiceManager.stop("CrowdStrike Falcon")

Win32ServiceManager.start("MyService")

Complete Example (macOS / Jamf Pro)

#!/usr/local/bin/managed_python3
"""Example Jamf Pro policy script."""

from pymdm import (
    MdmLogger,
    ParamParser,
    CommandRunner,
    SystemInfo,
    WebhookSender,
)

# Setup
logger = MdmLogger(
    debug=ParamParser.get_bool(4),
    output_path="/var/log/my_script.log"
)
runner = CommandRunner(logger=logger)

logger.log_startup("my_script", version="1.0.0")

try:
    # Get system info
    serial = SystemInfo.get_serial_number()
    hostname = SystemInfo.get_hostname()

    logger.info(f"Running on {hostname} ({serial})")

    # Execute command
    output = runner.run(["/usr/bin/sw_vers", "-productVersion"])
    logger.info(f"macOS version: {output}")

    # Send results
    webhook = WebhookSender(
        url=ParamParser.get(5),
        logger=logger
    )
    webhook.send(
        hostname=hostname,
        serial=serial,
        status="success"
    )

except Exception as e:
    logger.log_exception("Script failed", e, exit_code=1)

Complete Example (Windows / Intune)

"""Example Intune deployment script."""

from pymdm import MdmLogger, CommandRunner, SystemInfo, WebhookSender
from pymdm.mdm import IntuneParamProvider

# Setup
params = IntuneParamProvider()
logger = MdmLogger(
    debug=params.get_bool("DEBUG"),
    output_path="C:\\ProgramData\\Scripts\\my_script.log"
)
runner = CommandRunner(logger=logger)

logger.log_startup("my_script", version="1.0.0")

try:
    serial = SystemInfo.get_serial_number()
    hostname = SystemInfo.get_hostname()

    logger.info(f"Running on {hostname} ({serial})")

    # Windows-specific command
    output = runner.run(["powershell", "-Command", "Get-ComputerInfo | Select-Object OsVersion"])
    logger.info(f"System info: {output}")

    webhook_url = params.get("WEBHOOK_URL")
    if webhook_url:
        webhook = WebhookSender(url=webhook_url, logger=logger)
        webhook.send(hostname=hostname, serial=serial, status="success")

except Exception as e:
    logger.log_exception("Script failed", e, exit_code=1)

Platform Configuration

Environment Variables

Variable Purpose Values
PYMDM_PLATFORM Override platform auto-detection darwin, win32
PYMDM_MDM_PROVIDER Override MDM provider auto-detection jamf, intune

Task Mapping: Jamf vs Intune

Jamf Pro Intune Equivalent pymdm API
ParamParser.get(4) IntuneParamProvider().get("PARAM_NAME") get_provider().get(...)
ParamParser.get_bool(5) IntuneParamProvider().get_bool("FLAG") get_provider().get_bool(...)
Script params via sys.argv[4-11] Env vars or sys.argv Provider-specific
jamf recon Microsoft Graph API Not in pymdm (use provider SDK)
swiftDialog Windows toast/WPF Dialog (macOS only)

Migration Notes

From pymdm < 0.4.0

All existing imports and APIs are fully backward compatible. No changes required for macOS/Jamf scripts:

# These all work exactly as before
from pymdm import SystemInfo, CommandRunner, ParamParser, MdmLogger

For new Windows/Intune scripts, use the new provider APIs:

from pymdm.mdm import IntuneParamProvider, get_provider
from pymdm.platforms import get_platform

Architecture

pymdm/
├── platforms/          # OS-specific implementations
│   ├── darwin.py       # macOS: system_profiler, launchctl, defaults
│   └── win32.py        # Windows: PowerShell, wmic, runas, winreg, sc.exe
├── mdm/                # MDM provider implementations
│   ├── jamf.py         # Jamf Pro: sys.argv[4-11] parsing
│   └── intune.py       # Intune: env vars, flexible argv
├── command_runner.py   # Cross-platform subprocess wrapper
├── dialog.py           # swiftDialog integration (macOS)
├── logger.py           # Structured logging
├── param_parser.py     # Backward-compat Jamf parser facade
├── system_info.py      # Cross-platform system info facade
└── webhook_sender.py   # HTTP webhook sender

Requirements

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymdm-0.5.0.tar.gz (50.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymdm-0.5.0-py3-none-any.whl (38.5 kB view details)

Uploaded Python 3

File details

Details for the file pymdm-0.5.0.tar.gz.

File metadata

  • Download URL: pymdm-0.5.0.tar.gz
  • Upload date:
  • Size: 50.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pymdm-0.5.0.tar.gz
Algorithm Hash digest
SHA256 f5bbafd377c5ba25d97ea549268e3733fafadc8b167b70f0192936921086ca58
MD5 03b8948e6beed17b6ff34372e6603ffe
BLAKE2b-256 a4d06e32ce78bffe7d09565fefa9f727a90feb9f1b63fb7c7a65a127408e5a1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymdm-0.5.0.tar.gz:

Publisher: build-release.yml on liquidz00/pymdm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymdm-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: pymdm-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 38.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pymdm-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 204b1519cb5f973e9022268f4ee64bcebf6f4ef7874a986ab6f8b72dc3aab1bd
MD5 e11e79e17ef166471e3fe90289f3dbb3
BLAKE2b-256 c69ff8adfb95c3c82230959ff46fe18b4257a63a67eb4532db965a4ecaba8927

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymdm-0.5.0-py3-none-any.whl:

Publisher: build-release.yml on liquidz00/pymdm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page