upload
napt.upload.manager
Upload orchestrator for NAPT Intune deployment.
Coordinates the full upload pipeline: loading recipe config, inferring the package path, authenticating, parsing the .intunewin file, building app metadata, and executing the Graph API upload flow.
Example
Upload a packaged app to Intune:
upload_package
Upload a packaged app to Microsoft Intune via the Graph API.
Loads the recipe config, infers the .intunewin package path, authenticates using the available Azure credential, parses encryption metadata from the package, and executes the full Graph API upload flow.
When intune.build_types is "both" (the default), two Intune app entries are created: an install entry (detection script only) and an update entry (detection + requirements scripts). Each entry is created, uploaded, and committed in sequence before moving to the next.
The package directory is inferred as packages/{app.id}/{version}/. Run 'napt package' before calling this function.
Authentication is automatic — no configuration required:
- Developers: set AZURE_CLIENT_ID and AZURE_TENANT_ID, complete device code flow
- CI/CD: set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
- Azure-hosted runners: assign a managed identity to the resource
Before any Graph call, the package's installer hash (from the build manifest) is verified against the pending release recorded in the app's deployment state, so what was recorded at discovery is byte-for-byte what ships. A hash mismatch aborts the upload. When no pending release is recorded, the upload proceeds with a warning — or fails when deployment.require_pending is enabled. On success, the deployment state records the published version, hash, and Intune app IDs, and a matching pending slot is cleared.
Re-running an upload is safe: existing NAPT-stamped apps matching this publish instance (recipe id, entry type, installer hash) are adopted — or their interrupted content upload resumed — instead of duplicated. Adoption keeps the app as it is; it does not re-send metadata or content. Pass force=True to update matched apps' metadata and upload a fresh content version (e.g., after changing PSADT commands or detection settings without a new installer release).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recipe_path
|
Path
|
Path to the recipe YAML file. |
required |
force
|
bool
|
When True, matched stamped apps are re-uploaded (metadata and content) instead of adopted as-is. Never creates duplicates. |
False
|
Returns:
| Type | Description |
|---|---|
UploadResult
|
Upload result including the Intune app ID(s), app name, version, and package path. intune_app_id is None when build_types is "update_only"; intune_update_app_id is None when build_types is "app_only". |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the package directory is not found, or detection/ requirements scripts are absent from the package directory. Run 'napt package' to create or recreate the package. |
AuthError
|
If all Azure credential methods fail. |
NetworkError
|
If Graph API or Azure Blob Storage calls fail. |
PackagingError
|
If the .intunewin file is malformed, the package's installer hash does not match the pending release in deployment state, or no pending release is recorded while deployment.require_pending is enabled. |
StateError
|
On a corrupted deployment state file. |
Example
Upload and print the resulting Intune app IDs:
Source code in napt/upload/manager.py
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 | |
napt.upload.intunewin
Parses .intunewin package files for NAPT upload operations.
A .intunewin file is a ZIP archive created by IntuneWinAppUtil with the following structure:
IntuneWinPackage/
Contents/
IntunePackage.intunewin <- encrypted payload
Metadata/
Detection.xml <- encryption metadata
This module extracts the encryption metadata from Detection.xml and provides utilities for extracting the encrypted payload for upload to Azure Blob Storage.
Example
Parse metadata and extract payload:
from pathlib import Path
from napt.upload.intunewin import parse_intunewin, extract_encrypted_payload
metadata = parse_intunewin(
Path("packages/napt-chrome/Invoke-AppDeployToolkit.intunewin")
)
print(f"Encrypted file: {metadata.encrypted_file_name}")
print(f"Encryption key: {metadata.encryption_key}")
IntunewinMetadata
dataclass
Encryption metadata extracted from a .intunewin package.
All fields are sourced from Detection.xml inside the .intunewin ZIP archive. This metadata is required by the Graph API file commit endpoint.
Attributes:
| Name | Type | Description |
|---|---|---|
encrypted_file_name |
str
|
Filename of the encrypted payload inside the Contents/ directory (always "IntunePackage.intunewin"). |
unencrypted_content_size |
int
|
Original size in bytes before encryption. |
file_digest |
str
|
Base64-encoded SHA-256 hash of the encrypted payload. |
file_digest_algorithm |
str
|
Hash algorithm used (always "SHA256"). |
encryption_key |
str
|
Base64-encoded AES-256 encryption key. |
mac_key |
str
|
Base64-encoded HMAC key for MAC verification. |
init_vector |
str
|
Base64-encoded AES initialization vector. |
mac |
str
|
Base64-encoded MAC value for integrity verification. |
profile_identifier |
str
|
Encryption profile version (always "ProfileVersion1"). |
encrypted_file_size |
int
|
Byte size of the encrypted payload file. |
Source code in napt/upload/intunewin.py
parse_intunewin
Parse a .intunewin package and extract encryption metadata.
Reads IntuneWinPackage/Metadata/Detection.xml from inside the .intunewin ZIP and returns all encryption fields required for the Graph API upload flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
intunewin_path
|
Path
|
Path to the .intunewin file to parse. |
required |
Returns:
| Type | Description |
|---|---|
IntunewinMetadata
|
Parsed encryption metadata from Detection.xml. |
Raises:
| Type | Description |
|---|---|
PackagingError
|
If the file is not a valid ZIP, Detection.xml is missing, or required XML fields are absent or malformed. |
Example
Parse an existing package:
Source code in napt/upload/intunewin.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
extract_encrypted_payload
Extract the encrypted payload from a .intunewin package.
Extracts IntuneWinPackage/Contents/IntunePackage.intunewin to the destination directory for upload to Azure Blob Storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
intunewin_path
|
Path
|
Path to the .intunewin file. |
required |
dest_dir
|
Path
|
Directory to extract the payload into. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
Path to the extracted encrypted payload file. |
Raises:
| Type | Description |
|---|---|
PackagingError
|
If the file is not a valid ZIP or the payload is missing. |
Source code in napt/upload/intunewin.py
napt.upload.auth
Azure credential acquisition for NAPT Intune upload.
Requires a NAPT app registration in Microsoft Entra ID with the
DeviceManagementApps.ReadWrite.All Microsoft Graph API permission.
See the authentication documentation for setup instructions.
Authentication is selected automatically based on environment variables:
Authentication order
- EnvironmentCredential -- service principal via environment variables. Set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, and AZURE_TENANT_ID. Recommended for CI/CD pipelines (GitHub Actions, Azure DevOps, etc.).
- ManagedIdentityCredential -- Azure managed identity. Works automatically on Azure VMs, Azure Container Instances, and Azure-hosted pipeline agents with a managed identity assigned. No credentials to manage.
- DeviceCodeCredential -- interactive device code flow (TTY only). Requires AZURE_CLIENT_ID and AZURE_TENANT_ID to be set (no secret needed). Prints a URL and code; the user completes authentication in any browser. Skipped in CI/CD and when output is redirected.
If all available methods fail, an AuthError is raised with guidance on which environment variables to set.
Example
Acquiring a token for Graph API:
get_credential
Build the Phase 1 credential chain for non-interactive authentication.
Returns a credential that tries service principal auth (via environment
variables) first, then managed identity for Azure-hosted workloads.
Both use the .default scope, suitable for application permissions.
For interactive device code auth, use get_access_token() directly,
which handles Phase 2 automatically when Phase 1 fails.
Returns:
| Type | Description |
|---|---|
ChainedTokenCredential
|
A ChainedTokenCredential for non-interactive authentication. |
Source code in napt/upload/auth.py
get_access_token
Acquire a Microsoft Graph API access token.
Tries credential methods in order until one succeeds:
Phase 1 (always tried):
EnvironmentCredential (service principal via AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET, AZURE_TENANT_ID) then ManagedIdentityCredential.
Both use the .default scope (application permissions).
Phase 2 (only if Phase 1 fails and stdout is a TTY): DeviceCodeCredential using AZURE_CLIENT_ID and AZURE_TENANT_ID. Uses the explicit DeviceManagementApps.ReadWrite.All scope, which triggers a consent prompt on first run.
Returns:
| Type | Description |
|---|---|
str
|
Bearer token string for use in Authorization headers. |
Raises:
| Type | Description |
|---|---|
AuthError
|
If all credential types fail or are unavailable, with guidance on which environment variables to set. |
Example
Get a token and use it in a request:
Source code in napt/upload/auth.py
napt.upload.graph
Microsoft Graph API and Azure Blob Storage client for Intune Win32 app upload.
Implements the full upload flow for a Win32 LOB app:
1. Create Win32 app record in Intune (POST mobileApps)
2. Create a content version (POST contentVersions)
3. Create a file entry and wait for SAS URI (POST files + polling)
4. Upload encrypted payload to Azure Blob Storage (PUT blocks + block list)
5. Commit the uploaded file with encryption metadata (POST commit + polling)
6. Set the committed content version on the app (PATCH mobileApps)
Also provides app queries used for reconciliation (list_mobile_apps, get_mobile_app, update_win32_app) and group-based assignment plumbing (resolve_group_id, get_app_assignments, build_group_assignment, assign_app) used by deployment promotion.
All functions take an access_token as the first argument. Obtain one via napt.upload.auth.get_access_token().
Graph calls retry transient failures — HTTP 429 (honoring Retry-After), transient server errors, and connection drops — with bounded exponential backoff before raising. Resource-creating POSTs retry only unambiguous throttling responses, so a lost reply to a processed create is never resubmitted as a duplicate. Azure Blob PUTs carry their own retry tuned for SAS-propagation 403s.
Example
Full upload flow:
from pathlib import Path
from napt.upload.auth import get_access_token
from napt.upload.graph import (
create_win32_app, create_content_version,
create_content_version_file, upload_to_azure_blob,
commit_content_version_file, commit_content_version,
)
from napt.upload.intunewin import parse_intunewin
token = get_access_token()
metadata = parse_intunewin(Path("packages/napt-chrome/144.0.7559.110/Invoke-AppDeployToolkit.intunewin"))
app_id = create_win32_app(token, app_metadata)
cv_id = create_content_version(token, app_id)
file_id, sas_uri = create_content_version_file(token, app_id, cv_id, metadata)
upload_to_azure_blob(sas_uri, Path("/tmp/IntunePackage.intunewin"))
commit_content_version_file(token, app_id, cv_id, file_id, metadata)
commit_content_version(token, app_id, cv_id)
resolve_group_id
Resolves an Entra ID group name or object ID to an object ID.
Values that already look like object IDs (GUIDs) pass through without a Graph call. Names are looked up by exact displayName match, which requires the Group.Read.All application permission.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
group
|
str
|
Group displayName or object ID (GUID). |
required |
Returns:
| Type | Description |
|---|---|
str
|
The group's object ID. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403 (check Group.Read.All permission). |
ConfigError
|
If no group or more than one group matches the name. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
resolve_assignment_target
resolve_assignment_target(
access_token: str, group: str, group_id_cache: dict[str, str] | None = None
) -> dict
Resolves a deployment group entry to an assignment target dict.
The reserved names "All Users" and "All Devices" map to Intune's built-in virtual targets; anything else resolves to an Entra ID group target via resolve_group_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
group
|
str
|
Group displayName, object ID, or reserved virtual name. |
required |
group_id_cache
|
dict[str, str] | None
|
Optional cache of name to object ID, shared across calls to avoid repeated lookups. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
An assignment target dict for use with build_assignment. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403 (check Group.Read.All permission). |
ConfigError
|
If no group or more than one group matches a name. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
get_app_assignments
Gets the current assignments of a mobile app.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the app. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
A list of mobileAppAssignment dicts (empty when unassigned). |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
build_assignment
Builds a mobileAppAssignment payload for a resolved target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
dict
|
An assignment target dict (group or virtual target). |
required |
intent
|
str
|
Assignment intent, "available" or "required". |
required |
Returns:
| Type | Description |
|---|---|
dict
|
A mobileAppAssignment dict for use with assign_app. |
Source code in napt/upload/graph.py
build_group_assignment
Builds a mobileAppAssignment payload targeting one Entra ID group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_id
|
str
|
Object ID of the target group. |
required |
intent
|
str
|
Assignment intent, "available" or "required". |
required |
Returns:
| Type | Description |
|---|---|
dict
|
A mobileAppAssignment dict for use with assign_app. |
Source code in napt/upload/graph.py
assign_app
Sets a mobile app's assignments.
The assign action replaces the app's entire assignment set. Callers that intend to preserve existing assignments must read them first with get_app_assignments and include them in the new list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the app. |
required |
assignments
|
list[dict]
|
Complete list of mobileAppAssignment dicts to apply. |
required |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
ConfigError
|
On 400 (invalid assignment payload). |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
list_mobile_apps
Lists all mobile apps in the tenant with id, displayName, and notes.
Follows @odata.nextLink pagination until the collection is exhausted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
A list of app dicts, each with at least "id", "displayName", and "notes" keys. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
delete_mobile_app
Deletes a mobile app from Intune.
A 404 is tolerated — the app being already gone is the desired end state, so retried deletions stay idempotent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the app to delete. |
required |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
get_mobile_app
Gets one mobile app's full object by Graph API ID.
Used to read subtype fields that $select on the collection cannot reliably return, such as win32LobApp.committedContentVersion.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the app. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The full app object dict. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
create_win32_app
Creates a new Win32 LOB app record in Intune.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_metadata
|
dict
|
Win32LobApp JSON payload (display name, install commands, detection rules, etc.). |
required |
Returns:
| Type | Description |
|---|---|
str
|
The Graph API object ID of the newly created app. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
ConfigError
|
On 400 (invalid metadata). |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
update_win32_app
Updates an existing Win32 LOB app record's metadata in Intune.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the app to update. |
required |
app_metadata
|
dict
|
Win32LobApp JSON payload to apply. |
required |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
ConfigError
|
On 400 (invalid metadata). |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
create_content_version
Creates a new content version for a Win32 app.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the Win32 app. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The content version ID string. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |
Source code in napt/upload/graph.py
create_content_version_file
create_content_version_file(
access_token: str, app_id: str, cv_id: str, metadata: IntunewinMetadata
) -> tuple[str, str]
Creates a file entry for a content version and waits for the SAS URI.
Posts the file size information to Graph API, then polls until Azure Storage has provisioned a SAS URI for the upload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the Win32 app. |
required |
cv_id
|
str
|
Content version ID from create_content_version. |
required |
metadata
|
IntunewinMetadata
|
Parsed .intunewin metadata (provides file sizes). |
required |
Returns:
| Type | Description |
|---|---|
tuple[str, str]
|
A tuple of (file_id, sas_uri) where sas_uri is the Azure Blob Storage SAS URI to upload the encrypted payload to. |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx, connection error, or upload state error. |
Source code in napt/upload/graph.py
upload_to_azure_blob
Uploads the encrypted payload to Azure Blob Storage using block blobs.
Splits the file into CHUNK_SIZE chunks, uploads each as a block with a base64-encoded block ID, then commits the block list. Prints an inline progress percentage as each chunk completes. Transient per-request failures (including 403 from a not-yet-propagated SAS URI) are retried with backoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sas_uri
|
str
|
Azure Blob Storage SAS URI from create_content_version_file. |
required |
encrypted_payload_path
|
Path
|
Path to the extracted encrypted payload file (IntunePackage.intunewin from inside the .intunewin ZIP). |
required |
Raises:
| Type | Description |
|---|---|
NetworkError
|
If any block upload or the block list commit fails after retries. |
Source code in napt/upload/graph.py
826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 | |
commit_content_version_file
commit_content_version_file(
access_token: str,
app_id: str,
cv_id: str,
file_id: str,
metadata: IntunewinMetadata,
) -> None
Commits the uploaded file with encryption metadata, then waits for confirmation.
Sends the encryption key, MAC, IV, and digest to Graph API, then polls until Intune confirms the file is committed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the Win32 app. |
required |
cv_id
|
str
|
Content version ID. |
required |
file_id
|
str
|
File entry ID from create_content_version_file. |
required |
metadata
|
IntunewinMetadata
|
Parsed .intunewin metadata (provides all encryption fields). |
required |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx, connection error, or if commit times out. |
Note
Graph returns 200 (not 201) for the commit POST.
Source code in napt/upload/graph.py
commit_content_version
Sets the committed content version on the Win32 app.
This is the final step — after calling this, the app is fully published in Intune and available for assignment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str
|
Bearer token for Graph API. |
required |
app_id
|
str
|
Graph API object ID of the Win32 app. |
required |
cv_id
|
str
|
Content version ID to mark as committed. |
required |
Raises:
| Type | Description |
|---|---|
AuthError
|
On 401 or 403. |
NetworkError
|
On 5xx or connection error. |