

Guidance inspired by the JadePuffer AI Actor scenario – an exposed service‑principal secret used to launch a destructive cloud attack.
Goal: Give developers ready‑to‑copy code that detects, hardens, and responds to credential‑exposure risks in Azure. The examples use the official Azure SDKs (Python & JavaScript/TypeScript) and follow modern security best‑practices (least‑privilege, managed identities, secret rotation, monitoring & alerting).
<a name="prerequisites"></a>
| Item | Why you need it | Version / Notes |
|---|---|---|
| Azure Subscription | Target tenant to protect | Any (Pay‑as‑you‑go, CSP, etc.) |
| Azure AD tenant | Source of identities & sign‑in logs | Must have Azure AD Premium P1/P2 (or Microsoft Defender for Cloud) for Identity Protection logs |
Azure CLI (az) | Quick login, role assignments, resource creation | >= 2.30.0 |
| Python | SDK samples | >= 3.8 |
| Node.js | SDK samples | >= 14.x (LTS) |
| Git (optional) | Clone sample repo | any |
| Permissions | To run the scripts you need: <br>• Reader on subscription (to list resources) <br>• Security Reader (to read sign‑in logs via Microsoft Graph) <br>• Key Vault Secrets User (if you test secret‑rotation) <br>• Policy Insights Reader (to evaluate policy compliance) | Assign via Azure Portal, Azure CLI, or RBAC JSON |
Tip: For a least‑privilege dev environment, create a service principal with only the above roles and use Managed Identity when running inside Azure (VM, App Service, AKS, etc.).
<a name="installation-and-setup"></a>
# Interactive login – opens a browser
az login
# If you prefer a service principal for CI/CD:
az login --service-principal -u <APP_ID> -p <PASSWORD> --tenant <TENANT_ID>
Set the default subscription (replace <SUB_ID>):
az account set --subscription <SUB_ID>
# Create a venv (recommended)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install Azure SDK packages
pip install --upgrade pip
pip install azure-identity azure-mgmt-resource azure-mgmt-monitor azure-mgmt-keyvault azure-graphrbac
# Optional: for Microsoft Graph (sign‑in logs)
pip install msgraph-core
# Initialize a Node project (if you don't have one)
npm init -y
# Install Azure SDK packages
npm install @azure/identity @azure/arm-resources @azure/arm-monitor @azure/keyvault-secrets @microsoft/microsoft-graph-client
# TypeScript typings (if using TS)
npm install -D @types/node typescript ts-node
Note: All SDKs use Azure Identity (
DefaultAzureCredential) which automatically picks up environment variables, managed identity, VS Code Azure Account extension, or Azure CLI login—making the same code work locally and in Azure.
<a name="basic-implementation"></a>
Below are two complete, runnable scripts that:
You can run each script independently; they demonstrate the same logic in Python and JavaScript/TypeScript.
<a name="python"></a>
azure_security_check.py)#!/usr/bin/env python3
"""
Azure Security Health‑Check Script
----------------------------------
* Detects publicly exposed storage accounts & VMs
* Flags service‑principal secrets older than DAYS_THRESHOLD
* Pulls risky sign‑ins from Microsoft Graph (if licensed)
* Evaluates a sample Azure Policy (MFA for privileged roles)
Requires:
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID
(or Managed Identity / Azure CLI login)
Run:
python azure_security_check.py
"""
import os
import datetime
from typing import List
from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.compute import ComputeManagementClient
from azure.mgmt.storage import StorageManagementClient
from azure.mgmt.keyvault import KeyVaultManagementClient
from azure.mgmt.keyvault.v7_0.models import VaultProperties
from msgraph.core import GraphClient # pip install msgraph-core
# -------------------------- CONFIG --------------------------
SUBSCRIPTION_ID = os.getenv("AZURE_SUBSCRIPTION_ID")
DAYS_THRESHOLD = int(os.getenv("CREDENTIAL_AGE_DAYS", "90")) # secrets older than this are flagged
# -----------------------------------------------------------
def get_credential():
"""DefaultAzureCredential tries Env, Managed Identity, VS Code, Azure CLI."""
return DefaultAzureCredential(exclude_interactive_browser_credential=False)
def list_public_storage_accounts(storage_client: StorageManagementClient) -> List[str]:
"""Return names of storage accounts with public blob access."""
public = []
for acct in storage_client.storage_accounts.list():
# acct.enable_https_traffic_only is a security flag; we also check network rule set
if acct.allow_blob_public_access: # True if public access allowed
public.append(acct.name)
return public
def list_vms_with_public_ip(compute_client: ComputeManagementClient) -> List[str]:
"""Return VM names that have a public IP address attached."""
vms_with_pip = []
for vm in compute_client.virtual_machines.list_all():
# Expand to get network profile details
vm_instance = compute_client.virtual_machines.get(
resource_group_name=vm.id.split("/")[4],
vm_name=vm.name,
expand='instanceView'
)
# Simplified: check if any NIC has a public IP
for nic_ref in vm_instance.network_profile.network_interfaces:
nic_name = nic_ref.id.split("/")[-1]
# In a real script you'd call the Network client to get NIC details.
# For brevity we assume a tag or naming convention indicates public IP.
# Replace with actual NetworkManagementClient call in production.
if "pub" in nic_name.lower():
vms_with_pip.append(vm.name)
break
return vms_with_pip
def list_stale_service_principals(graph_client: GraphClient) -> List[dict]:
"""Query Microsoft Graph for service principals with credentials older than DAYS_THRESHOLD."""
stale = []
# Graph endpoint: /servicePrincipals?$filter=accountEnabled eq true
# We'll pull a page and inspect keyCredentials & passwordCredentials
query = f"/servicePrincipals?$select=id,displayName,keyCredentials,passwordCredentials&$top=999"
resp = graph_client.get(query)
if resp.status_code != 200:
raise RuntimeError(f"Graph error: {resp.status_code} {resp.text}")
data = resp.json()
for sp in data.get("value", []):
now = datetime.datetime.utcnow()
for cred in sp.get("keyCredentials", []) + sp.get("passwordCredentials", []):
# cred contains 'startDateTime' and 'endDateTime' (ISO strings)
end_str = cred.get("endDateTime")
if not end_str:
continue
end_dt = datetime.datetime.fromisoformat(end_str.rstrip("Z"))
age_days = (now - end_dt).days
if age_days > DAYS_THRESHOLD:
stale.append({
"objectId": sp["id"],
"displayName": sp.get("displayName"),
"credentialType": "key" if cred in sp.get("keyCredentials", []) else "password",
"ageDays": age_days,
"endDateTime": end_str
})
return stale
def get_risky_signins(graph_client: GraphClient, lookback_hours: int = 24) -> List[dict]:
"""Pull risky sign‑ins from Azure AD Identity Protection (requires P1/P2)."""
# Microsoft Graph beta endpoint: /identityProtection/riskyUsers
# For sign‑ins: /identityProtection/riskyDetections
# We'll use the simpler risky sign‑ins API:
now = datetime.datetime.utcnow()
start_time = (now - datetime.timedelta(hours=lookback_hours)).isoformat() + "Z"
query = f"/identityProtection/riskyDetections?$filter=createdDateTime ge {start_time}"
resp = graph_client.get(query)
if resp.status_code != 200:
raise RuntimeError(f"Graph risky sign‑ins error: {resp.status_code} {resp.text}")
return resp.json().get("value", [])
def evaluate_mfa_policy(policy_client) -> bool:
"""
Placeholder: In a real scenario you would call Azure Policy Insights
to evaluate a built‑in policy like 'Require MFA for privileged roles'.
Here we simply return True to show where the call would go.
"""
# Example (pseudo):
# results = policy_client.policy_states.list_query_results_for_subscription(
# f"/subscriptions/{SUBSCRIPTION_ID}",
# options={"filter": f"policyAttribute eq 'Microsoft.Authorization/policyDefinitions/...'"}
# )
# return all(r.compliance_state == "Compliant" for r in results.value)
return True # Assume compliant for demo
def main():
cred = get_credential()
# Initialize clients
resource_client = ResourceManagementClient(cred, SUBSCRIPTION_ID)
compute_client = ComputeManagementClient(cred, SUBSCRIPTION_ID)
storage_client = StorageManagementClient(cred, SUBSCRIPTION_ID)
# Graph client (no subscription needed)
graph_client = GraphClient(credential=cred, scopes=["https://graph.microsoft.com/.default"])
print("=== Azure Security Health‑Check ===\n")
# 1️⃣ Public storage accounts
public_storage = list_public_storage_accounts(storage_client)
if public_storage:
print(f"[!] {len(public_storage)} storage account(s) allow public blob access:")
for name in public_storage:
print(f" - {name}")
else:
print("[✓] No storage accounts with public blob access detected.")
# 2️⃣ VMs with public IPs (simplified check)
vms_pub = list_vms_with_public_ip(compute_client)
if vms_pub:
print(f"\n[!] {len(vms_pub)} VM(s) appear to have a public IP attached:")
for name in vms_pub:
print(f" - {name}")
else:
print("\n[✓] No VMs with obvious public IP detected (check Network client for full accuracy).")
# 3️⃣ Stale service‑principal credentials
try:
stale_sp = list_stale_service_principals(graph_client)
if stale_sp:
print(f"\n[!] {len(stale_sp)} service principal(s) have credentials older than {DAYS_THRESHOLD} days:")
for sp in stale_sp:
print(f" - {sp['displayName']} ({sp['objectId']}) "
f"{sp['credentialType']} credential age: {sp['ageDays']} days "
f"(expires {sp['endDateTime']})")
else:
print(f"\n[✓] No service‑principal credentials older than {DAYS_THRESHOLD} days found.")
except Exception as e:
print(f"\n[⚠] Could not evaluate service‑principal credentials: {e}")
# 4️⃣ Risky sign‑ins (last 24 h)
try:
risky = get_risky_signins(graph_client, lookback_hours=24)
if risky:
print(f"\n[!] {len(risky)} risky sign‑in detection(s) in the last 24 h:")
for r in risky[:5]: # show first 5
print(f" - User: {r.get('userDisplayName')} | Risk: {r.get('riskLevel')} | "
f"Type: {r.get('riskEventType')} | Time: {r.get('createdDateTime')}")
if len(risky) > 5:
print(f" ... and {len(risky)-5} more.")
else:
print("\n[✓] No risky sign‑ins detected in the last 24 h.")
except Exception as e:
print(f"\n[⚠] Could not query risky sign‑ins (maybe missing Azure AD P1/P2 license): {e}")
# 5️⃣ Azure Policy compliance (MFA for privileged roles)
mfa_ok = evaluate_mfa_policy(None) # pass real policy client in prod
if mfa_ok:
print("\n[✓] Azure Policy for MFA on privileged roles is compliant.")
else:
print("\n[!] Azure Policy for MFA on privileged roles is NON‑COMPLIANT – remediate!")
print("\n=== Health‑Check Complete ===")
if __name__ == "__main__":
main()
What the script does
| Section | Security relevance |
|---|---|
| Public storage accounts | Prevents data leakage via open blob containers. |
| VMs with public IPs | Reduces attack surface; ideally VMs are behind private subnets or Azure Bastion. |
| Stale SP credentials | Directly addresses the JadePuffer attack vector – old/exposed secrets. |
| Risky sign‑ins | Uses Azure AD Identity Protection to catch compromised credentials in real time. |
| MFA policy check | Ensures privileged roles enforce MFA – a core defensive control. |
Run it locally (after setting env vars) or deploy as an Azure Function/Automation Account that runs on a schedule (e.g., every 6 h).
<a name="javascripttypescript"></a>
azure-security-check.ts)/**
* Azure Security Health‑Check (Node.js/TS)
* -------------------------------------------------
* Same functionality as the Python script but using the Azure SDK for JS.
*
* Prerequisites (env vars):
* AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID
*
* To run:
* npx ts-node azure-security-check.ts # if you have ts-node installed
* # or compile: tsc azure-security-check.ts && node azure-security-check.js
*/
import { DefaultAzureCredential } from "@azure/identity";
import {
ResourceManagementClient,
ComputeManagementClient,
StorageManagementClient,
} from "@azure/arm-resources"; // actually separate packages; see imports below
import { GraphClient } from "@microsoft/microsoft-graph-client";
import * as dotenv from "dotenv";
dotenv.config(); // loads .env file into process.env
const SUBSCRIPTION_ID = process.env.AZURE_SUBSCRIPTION_ID!;
const DAYS_THRESHOLD = parseInt(process.env.CREDENTIAL_AGE_DAYS ?? "90", 10);
// ------------------------------------------------------------------
// Helper: create Azure SDK clients
// ------------------------------------------------------------------
const credential = new DefaultAzureCredential({
excludeInteractiveBrowserCredential: false,
});
const resourceClient = new ResourceManagementClient(credential, SUBSCRIPTION_ID);
const computeClient = new ComputeManagementClient(credential, SUBSCRIPTION_ID);
const storageClient = new StorageManagementClient(credential, SUBSCRIPTION_ID);
// Microsoft Graph client (requires delegated permission: IdentityRiskEvent.Read.All)
const graphClient = GraphClient.init({
authProvider: (done) => {
credential.getToken(["https://graph.microsoft.com/.default"]).then(
(tokenInfo) => done(null, tokenInfo.token),
(err) => done(err, null)
);
},
});
// ------------------------------------------------------------------
// 1️⃣ Public storage accounts
// ------------------------------------------------------------------
async function listPublicStorageAccounts(): Promise<string[]> {
const publicAccounts: string[] = [];
for await const acct of storageClient.storageAccounts.list()) {
if (acct.allowBlobPublicAccess) {
publicAccounts.push(acct.name!);
}
}
return publicAccounts;
}
// ------------------------------------------------------------------
// 2️⃣ VMs with public IPs (simplified)
// ------------------------------------------------------------------
async function listVmsWithPublicIp(): Promise<string[]> {
const vms: string[] = [];
for await const vm of computeClient.virtualMachines.listAll()) {
// In production, call the Network client to inspect NICs.
// Here we use a naming convention as a placeholder.
if (vm.name?.toLowerCase().includes("pub")) {
vms.push(vm.name);
}
}
return vms;
}
// ------------------------------------------------------------------
// 3️⃣ Stale service‑principal credentials (Microsoft Graph)
// ------------------------------------------------------------------
interface StaleSP {
objectId: string;
displayName?: string;
credentialType: "key" | "password";
ageDays: number;
endDateTime: string;
}
async function getStaleServicePrincipals(): Promise<StaleSP[]> {
const stale: StaleSP[] = [];
const now = new Date();
const thresholdDate = new Date(now.getTime() - DAYS_THRESHOLD * 24 * 60 * 60 * 1000);
// Paginate through servicePrincipals (max 999 per page)
let nextLink = "/servicePrincipals?$select=id,displayName,keyCredentials,passwordCredentials&$top=999";
while (nextLink) {
const resp = await graphClient.api(nextLink).get();
const value = value as any[] ?? resp.value;
for (const sp of value) {
const creds = [...(sp.keyCredentials ?? []), ...(sp.passwordCredentials ?? [])];
for (const cred of creds) {
const endStr = cred.endDateTime;
if (!endStr) continue;
const endDate = new Date(endStr);
if (endDate < thresholdDate) {
stale.push({
objectId: sp.id,
displayName: sp.displayName,
credentialType: cred.keyId ? "key" : "password",
ageDays: Math.floor((now.getTime() - endDate.getTime()) / (1000 * 60 * 60 * 24)),
endDateTime: endStr,
});
}
}
}
nextLink = resp["@odata.nextLink"] ?? null;
}
return stale;
}
// ------------------------------------------------------------------
// 4️⃣ Risky sign‑ins (last 24 h)
// ------------------------------------------------------------------
interface RiskySignIn {
userDisplayName?: string;
riskLevel: string;
riskEventType: string;
createdDateTime: string;
}
async function getRiskySignins(hours = 24): Promise<RiskySignIn[]> {
const start = new Date(Date.now() - hours * 60 * 60 * 1000)
.toISOString()
.replace(/\.\d+/, "") // strip ms
+ "Z";
const filter = `createdDateTime ge ${start}`;
const resp = await graphClient
.api(`/identityProtection/riskyDetections`)
.filter(filter)
.get();
return resp.value as RiskySignIn[];
}
// ------------------------------------------------------------------
// 5️⃣ Placeholder for Azure Policy MFA check
// ------------------------------------------------------------------
async def evaluateMfaPolicy(): Promise<boolean> {
// In real code: use @azure/arm-policyinsights
// For demo, assume compliant.
return true;
}
// ------------------------------------------------------------------
// Main orchestration
// ------------------------------------------------------------------
(async () => {
console.log("=== Azure Security Health‑Check (JS/TS) ===\n");
// 1️⃣ Public storage
const publicStorage = await listPublicStorageAccounts();
if (publicStorage.length) {
console.log(`[!] ${publicStorage.length} storage account(s) allow public blob access:`);
publicStorage.forEach((n) => console.log(` - ${n}`));
} else {
console.log("[✓] No storage accounts with public blob access detected.");
}
// 2️⃣ VMs with public IP (placeholder)
const vmsPub = await listVmsWithPublicIp();
if (vmsPub.length) {
console.log(`\n[!] ${vmsPub.length} VM(s) appear to have a public IP attached:`);
vmsPub.forEach((n) => console.log(` - ${n}`));
} else {
console.log("\n[✓] No VMs with obvious public IP detected (see note in code).");
}
// 3️⃣ Stale SP credentials
try {
const staleSP = await getStaleServicePrincipals();
if (staleSP.length) {
console.log(
`\n[!] ${staleSP.length} service principal(s) have credentials older than ${DAYS_THRESHOLD} days:`
);
staleSP.forEach((sp) => {
console.log(
` - ${sp.displayName ?? sp.objectId} (${sp.objectId}) ${sp.credentialType} credential age: ${sp.ageDays} days (expires ${sp.endDateTime})`
);
});
} else {
console.log(
`\n[✓] No service‑principal credentials older than ${DAYS_THRESHOLD} days found.`
);
}
} catch (e) {
console.error("\n[⚠] Failed to evaluate service‑principal credentials:", e);
}
// 4️⃣ Risky sign‑ins
try {
const risky = await getRiskySignins(24);
if (risky.length) {
console.log(`\n[!] ${risky.length} risky sign‑in detection(s) in the last 24 h:`);
risky.slice(0, 5).forEach((r) => {
console.log(
` - User: ${r.userDisplayName ?? "unknown"} | Risk: ${r.riskLevel} | Type: ${r.riskEventType} | Time: ${r.createdDateTime}`
);
});
if (risky.length > 5) console.log(` ... and ${risky.length - 5} more.`);
} else {
console.log("\n[✓] No risky sign‑ins detected in the last 24 h.");
}
} catch (e) {
console.error("\n[⚠] Could not query risky sign‑ins (maybe missing Azure AD P1/P2 license):", e);
}
// 5️⃣ MFA policy
const mfaOk = await evaluateMfaPolicy();
if (mfaOk) {
console.log("\n[✓] Azure Policy for MFA on privileged roles is compliant.");
} else {
console.log("\n[!] Azure Policy for MFA on privileged roles is NON‑COMPLIANT – remediate!");
}
console.log("\n=== Health‑Check Complete ===");
})();
How to run
# 1️⃣ Install deps
npm install @azure/identity @azure/arm-resources @azure/arm-monitor @azure/keyvault-secrets @microsoft/microsoft-graph-client dotenv
# 2️⃣ Create a .env file (never commit this!)
cat > .env <<EOF
AZURE_TENANT_ID=<your-tenant-id>
AZURE_CLIENT_ID=<your-app-id>
AZURE_CLIENT_SECRET=<your-client-secret>
AZURE_SUBSCRIPTION_ID=<your-subscription-id>
CREDENTIAL_AGE_DAYS=90
EOF
# 3️⃣ Execute
npx ts-node azure-security-check.ts # or compile first: tsc azure-security-check.ts && node azure-security-check.js
Security note: The script uses a service principal with secret stored in
.env. In production, replace this with a Managed Identity (Azure Functions, App Service, AKS, etc.) or Azure Key Vault reference to avoid persisting secrets on disk.
<a name="configuration"></a>
| Variable | Description | Example |
|---|---|---|
AZURE_TENANT_ID | Azure AD tenant GUID | 11111111-1111-1111-1111-111111111111 |
AZURE_CLIENT_ID | Application (client) ID of the service principal | 22222222-2222-2222-2222-222222222222 |
AZURE_CLIENT_SECRET | Secret value for the SP (or use Key Vault reference) | super-secret-value |
AZURE_SUBSCRIPTION_ID | Target subscription GUID | 33333333-3333-3333-3333-333333333333 |
CREDENTIAL_AGE_DAYS | Age threshold for flagging stale credentials (default 90) | 90 |
KEYVAULT_NAME (optional) | If you prefer to fetch the SP secret from Key Vault at runtime | my-prod-kv |
LOG_ANALYTICS_WORKSPACE_ID (optional) | For sending health‑check results to Azure Monitor Logs | /subscriptions/.../resourceGroups/rg-log/providers/Microsoft.OperationalInsights/workspaces/law |
How to load Key Vault secret at runtime (Python example)
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
kv_name = os.getenv("KEYVAULT_NAME")
kv_uri = f"https://{kv_name}.vault.azure.net/"
credential = DefaultAzureCredential()
client = SecretClient(vault_url=kv_uri, credential=credential)
sp_secret = client.get_secret("AzureSpSecret").value
JavaScript/TS equivalent
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
const kvName = process.env.KEYVAULT_NAME;
const kvUri = `https://${kvName}.vault.azure.net/`;
const credential = new DefaultAzureCredential();
const client = new SecretClient(kvUri, credential);
const spSecret = await client.getSecret("AzureSpSecret");
Best practice: Never store
AZURE_CLIENT_SECRETin plain text files or repo. Use Azure Key Vault, GitHub Actions secrets, or Azure Pipelines library variables.
<a name="common-patterns"></a>
| Pattern | Why it matters | Code snippet (Python) | Code snippet (JS/TS) |
|---|---|---|---|
| Managed Identity | No secrets to manage; Azure provides short‑lived token automatically. | credential = DefaultAzureCredential() (works when running inside Azure) | Same – new DefaultAzureCredential() |
| Centralized Secret Retrieval | Reduces secret sprawl; enables rotation & audit. | See Key Vault example above. | See Key Vault example above. |
| Structured Logging to Log Analytics | Enables alerting, dashboards, and correlation with other signals. | python\nfrom azure.monitor.opentelemetry import configure_azure_monitor\nconfigure_azure_monitor(connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING))\nlogger = logging.getLogger(__name__)\nlogger.info("HealthCheckCompleted", extra={"custom_dimensions": {"publicStorage": len(public_storage)}})\n | javascript\nconst { AzureMonitorExporter } = require("@azure/monitor-opentelemetry-exporter");\nconst exporter = new AzureMonitorExporter({ connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING });\n// ... use OpenTelemetry SDK to push metrics/logs\n |
| Retry with Exponential Backoff | Azure APIs can throttle; retry improves resilience. | python\nfrom azure.core.pipeline.policies import RetryPolicy\nretry_policy = RetryPolicy(total=5, backoff_factor=0.8)\n# pass to client constructor if needed\n | javascript\n// Azure SDK JS already includes built‑in retry (configurable via retryOptions)\nconst client = new ResourceManagementClient(credential, subId, { retryOptions: { maxRetries: 4 } });\n |
| Role‑Based Access Control (RBAC) Least Privilege | Limits blast radius if credentials are leaked. | Assign only Reader + Security Reader + Key Vault Secrets User to the SP. | Same – assign via Azure Portal/CLI. |
| Policy as Code | Enforces standards (e.g., no public storage, MFA required) automatically. | Use Azure Policy definitions exported as JSON and deploy via ARM/Bicep/Terraform. | Same – use Azure CLI az policy assignment create. |
| Automated Remediation (Logic Apps / Azure Functions) | When a detection fires, auto‑remediate (e.g., disable public blob access). | Trigger via Azure Monitor Action Group → Function that calls storage_client.storage_accounts.update. | Same – use JavaScript Function. |
<a name="troubleshooting"></a>
| Symptom | Likely Cause | Fix |
|---|---|---|
CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token | No valid auth method (env vars, managed identity, Azure CLI) | Ensure one of: <br>• AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET set or <br>• Run inside Azure with Managed Identity <br>• az login performed and default subscription set |
403 Forbidden when calling Microsoft Graph | Missing Graph permission or insufficient admin consent | Grant the app IdentityRiskEvent.Read.All (or IdentityRiskyUser.Read.All) via Azure Portal > App Registration > API Permissions > Grant admin consent. |
404 ResourceNotFound for storage account list | Using wrong subscription ID or lacking Reader role | Double‑check AZURE_SUBSCRIPTION_ID and that the SP has Reader on that subscription. |
TooManyRequests (HTTP 429) from Azure SDK | Throttling due to high call frequency | Enable built‑in retry (retry_options) or add a sleep/retry loop; consider batching requests or using Azure Resource Graph for bulk queries. |
KeyVaultError: SecretNotFound | Secret name typo or missing access policy | Verify secret name and that the SP (or Managed Identity) has Get secret permission on the Key Vault (az keyvault set-policy). |
ModuleNotFoundError: No module named 'msgraph' (Python) | Package not installed | pip install msgraph-core (or pip install azure-graphrbac for older Graph RBAC). |
Cannot find module '@azure/identity' (JS) | Node modules not installed | Run npm install. Ensure you’re using the correct project folder. |
The subscription is not registered to use namespace 'Microsoft.Insights' | Missing resource provider registration | Run: az provider register --namespace Microsoft.Insights (repeat for any missing providers like Microsoft.Authorization, Microsoft.KeyVault). |
Unauthorized when calling Azure Policy Insights | SP lacks Policy Insights Reader or Policy Metadata Reader | Assign appropriate role: az role assignment create --assignee <sp-object-id> --role "Policy Insights Reader" --scope /subscriptions/<sub-id> |
General debugging tip:
Enable Azure SDK logging to see raw HTTP requests/responses.
Python:
import logging, sys
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
JavaScript/TypeScript:
process.env.AZURE_LOG_LEVEL = "info"; // or "verbose"
<a name="production-checklist"></a>
| ✅ Item | Description | How to Verify |
|---|---|---|
| Least‑Privilege Service Principal | Only the permissions listed in Prerequisites. | Review role assignments: az role assignment list --assignee <sp-object-id> |
| Managed Identity Preferred | Use |
Source: Dark Reading
Follow ICARAX for more AI insights and tutorials.
