This is a standalone security lab. There is currently no published companion blog post, so it is intentionally omitted from the website's Labs catalog until a real companion article exists. Do not describe this repository as a companion lab or link it to an unpublished article.
The lab deploys an Azure Machine Learning (AML) workspace, safely validates or updates the documented Defender base plans with reversible lifecycle tracking, and deploys three disabled-by-default Microsoft Sentinel activity-audit rules plus a workbook. By default it builds and registers only a clean model. Deliberately unsafe demonstration artifacts require a separate, exact opt-in.
| Artifact | Default deploy | Purpose | Execution risk |
|---|---|---|---|
clean_iris_rf.pkl |
Yes | Negative-control sklearn model | Normal pickle trust rules still apply |
malicious_reduce.pkl |
No | Demonstrates pickle REDUCE code execution |
Unsafe if deserialized; fixed local sentinel only |
secret_exposed_model.pkl |
No | Demonstrates fake credentials serialized with a model | No intentional code execution |
unsafe_torchscript.pt |
Never automatic | Optional PyTorch pickle-opcode sample | Unsafe if loaded; fixed local sentinel only |
Microsoft controls supported model formats, scan timing, licensing, and feature availability. Verify the current behavior in your tenant and the linked Microsoft documentation rather than treating the examples here as a service-level guarantee.
.
├── .github/workflows/model-scan.yml # Active, SHA-pinned CI workflow
├── ci/github-actions-model-scan.yml # Identical copy for reuse
├── infra/
│ ├── main.bicep # AML workspace and dependencies
│ ├── sentinel-rules.bicep # Provenance-marked rules and workbook
│ └── workbook.json
├── payloads/
│ ├── build_utils.py # Atomic output and unsafe-build guard
│ ├── build_clean_model.py
│ ├── build_malicious_reduce.py
│ ├── build_secret_exposed.py
│ ├── build_unsafe_torchscript.py # Optional; Torch is not installed by deploy
│ └── requirements.txt # Exact Python dependency versions
├── scripts/
│ ├── deploy-lab.sh
│ ├── cleanup.sh
│ ├── install-defender-cli.sh # HTTPS + mandatory SHA-256 verification
│ └── lab_state.py # Ownership and rollback manifest helper
└── tests/
Microsoft's public onboarding documentation exposes the AI model security component as a portal setting, while the public Microsoft.Security/pricings@2024-01-01 schema does not document an API extension for that component. This lab therefore does not send a guessed or reverse-engineered component value. Enable Defender for Cloud → Environment settings → AI services → Settings → AI model security first, then attest that the portal step is complete:
export CONFIRM_PORTAL_AI_MODEL_SECURITY=ENABLED-IN-DEFENDER-PORTALThe deploy can still change the documented shared subscription-level CloudPosture and AI base pricing tiers if they are not already Standard. Before the first write it captures the mutable prior properties in .defender-ai-model-security-state.json, preserves subPlan, enforce, and every existing extension, and requires this separate acknowledgement:
export CONFIRM_SUBSCRIPTION_SCOPE=ENABLE-DEFENDER-CSPM-AND-AI-PLANSThese plans may incur Azure charges. The script does not silently select the active subscription, register resource providers, ignore plan failures, guess an undocumented component API, or replace unrelated extensions.
The two code-execution payload builders refuse to run unless this separate acknowledgement is present:
export CONFIRM_UNSAFE_MODEL_ARTIFACTS=BUILD-PICKLE-RCE-DEMONever unpickle or load those artifacts. Their reducers contain a fixed command that can only write a named sentinel under /tmp, with no network or destructive operation, but they remain executable untrusted data.
- An Azure subscription where you can deploy resource-group resources and manage Defender pricing.
- Azure CLI with the
mlextension already reviewed and installed. - Python 3.11+ with
venvsupport. - A Log Analytics workspace already onboarded to Microsoft Sentinel.
- These resource providers already registered:
Microsoft.Insights,Microsoft.KeyVault,Microsoft.MachineLearningServices,Microsoft.OperationalInsights,Microsoft.Security,Microsoft.SecurityInsights, andMicrosoft.Storage. - Defender CSPM, Defender for AI Services, and the portal-only AI model security component enabled for the target subscription.
- Azure Activity data available in the Sentinel workspace for the AML audit rules and workbook. Connector configuration is tenant-specific and is not changed by this lab.
Select the subscription yourself and pass the same ID explicitly:
az login
az account set --subscription '<subscription-id>'
export SUBSCRIPTION_ID='<subscription-id>'
export SENTINEL_WS_ID='/subscriptions/<subscription-id>/resourceGroups/<sentinel-rg>/providers/Microsoft.OperationalInsights/workspaces/<workspace>'
export CONFIRM_PORTAL_AI_MODEL_SECURITY=ENABLED-IN-DEFENDER-PORTAL
# Only required if the script reports that the documented base plans must change:
export CONFIRM_SUBSCRIPTION_SCOPE=ENABLE-DEFENDER-CSPM-AND-AI-PLANS
./scripts/deploy-lab.shThe portal-component attestation is always required because there is no documented API readback in this repository. If both documented Defender base plans already match the required state, the subscription-scope confirmation is not needed and no pricing write occurs. A rerun must use the same subscription, Sentinel workspace, resource-group name, and lifecycle manifest. Target drift and pre-existing unowned resources fail closed.
The script performs these operations:
- Validates the explicit subscription, portal-component attestation, Sentinel workspace, provider registrations, and AML CLI extension.
- Captures the prior Defender pricing state before any mutation.
- Preflights deterministic Sentinel resource IDs and refuses provenance collisions.
- Changes only documented base-plan properties when required and records ownership before each write.
- Creates a provenance-tagged lab resource group and deploys the AML infrastructure.
- Deploys or updates only lab-owned, disabled-by-default Sentinel AzureActivity audit resources.
- Builds and registers the clean model.
To additionally build and register the pickle code-execution and fake-secret samples:
export BUILD_UNSAFE_PAYLOADS=true
export CONFIRM_UNSAFE_MODEL_ARTIFACTS=BUILD-PICKLE-RCE-DEMO
./scripts/deploy-lab.shThe optional PyTorch checkpoint is never part of the deploy. After independently reviewing and installing a pinned CPU-only Torch build in an isolated environment, run its builder with the same unsafe-artifact confirmation. Do not load the output.
The official AI model-security documentation surfaces scan results as Defender security findings/recommendations; it does not document an Ai.AIModelScan* security-alert family. Accordingly, this repository does not pretend those findings exist in Sentinel's SecurityAlert table.
The deployment creates these exact, provenance-marked Azure Activity audit resources when their deterministic IDs are unoccupied. Rules are disabled by default to avoid turning expected lab actions into incidents:
| Rule | Signal actually proven |
|---|---|
| AML model registration or update observed | A successful AML model write appears in AzureActivity |
| Repeated AML model or deployment writes by same caller | Two or more relevant write events share the same provider-supplied Caller value in seven days |
| AML endpoint deployment write requires scan verification | A successful AML online or batch endpoint-deployment write appears in AzureActivity |
These signals do not contain model contents or scan verdicts. A repeated Caller value is an audit lead, not proof of compromise. The deployment-review rule does not claim a scan was missing or incomplete. Review model findings separately in Defender for Cloud or via a trusted Defender CLI SARIF result. The workbook repeats these boundaries.
The active workflow is .github/workflows/model-scan.yml; ci/github-actions-model-scan.yml is an identical reusable copy, not a nonexistent symlink. It scans model files under models/ only. Intentionally unsafe lab payloads are never generated or included in that CI search path.
Configure these GitHub settings:
- OIDC secrets:
AZURE_CLIENT_ID,AZURE_TENANT_ID, andAZURE_SUBSCRIPTION_ID. - Repository variable
DEFENDER_CLI_SHA256: the reviewed 64-character SHA-256 for the exact Linux Defender CLI binary. - Optional repository variable
DEFENDER_CLI_URL: an alternate reviewed HTTPS URL. If omitted, the official short URL in the installer is used, but the checksum remains mandatory.
All workflow actions are pinned to full commit SHAs. The local installer downloads over HTTPS, rejects a checksum mismatch, and never pipes remote content into a shell:
export DEFENDER_CLI_SHA256='<reviewed-64-character-sha256>'
export DEFENDER_CLI_INSTALL_DIR="$PWD/.local/bin"
bash ./scripts/install-defender-cli.shUse the same exact target values as deployment:
export SUBSCRIPTION_ID='<subscription-id>'
export SENTINEL_WS_ID='/subscriptions/<subscription-id>/resourceGroups/<sentinel-rg>/providers/Microsoft.OperationalInsights/workspaces/<workspace>'
./scripts/cleanup.shCleanup validates the manifest, restores only Defender plan state changed by this run, deletes only exact Sentinel resources recorded as lab-owned, and deletes the lab resource group only when its ID and provenance tag match. It never enumerates or purges Key Vaults by prefix.
Azure retains the exact soft-deleted Key Vault for recovery by default. Permanent purge is optional and irreversible; the confirmation includes the manifest-recorded name:
export CONFIRM_EXACT_KEY_VAULT_PURGE='PURGE-<exact-key-vault-name-from-cleanup-output>'
./scripts/cleanup.shThe lifecycle manifest is intentionally retained as an audit record after cleanup. Archive or deliberately remove it only when starting a genuinely new lifecycle.