Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

microsoft-tools

Microsoft Graph / Intune helpers for Python, including a complete Win32 LOB application upload pipeline — the part of the Graph API that is genuinely hard to get right and rarely shown end to end.

Status

Demonstrator. This is an extract of an internal tool used in production, republished without its production plumbing: the internal version reads its tenant credentials and OAuth token from a secret manager and drives deployments from a fleet database. This extract takes credentials from environment variables and caches its token in a local file. It is meant to show the approach, not to be deployed as-is.

What's here

Module Contents
base.py MicrosoftAPI: auth, OData query builder, paged user/group listing, users, licences, aliases, devices
mobileapps.py The Win32 upload pipeline (AppSpec + MobileApps) — the centrepiece
intune.py Managed devices and primary-user resolution
bitlocker.py Read BitLocker recovery keys
auditlogs.py Read Entra ID sign-in logs

The Win32 pipeline

Publishing a Win32 app to Intune is a nine-step handshake across Graph and Azure Blob storage, and nothing about it is idempotent on its own:

create app -> create content version -> declare a file placeholder
-> poll Graph for an Azure Blob SAS URL -> upload the payload in blocks
-> commit the block list (Azure) -> commit the file + encryption info (Graph)
-> poll until Graph accepts the commit -> mark the version committed

.intunewin packages carry an AES-CBC encrypted payload plus a detection.xml with the key, IV, MAC and digests. This code never re-encrypts: it uploads the encrypted blob untouched, and decrypts only to measure the plaintext size Graph requires up front.

What makes upload_app() re-runnable is not the API — it is the read-compare-patch logic around it:

  • App metadata: fetch the current app, PATCH only the fields that differ, after stripping the read-only fields Graph rejects on PATCH.
  • Content: skip the whole upload when the deployed version already matches.
  • Assignment: reuse the existing "required, all devices" assignment unless its settings drifted, in which case recreate it.
  • Ambiguity: if two apps share the target display name, refuse to act rather than guess.
from microsoft_tools import AppSpec, MicrosoftAPI

api = MicrosoftAPI.from_env()
spec = AppSpec(
    display_name="Example Agent",
    version="1.4.2",
    intunewin_path="/path/to/example_agent.intunewin",
    detection_template_path="examples/detection.ps1.jinja.example",
)
api.upload_app(spec)     # create/patch/upload/assign as needed; safe to repeat

See examples/deploy_app.py for the full example.

Install

pip install -e .

Configuration

Copy .env.example to .env. You need an Entra ID app registration (confidential client) and its secret. The flow is delegated authorization-code, so the first run needs an interactive browser sign-in that caches a token:

python -c "from microsoft_tools import MicrosoftAPI; MicrosoftAPI.from_env().authenticate()"

Later runs reuse the cached token and refresh it automatically. Store the token elsewhere than a file by swapping FileSystemTokenBackend for any O365 BaseTokenBackend (a secret manager, a database, ...).

Scopes default to a read/write set (DEFAULT_SCOPES); a READONLY_SCOPES preset is provided for audit-only tenants. Pass scopes= to from_env() to override.

Known limitations

  • Auth is delegated, not app-only. It needs a one-time browser sign-in per tenant and then lives on refresh tokens. There is no client-credentials path, which would be the natural choice for unattended app-only automation.
  • Pagination is not universal. get_users() / list_groups() page through @odata.nextLink, but get_devices(), get_apps(), get_bitlocker_recovery_keys() and get_sign_in_logs() read a single page and stop. On a large tenant they silently return a truncated list.
  • 429 handling is only on blob upload. The retry policy (8 tries, backoff, 429/5xx) is mounted on the requests session used for Azure Blob uploads. The Graph calls go through the O365 connection and are not covered, so a throttled Graph call fails rather than retries.
  • Errors are swallowed. Several getters do except HTTPError: return [], which hides a permissions or throttling problem behind an empty result.
  • Version tracking is by description string. upload_app reads the deployed version by splitting the app's description on spaces and taking the last token, expecting exactly three parts. A hand-edited description breaks the comparison and the upload is skipped. A dedicated field would be sturdier.
  • /beta endpoints. Device management scripts and some device calls use the Graph beta endpoint, which Microsoft may change without notice.

Licence

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages