Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
# NEVER commit .env to source control!
# =============================================================================

# Azure Data Explorer
# Backend choice: 'ADX' or 'LogAnalytics'
ANALYTICS_BACKEND=ADX

# ---- ADX Backend ----
ADX_CLUSTER_URI=https://yourcluster.region.kusto.windows.net
ADX_DATABASE=IntuneAnalytics

# Azure AD / Entra ID (for local development with Service Principal)
TENANT_ID=your-tenant-id
CLIENT_ID=your-app-registration-client-id
CLIENT_SECRET=your-app-registration-client-secret
# ---- Log Analytics Backend ----
# LOG_ANALYTICS_DCE=https://your-dce.uksouth.ingest.monitor.azure.com
# LOG_ANALYTICS_DCR_ID=dcr-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# LOG_ANALYTICS_WORKSPACE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# Optional: Override function behavior
# SKIP_COMPLIANCE_STATES=false
# SKIP_ENDPOINT_ANALYTICS=false
# MAX_DEVICES=0
# DRY_RUN=false
# ---- App Registration Credentials ----
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-app-registration-client-id
AZURE_CLIENT_SECRET=your-app-registration-client-secret
223 changes: 53 additions & 170 deletions deployment/adx/main.bicep
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// ============================================================================
// Intune Analytics Platform - ADX Backend
// ============================================================================
// This template deploys an Azure Function App with connection string auth
// for deployment storage (simpler setup, no role assignments needed).
// Deploys a Function App that exports Intune data to Azure Data Explorer.
//
// After deployment:
// 1. Add app registration credentials in Function App > Configuration:
// - AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET
// 2. Grant Graph API permissions to your app registration
// 3. Upload function code via Deployment Center
// ============================================================================

@description('Base name for all resources (max 11 characters, will be appended with unique suffix)')
@description('Base name for all resources (max 11 chars)')
@maxLength(11)
param baseName string

Expand All @@ -16,221 +21,99 @@ param location string = resourceGroup().location
param adxClusterUri string = ''

@description('Azure Data Explorer database name')
param adxDatabaseName string = 'IntuneAnalytics'
param adxDatabase string = 'IntuneAnalytics'

// ============================================================================
// Variables
// ============================================================================

var uniqueSuffix = uniqueString(resourceGroup().id)
var storageAccountName = toLower('${take(baseName, 11)}${take(uniqueSuffix, 13)}')
var functionAppName = '${baseName}-func-${uniqueSuffix}'
var appServicePlanName = '${baseName}-asp-${uniqueSuffix}'
var managedIdentityName = '${baseName}-mi-${uniqueSuffix}'
var suffix = uniqueString(resourceGroup().id)
var storageName = toLower('${take(baseName, 11)}${take(suffix, 13)}')
var funcName = '${baseName}-func-${suffix}'
var planName = '${baseName}-plan-${suffix}'

// ============================================================================
// User-Assigned Managed Identity (for Graph API access)
// Storage Account (required for Function App)
// ============================================================================

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: managedIdentityName
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
}

// ============================================================================
// Storage Account
// ============================================================================

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
defaultToOAuthAuthentication: true
}
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
parent: storageAccount
name: 'default'
}

resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
parent: blobService
name: 'deploymentpackage'
properties: {
publicAccess: 'None'
}
}

// ============================================================================
// Deployment Script: Copy function app code from GitHub to storage
// ============================================================================

resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: '${baseName}-deploy-code'
location: location
kind: 'AzureCLI'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
azCliVersion: '2.52.0'
timeout: 'PT10M'
retentionInterval: 'PT1H'
cleanupPreference: 'OnSuccess'
environmentVariables: [
{ name: 'STORAGE_ACCOUNT', value: storageAccount.name }
{ name: 'STORAGE_KEY', secureValue: storageAccount.listKeys().keys[0].value }
{ name: 'CONTAINER_NAME', value: 'deploymentpackage' }
{ name: 'ZIP_URL', value: 'https://github.com/JacobWLMS/IntuneReporting/releases/download/latest/released-package.zip' }
]
scriptContent: '''
set -e
echo "Starting deployment script..."
echo "Downloading from GitHub: $ZIP_URL"
curl -L -f -o /tmp/released-package.zip "$ZIP_URL"
echo "Download complete. File size: $(stat -c%s /tmp/released-package.zip) bytes"
echo "Uploading to storage account: $STORAGE_ACCOUNT"
az storage blob upload \
--account-name "$STORAGE_ACCOUNT" \
--account-key "$STORAGE_KEY" \
--container-name "$CONTAINER_NAME" \
--name "released-package.zip" \
--file /tmp/released-package.zip \
--overwrite
echo "Upload complete"
echo "{\"status\": \"success\", \"blobName\": \"released-package.zip\"}" > $AZ_SCRIPTS_OUTPUT_PATH
'''
}
dependsOn: [
deploymentContainer
]
}

// ============================================================================
// App Service Plan (Flex Consumption)
// ============================================================================

resource appServicePlan 'Microsoft.Web/serverfarms@2024-04-01' = {
name: appServicePlanName
resource plan 'Microsoft.Web/serverfarms@2024-04-01' = {
name: planName
location: location
sku: {
name: 'FC1'
tier: 'FlexConsumption'
}
sku: { name: 'FC1', tier: 'FlexConsumption' }
kind: 'functionapp'
properties: {
reserved: true
}
properties: { reserved: true }
}

// ============================================================================
// Function App
// ============================================================================

resource functionApp 'Microsoft.Web/sites@2024-04-01' = {
name: functionAppName
resource func 'Microsoft.Web/sites@2024-04-01' = {
name: funcName
location: location
kind: 'functionapp,linux'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
serverFarmId: appServicePlan.id
serverFarmId: plan.id
httpsOnly: true
functionAppConfig: {
deployment: {
storage: {
type: 'blobContainer'
value: '${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
authentication: {
type: 'StorageAccountConnectionString'
storageAccountConnectionStringName: 'DEPLOYMENT_STORAGE_CONNECTION_STRING'
}
}
}
scaleAndConcurrency: {
maximumInstanceCount: 100
maximumInstanceCount: 40
instanceMemoryMB: 2048
}
runtime: {
name: 'python'
version: '3.11'
}
runtime: { name: 'python', version: '3.11' }
}
siteConfig: {
appSettings: [
// Deployment storage connection string
{
name: 'DEPLOYMENT_STORAGE_CONNECTION_STRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
}
// AzureWebJobsStorage connection string
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
}
// Function runtime settings
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
value: 'true'
}
// Analytics backend configuration
{
name: 'ANALYTICS_BACKEND'
value: 'ADX'
}
{
name: 'ADX_CLUSTER_URI'
value: adxClusterUri
}
{
name: 'ADX_DATABASE'
value: adxDatabaseName
}
{
name: 'TENANT_ID'
value: subscription().tenantId
}
// Managed Identity client ID for Graph API calls
{
name: 'AZURE_CLIENT_ID'
value: managedIdentity.properties.clientId
}
{ name: 'AzureWebJobsStorage', value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' }
{ name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'true' }
{ name: 'ANALYTICS_BACKEND', value: 'ADX' }
{ name: 'ADX_CLUSTER_URI', value: adxClusterUri }
{ name: 'ADX_DATABASE', value: adxDatabase }
// Add these manually in portal after deployment:
// AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET
]
}
}
dependsOn: [
deploymentScript
]
}

// ============================================================================
// Outputs
// ============================================================================

output functionAppName string = functionApp.name
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
output managedIdentityObjectId string = managedIdentity.properties.principalId
output managedIdentityClientId string = managedIdentity.properties.clientId
output storageAccountName string = storageAccount.name
output functionAppName string = func.name
output functionAppUrl string = 'https://${func.properties.defaultHostName}'
output storageAccountName string = storage.name

output postDeploymentSteps string = '''
After deployment, complete these steps:

1. Add app registration credentials in Function App > Configuration > Application settings:
- AZURE_TENANT_ID = your tenant ID
- AZURE_CLIENT_ID = your app registration client ID
- AZURE_CLIENT_SECRET = your app registration client secret

2. Grant Graph API permissions to your app registration:
- DeviceManagementManagedDevices.Read.All
- DeviceManagementConfiguration.Read.All

3. Grant your app registration "Ingestor" role on the ADX database

output nextStep string = 'IMPORTANT: Run scripts/Grant-GraphPermissions.ps1 to grant Microsoft Graph API permissions to the Managed Identity.'
output grantPermissionsCommand string = '.\\scripts\\Grant-GraphPermissions.ps1 -ManagedIdentityObjectId "${managedIdentity.properties.principalId}"'
4. Upload function code via Deployment Center (ZIP deploy)
'''
7 changes: 5 additions & 2 deletions deployment/adx/main.bicepparam
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using 'main.bicep'

// Example parameter values - customize as needed
// Customize these values
param baseName = 'intune'
param location = 'uksouth'

// Optional: Set these if you already have an ADX cluster
// Otherwise leave empty and set after deployment
param adxClusterUri = ''
param adxDatabaseName = 'IntuneAnalytics'
param adxDatabase = 'IntuneAnalytics'
Loading