Technical Guide Β· Published by MBIC Β· 2025
A technical guide to service principals, API security, and compliance for automated Power BI deployments.
Prefer the PDF? Download the originalAutomating Power BI workflows introduces new security considerations beyond traditional BI deployments. This guide provides technical implementation details for securing automated workflows while maintaining compliance with HIPAA, SOC 2, and other regulatory frameworks.
Key topics covered: service principal authentication and authorization; API security and token management; webhook authentication and validation; row-level security in automated contexts; compliance requirements (HIPAA, SOC 2, ISO 27001); audit logging and monitoring; and network security and data protection.
Target audience: security architects designing BI automation systems, IT security teams evaluating automation proposals, compliance officers assessing risk, and data engineers implementing secure workflows.
Figures in this guide are benchmark-based directional estimates, and any implementation examples are realistic reference implementations drawn from common patterns β not descriptions of specific client engagements. Validate all controls and configurations against your own environment and policies.
Traditional Power BI security assumes interactive user sessions. Automation requires:
Azure AD App Registration β Service Principal β Power BI API Access
Pros:
Cons:
Dedicated User Account β Power BI Pro License β API Access
Pros:
Cons:
# PowerShell script to create app registration
# Login to Azure
Connect-AzAccount
# Create app registration
$appName = "PowerBI-Automation-Service"
$app = New-AzADApplication -DisplayName $appName
# Get application ID
$appId = $app.ApplicationId
Write-Host "Application ID: $appId"
# Create service principal
$sp = New-AzADServicePrincipal -ApplicationId $appId
# Get service principal object ID
$spObjectId = $sp.Id
Write-Host "Service Principal Object ID: $spObjectId"
Option A: Client Secret (Simpler)
# Create client secret (valid for 2 years)
$endDate = (Get-Date).AddYears(2)
$secret = New-AzADAppCredential `
-ObjectId $app.Id `
-EndDate $endDate
# IMPORTANT: Save this secret immediately - cannot retrieve later
$clientSecret = $secret.SecretText
Write-Host "Client Secret: $clientSecret"
# Store in Azure Key Vault (next section)
Option B: Certificate (More Secure)
# Create self-signed certificate
$cert = New-SelfSignedCertificate `
-Subject "CN=PowerBI-Automation" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-KeySpec Signature `
-KeyLength 2048 `
-KeyAlgorithm RSA `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(2)
# Export certificate
$certPath = "C:\Temp\PowerBI-Automation.pfx"
$certPassword = ConvertTo-SecureString -String "YourSecurePassword" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $certPath -Password $certPassword
# Upload to app registration
$certData = Get-Content $certPath -Encoding Byte
New-AzADAppCredential `
-ObjectId $app.Id `
-CertValue ([System.Convert]::ToBase64String($certData))
# Store certificate in Azure Key Vault
# Get Power BI service principal
$pbiServicePrincipal = Get-AzADServicePrincipal -Filter "AppId eq '00000009-0000-0000-c000-000000000000'"
# Grant required API permissions
# Permission: Dataset.ReadWrite.All
$datasetPermission = $pbiServicePrincipal.Oauth2Permission |
Where-Object { $_.Value -eq "Dataset.ReadWrite.All" }
# Add permission to app
Add-AzADAppPermission `
-ObjectId $app.Id `
-ApiId $pbiServicePrincipal.AppId `
-PermissionId $datasetPermission.Id `
-Type Scope
# Admin must consent in Azure Portal or via PowerShell
# Navigate to: Azure AD β App Registrations β [Your App] β API Permissions β Grant Admin Consent
# Add service principal to workspace
# Use Power BI REST API or Admin Portal
# REST API approach
$workspaceId = "your-workspace-id"
$headers = @{
"Authorization" = "Bearer $accessToken"
}
$body = @{
"identifier" = $spObjectId
"principalType" = "App"
"groupUserAccessRight" = "Admin" # or "Member", "Contributor", "Viewer"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" `
-Method Post `
-Headers $headers `
-Body $body `
-ContentType "application/json"
# Store client secret in Key Vault
$vaultName = "YourKeyVault"
$secretName = "PowerBI-Automation-ClientSecret"
Set-AzKeyVaultSecret `
-VaultName $vaultName `
-Name $secretName `
-SecretValue (ConvertTo-SecureString -String $clientSecret -AsPlainText -Force)
# Grant service principal access to Key Vault
Set-AzKeyVaultAccessPolicy `
-VaultName $vaultName `
-ObjectId $spObjectId `
-PermissionsToSecrets Get
# Action: Get secret from Azure Key Vault
HTTP
Method: GET
URI: https://@{keyVaultName}.vault.azure.net/secrets/@{secretName}?api-version=7.2
Authentication: Managed Identity
Parse JSON
Content: @{body('HTTP')}
Set variable: clientSecret
Value: @{body('Parse_JSON')?['value']}
// C# example for Azure Function using certificate auth
using Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;
public class PowerBIAuth
{
private readonly string tenantId = "your-tenant-id";
private readonly string clientId = "your-client-id";
private readonly string certificateThumbprint = "cert-thumbprint";
public async Task<string> GetAccessToken()
{
// Load certificate from store
X509Certificate2 cert = GetCertificate(certificateThumbprint);
// Build confidential client application
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(cert)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
// Get token
var scopes = new[] { "https://analysis.windows.net/powerbi/api/.default" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
return result.AccessToken;
}
private X509Certificate2 GetCertificate(string thumbprint)
{
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(
X509FindType.FindByThumbprint,
thumbprint,
validOnly: false
);
if (certs.Count == 0)
throw new Exception("Certificate not found");
return certs[0];
}
}
# Automated secret rotation flow
# Runs monthly, rotates if secret expires in <30 days
Trigger: Recurrence
Frequency: Monthly
Day: 1
# Check secret expiration
HTTP: Get app registration details
URI: https://graph.microsoft.com/v1.0/applications/@{appObjectId}
Authentication: Managed Identity
Parse JSON: App details
# Calculate days until expiration
Set variable: daysUntilExpiry
Value: @{div(sub(ticks(item()?['passwordCredentials'][0]['endDateTime']), ticks(utcnow())), 864000000000)}
# If expiring soon, create new secret
Condition: daysUntilExpiry < 30
Then:
# Create new secret
HTTP: Create new credential
Method: POST
URI: https://graph.microsoft.com/v1.0/applications/@{appObjectId}/addPassword
Body: {
"passwordCredential": {
"displayName": "AutoRotated-@{utcnow()}",
"endDateTime": "@{addYears(utcnow(), 1)}"
}
}
# Store new secret in Key Vault
HTTP: Update Key Vault secret
[Store new secret]
# Wait 24 hours for propagation
Delay: 1 day
# Delete old secret
HTTP: Remove old credential
[Delete previous credential]
# Notify security team
Send email: Secret rotation completed
// Don't do this (insecure):
var token = GetToken();
// Token stored in memory, potentially logs, error messages
// Do this (secure):
public class SecureTokenCache
{
private string _token;
private DateTime _expiry;
private readonly object _lock = new object();
public string GetToken()
{
lock (_lock)
{
if (_token == null || DateTime.UtcNow >= _expiry)
{
_token = RequestNewToken();
_expiry = DateTime.UtcNow.AddMinutes(55); // Refresh 5 min early
}
return _token;
}
}
public void InvalidateToken()
{
lock (_lock)
{
_token = null;
_expiry = DateTime.MinValue;
}
}
}
# Power Automate example - CORRECT
HTTP
URI: https://api.powerbi.com/v1.0/myorg/datasets
Method: GET
# INCORRECT (never use HTTP for Power BI API)
# URI: http://api.powerbi.com/... β
public async Task<Dataset> GetDataset(string datasetId)
{
var response = await _httpClient.GetAsync(
$"https://api.powerbi.com/v1.0/myorg/datasets/{datasetId}"
);
// Validate status code
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
_logger.LogError($"API error: {error}");
throw new PowerBIApiException($"Failed to get dataset: {response.StatusCode}");
}
// Validate content type
if (response.Content.Headers.ContentType?.MediaType != "application/json")
{
throw new PowerBIApiException("Unexpected content type");
}
// Parse with validation
var content = await response.Content.ReadAsStringAsync();
// Validate JSON structure before deserialization
try
{
var dataset = JsonConvert.DeserializeObject<Dataset>(content);
// Validate required fields
if (string.IsNullOrEmpty(dataset?.Id))
throw new PowerBIApiException("Invalid dataset structure");
return dataset;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to parse API response");
throw new PowerBIApiException("Invalid API response format");
}
}
public class RateLimitedPowerBIClient
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); // 5 concurrent requests
private readonly Queue<DateTime> _requestTimes = new Queue<DateTime>();
private readonly int _maxRequestsPerMinute = 60;
public async Task<T> ExecuteWithRateLimit<T>(Func<Task<T>> apiCall)
{
await _semaphore.WaitAsync();
try
{
// Check rate limit
var now = DateTime.UtcNow;
var oneMinuteAgo = now.AddMinutes(-1);
// Remove old requests from queue
while (_requestTimes.Count > 0 && _requestTimes.Peek() < oneMinuteAgo)
{
_requestTimes.Dequeue();
}
// If at limit, wait
if (_requestTimes.Count >= _maxRequestsPerMinute)
{
var oldestRequest = _requestTimes.Peek();
var waitTime = oldestRequest.AddMinutes(1) - now;
if (waitTime > TimeSpan.Zero)
{
await Task.Delay(waitTime);
}
}
// Execute request
_requestTimes.Enqueue(DateTime.UtcNow);
return await apiCall();
}
finally
{
_semaphore.Release();
}
}
}
β Don't: Grant Workspace Admin to all automation services
β Do: Grant specific permissions based on need
Examples:
- Report export automation: Viewer permission only
- Dataset refresh automation: Contributor permission
- Workspace management: Admin permission (rarely needed)
# Audit service principal permissions across workspaces
$servicePrincipalId = "your-sp-object-id"
$workspaces = Get-PowerBIWorkspace -Scope Organization
foreach ($workspace in $workspaces) {
$users = Get-PowerBIWorkspaceUser -WorkspaceId $workspace.Id
$spAccess = $users | Where-Object { $_.Identifier -eq $servicePrincipalId }
if ($spAccess) {
Write-Host "Workspace: $($workspace.Name)"
Write-Host " Access: $($spAccess.AccessRight)"
Write-Host " Review: Is Admin access required? β " -ForegroundColor Yellow
}
}
Webhooks expose HTTP endpoints that trigger automation. Without proper security:
# Power Automate - When HTTP Request Received trigger
When HTTP request received
Method: POST
URL: [Auto-generated]
# Validate shared secret in header
Condition: Check authorization header
Headers: @{triggerOutputs()['headers']}
IF headers['X-Webhook-Secret'] equals '@{parameters('SharedSecret')}'
THEN
# Process request
[Your automation logic]
ELSE
# Reject request
Response
Status code: 401
Body: "Unauthorized"
Terminate
Sender configuration:
// When calling webhook, include secret
var client = new HttpClient();
var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
client.DefaultRequestHeaders.Add("X-Webhook-Secret", secret);
var payload = new { data = "your-data" };
var content = new StringContent(
JsonConvert.SerializeObject(payload),
Encoding.UTF8,
"application/json"
);
await client.PostAsync(webhookUrl, content);
# More secure - validates both authenticity and integrity
When HTTP request received
Method: POST
# Parse request
Set variable: requestBody
Value: @{triggerBody()}
Set variable: receivedSignature
Value: @{triggerOutputs()['headers']['X-Hub-Signature-256']}
# Calculate expected signature
# (Azure Function for complex crypto operations)
HTTP: Validate HMAC
Method: POST
URI: https://yourfunction.azurewebsites.net/api/ValidateHMAC
Body: {
"payload": "@{body('trigger')}",
"signature": "@{variables('receivedSignature')}",
"secret": "@{parameters('WebhookSecret')}"
}
Parse JSON: Validation result
Condition: Is signature valid?
IF body('Parse_JSON')?['isValid'] = true
THEN
# Process webhook
ELSE
# Log suspicious activity
# Return 401
HMAC validation function:
[FunctionName("ValidateHMAC")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
ILogger log)
{
var requestBody = new StreamReader(req.Body).ReadToEnd();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string payload = data?.payload;
string receivedSignature = data?.signature;
string secret = data?.secret;
// Calculate expected signature
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = "sha256=" + BitConverter.ToString(hash)
.Replace("-", "").ToLower();
bool isValid = receivedSignature == expectedSignature;
if (!isValid)
{
log.LogWarning($"Invalid HMAC signature from {req.HttpContext.Connection.RemoteIpAddress}");
}
return new OkObjectResult(new { isValid = isValid });
}
}
# For webhooks that need to call back to source system
When HTTP request received
Method: POST
# Extract bearer token
Set variable: bearerToken
Value: @{replace(triggerOutputs()['headers']['Authorization'], 'Bearer ', '')}
# Validate token with Azure AD
HTTP: Validate token
Method: POST
URI: https://login.microsoftonline.com/@{tenantId}/oauth2/v2.0/token
Body: [Token validation request]
Condition: Is token valid?
IF valid
THEN process
ELSE reject (401)
// host.json configuration
{
"extensions": {
"http": {
"routePrefix": "api",
"ipSecurityRestrictions": [
{
"ipAddress": "203.0.113.0/24",
"action": "Allow",
"priority": 100,
"name": "AllowPowerBI"
},
{
"ipAddress": "0.0.0.0/0",
"action": "Deny",
"priority": 2147483647,
"name": "DenyAll"
}
]
}
}
}
# Define expected schema
Initialize variable: expectedSchema
Value: {
"type": "object",
"required": ["dataset_id", "action", "timestamp"],
"properties": {
"dataset_id": {"type": "string", "pattern": "^[0-9a-f-]{36}$"},
"action": {"type": "string", "enum": ["refresh", "export"]},
"timestamp": {"type": "string"}
}
}
# Validate incoming request
HTTP: Validate JSON schema
[Call validation function]
Condition: Schema valid?
IF valid
THEN proceed
ELSE
Response: 400 Bad Request
// Azure Function with rate limiting
[FunctionName("WebhookHandler")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[CosmosDB(
databaseName: "RateLimitDB",
collectionName: "RequestLog",
ConnectionStringSetting = "CosmosDBConnection"
)] IAsyncCollector<RequestLog> requestLog,
ILogger log)
{
var clientIP = req.HttpContext.Connection.RemoteIpAddress.ToString();
// Check rate limit (max 10 requests per minute per IP)
var rateLimiter = new RateLimiter(requestLog);
if (!await rateLimiter.AllowRequest(clientIP, maxRequests: 10, windowMinutes: 1))
{
log.LogWarning($"Rate limit exceeded for {clientIP}");
return new StatusCodeResult(429); // Too Many Requests
}
// Process webhook
// ...
}
Automated workflows often run under service principal identity, which may have access to all data. How do you ensure:
Problem: Service principals bypass RLS by default when calling Power BI API.
Solution: Implement "effective identity" in API calls.
public async Task<Stream> ExportReportForUser(
string reportId,
string userPrincipalName)
{
var exportRequest = new ExportReportRequest
{
Format = FileFormat.PDF,
PowerBIReportConfiguration = new PowerBIReportExportConfiguration
{
// Apply RLS for specific user
Identities = new List<EffectiveIdentity>
{
new EffectiveIdentity
{
Username = userPrincipalName,
Roles = new List<string> { "SalesRegion" }, // RLS role
Datasets = new List<string> { datasetId }
}
}
}
};
// This export will respect RLS for the specified user
var export = await _powerBIClient.Reports.ExportToFileInGroupAsync(
workspaceId,
reportId,
exportRequest
);
// Wait for completion and download
return await WaitForExportAndDownload(export.Id);
}
// Define RLS role: SalesRegion
[Region] = USERPRINCIPALNAME()
// For service automation, USERNAME() will be the service principal
// Use effective identity to impersonate actual user
Scenario: Send automated reports to multiple users, each seeing only their data.
# Get list of users and their data scope
Get items (SharePoint)
List: UserRegions
Columns: Email, Region
# For each user
Apply to each:
# Export report with RLS for this user
HTTP: Export Power BI report
Method: POST
URI: https://api.powerbi.com/v1.0/myorg/groups/@{workspaceId}/reports/@{reportId}/ExportTo
Headers:
Authorization: Bearer @{accessToken}
Body:
{
"format": "PDF",
"powerBIReportConfiguration": {
"identities": [
{
"username": "@{currentUser.Email}",
"roles": ["SalesRegion"],
"datasets": ["@{datasetId}"]
}
]
}
}
# Wait for export completion
# Download file
# Email to user
Send email
To: @{currentUser.Email}
Attachments: [Report PDF - only their data]
Best practice: Create separate service principals for different data access levels.
Service Principal: SP-PowerBI-Executive
Access: All data
Use: Executive reports
Service Principal: SP-PowerBI-Regional
Access: Regional data (via RLS)
Use: Regional manager reports
Service Principal: SP-PowerBI-Public
Access: Public dashboards only
Use: External sharing
# Log when automation accesses data without RLS
Condition: Is RLS applied?
IF effectiveIdentity is null
THEN
# Log RLS bypass
Add row to audit log
ServicePrincipal: @{servicePrincipalId}
Report: @{reportId}
RLS Applied: No
Justification: [Required field]
Timestamp: @{utcnow()}
# Alert security team for review
IF datasetSensitivity = "High"
Send alert: RLS bypass on sensitive data
// Enforce TLS 1.2
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// Or in .NET Core / .NET 5+ (automatic)
// Just ensure server supports TLS 1.2+
# Test Power BI API TLS support
$uri = "https://api.powerbi.com/v1.0/myorg/datasets"
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$response = Invoke-WebRequest -Uri $uri -UseBasicParsing
Write-Host "β TLS 1.2 connection successful" -ForegroundColor Green
}
catch {
Write-Host "β TLS connection failed: $($_.Exception.Message)" -ForegroundColor Red
}
Power BI data:
// Encrypt exported reports before storing
using System.Security.Cryptography;
public byte[] EncryptReport(byte[] reportData, string encryptionKey)
{
using (Aes aes = Aes.Create())
{
aes.Key = Convert.FromBase64String(encryptionKey);
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream())
{
// Write IV first (needed for decryption)
ms.Write(aes.IV, 0, aes.IV.Length);
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(reportData, 0, reportData.Length);
}
return ms.ToArray();
}
}
}
public byte[] DecryptReport(byte[] encryptedData, string encryptionKey)
{
using (Aes aes = Aes.Create())
{
aes.Key = Convert.FromBase64String(encryptionKey);
// Extract IV from beginning
var iv = new byte[aes.IV.Length];
Array.Copy(encryptedData, 0, iv, 0, iv.Length);
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor())
using (var ms = new MemoryStream(encryptedData, iv.Length, encryptedData.Length - iv.Length))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var output = new MemoryStream())
{
cs.CopyTo(output);
return output.ToArray();
}
}
}
// β DON'T
_logger.LogInformation($"Processing report for user: {userEmail}");
// β DO
var hashedEmail = HashPII(userEmail);
_logger.LogInformation($"Processing report for user: {hashedEmail}");
// Helper function
private string HashPII(string value)
{
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value));
return Convert.ToBase64String(hash).Substring(0, 8); // First 8 chars for identification
}
}
try
{
await ProcessAutomation(userEmail, reportId);
}
catch (Exception ex)
{
// β DON'T - May expose PII
_logger.LogError($"Failed for {userEmail}: {ex.Message}");
// β DO - Sanitized
var sanitized = SanitizeErrorMessage(ex.Message);
_logger.LogError($"Failed for user {HashPII(userEmail)}: {sanitized}");
}
private string SanitizeErrorMessage(string message)
{
// Remove email addresses
message = Regex.Replace(message, @"\b[\w\.-]+@[\w\.-]+\.\w+\b", "[EMAIL_REDACTED]");
// Remove GUIDs (dataset IDs, etc.)
message = Regex.Replace(message, @"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", "[ID_REDACTED]");
// Remove file paths
message = Regex.Replace(message, @"[A-Z]:\\[\w\\]+", "[PATH_REDACTED]");
return message;
}
# Check export approval before allowing
When HTTP request received (webhook for export request)
Parse: Request details
# Check if user is authorized
HTTP: Query authorization service
User: @{requestedBy}
Dataset: @{datasetId}
Action: Export
Condition: Is authorized?
IF authorized = false
THEN
# Log unauthorized attempt
Add row to security log
User: @{requestedBy}
Action: Export (unauthorized)
Dataset: @{datasetId}
Timestamp: @{utcnow()}
# Alert security team
Send email: Unauthorized export attempt
# Deny request
Response: 403 Forbidden
Terminate
ELSE
# Proceed with export
[Export logic]
# Log authorized export
Add row to audit log
Key requirements:
βββββββββββββββββββββββββββββββββββββββ
β Power BI Service (Microsoft) β
β β BAA in place β
β β HIPAA-compliant infrastructure β
βββββββββββββββββββββββββββββββββββββββ
β TLS 1.2+ encrypted
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Azure Functions (Your automation) β
β β Deployed in HIPAA-compliant region β
β β Managed Identity authentication β
β β Private networking (optional) β
βββββββββββββββββββββββββββββββββββββββ
β Encrypted
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Target Systems (EMR, Databases) β
β β HL7/FHIR interfaces β
β β Audit logging enabled β
βββββββββββββββββββββββββββββββββββββββ
[FunctionName("ProcessPatientReport")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[CosmosDB(
databaseName: "HIPAACompliance",
collectionName: "PHIAccessLog",
ConnectionStringSetting = "CosmosDBConnection"
)] IAsyncCollector<PHIAccessLog> auditLog,
ILogger log)
{
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string userId = data?.userId;
string patientId = data?.patientId;
string reportId = data?.reportId;
// Log PHI access BEFORE processing
await auditLog.AddAsync(new PHIAccessLog
{
UserId = userId,
PatientId = patientId, // Hash in production
ReportId = reportId,
Action = "View Report",
Timestamp = DateTime.UtcNow,
IPAddress = req.HttpContext.Connection.RemoteIpAddress.ToString(),
UserAgent = req.Headers["User-Agent"].ToString(),
Authorized = false // Will update after auth check
});
// Verify authorization
var isAuthorized = await CheckHIPAAAuthorization(userId, patientId);
if (!isAuthorized)
{
log.LogWarning($"Unauthorized PHI access attempt: User {userId}, Patient {patientId}");
return new UnauthorizedResult();
}
// Update audit log
// [Update authorized flag to true]
// Process report
// ...
}
// Power BI measure - only show summary, not details
PatientCount =
IF(
HASONEVALUE(Providers[ProviderID]),
COUNTROWS(Patients),
BLANK() // Don't show counts at aggregate level (HIPAA minimum necessary)
)
// Alternative: Show counts only if >10 patients (de-identification threshold)
PatientCount =
VAR Count = COUNTROWS(Patients)
RETURN
IF(Count < 10, BLANK(), Count)
# Monitor for potential HIPAA breaches
# Trigger when unusual access patterns detected
When audit log entry created
Collection: PHIAccessLog
# Analyze access pattern
HTTP: Check for anomalies
Endpoint: /api/DetectHIPAABreach
Body: @{triggerBody()}
Parse JSON: Analysis result
Condition: Potential breach detected?
IF potentialBreach = true
THEN
# Immediate actions required by HIPAA
# 1. Notify Privacy Officer (within 60 min)
Send email (priority)
To: privacy-officer@hospital.org
Subject: URGENT: Potential HIPAA Breach Detected
Body: [Details]
# 2. Secure the data
HTTP: Revoke access
[Disable service principal temporarily]
# 3. Begin investigation tracking
Create work item
Type: HIPAA Breach Investigation
Priority: P0
Due: 60 days (HIPAA notification deadline)
# 4. Log incident
Add row to breach log
# Before exporting data for analytics, de-identify
Get rows (Power BI)
Dataset: Patient Data
Filter: Last 90 days
# Call de-identification service
HTTP: De-identify dataset
Method: POST
URI: https://your-deidentification-service.azurewebsites.net/api/deidentify
Body: {
"data": @{body('Get_rows')},
"method": "k-anonymity",
"k": 5
}
# Now safe to use for analytics without PHI restrictions
Security (all SOC 2 reports):
Availability:
Confidentiality:
# Automated access review process
Trigger: Recurrence
Frequency: Quarterly
# Get all service principals with Power BI access
HTTP: List service principals
URI: https://graph.microsoft.com/v1.0/servicePrincipals
# For each SP, document access
Apply to each:
# Get Power BI workspace access
HTTP: Get workspace permissions
# Get Azure resource access
HTTP: Get Azure role assignments
# Compile access report
Compose: Access summary
Service Principal: @{currentSP.displayName}
Power BI Workspaces: @{workspaceList}
Access Level: @{accessLevel}
Last Modified: @{lastModified}
Business Justification: [From metadata]
Approved By: [From approval record]
# Generate quarterly access review report
Create Excel file
Filename: ServicePrincipal_Access_Review_Q@{quarter}_@{year}.xlsx
Sheets:
- Summary
- Detailed Access
- Changes This Quarter
# Send to security team for review
Send email
To: security-team@company.com
Subject: Q@{quarter} Access Review Required
Attachments: [Access review spreadsheet]
SOC 2 requires tracking all changes to automated systems:
# Before deploying any automation changes
# 1. Create change request
HTTP: Create change ticket
System: ServiceNow
Body: {
"short_description": "Update Power BI automation workflow",
"description": "@{changeDescription}",
"risk_level": "@{riskLevel}",
"implementation_plan": "@{implementationPlan}",
"rollback_plan": "@{rollbackPlan}",
"testing_evidence": "@{testingResults}"
}
# 2. Get approvals (SOC 2 requirement)
Post adaptive card (Teams)
Channel: Change Approval Board
Card:
Title: Change Request: @{changeId}
Details: @{changeDescription}
Impact: @{estimatedImpact}
Actions:
- Approve
- Reject
- Request More Info
# 3. Wait for approval
Wait for approval
Condition: Approved?
IF approved = true
THEN
# Implement change
[Deployment logic]
# Document implementation
Update change ticket
Status: Implemented
Implementation Date: @{utcnow()}
Implemented By: @{implementer}
ELSE
# Log rejection
Update change ticket
Status: Rejected
Reason: @{rejectionReason}
# Continuous monitoring for SOC 2 availability criteria
Trigger: Recurrence
Frequency: Every 5 minutes
# Check automation health
HTTP: Check automation endpoints
Endpoints:
- Power BI API
- Azure Functions
- Key Vault
- Monitoring systems
Parse responses
# Calculate uptime metrics
Compose: Health metrics
Power BI API: @{pbiStatus}
Azure Functions: @{functionsStatus}
Overall Health: @{overallHealth}
Response Time: @{avgResponseTime}
# If any system down
Condition: Health < 100%
THEN
# Alert incident response team
Send alert (PagerDuty)
Severity: High
Description: Automation system degraded
# Log incident for SOC 2 audit trail
Add row to incident log
Incident Type: System Availability
Affected Systems: @{affectedSystems}
Start Time: @{utcnow()}
Detection Method: Automated Monitoring
# Begin incident response
Create incident ticket
# SOC 2 requires defined retention periods
# Audit logs: Retain 1 year minimum
# Access logs: Retain 90 days minimum
# Change records: Retain 3 years
# Automated retention enforcement
Trigger: Recurrence
Frequency: Daily
Time: 02:00
# Archive old audit logs
Get rows (Cosmos DB)
Query:
SELECT * FROM AuditLogs
WHERE timestamp < '@{addDays(utcnow(), -365)}'
AND archived = false
Apply to each old log:
# Move to archive storage
Create blob (Azure Storage Archive Tier)
Container: audit-archive
Path: /@{year}/@{month}/@{logId}.json
Content: @{currentLog}
# Mark as archived
Update document
Archived: true
# Delete logs older than retention period
Get rows (Cosmos DB)
Query:
SELECT * FROM AuditLogs
WHERE timestamp < '@{addDays(utcnow(), -1095)}' # 3 years
AND archived = true
Apply to each expired log:
Delete document
For maximum security, use Azure Private Link:
ββββββββββββββββ
β Power BI β
β (Private Link)β
ββββββββββββββββ
β Private connection
β (No internet exposure)
βΌ
ββββββββββββββββ
β Azure VNet β
β ββββββββββββ β
β β Functionsβ β
β ββββββββββββ β
ββββββββββββββββ
# Create private endpoint for Power BI (Premium only)
# Requires Power BI Premium Per User or Premium capacity
$resourceGroupName = "rg-powerbi-automation"
$vnetName = "vnet-automation"
$subnetName = "subnet-private-endpoints"
$privateEndpointName = "pe-powerbi"
# Create private endpoint
New-AzPrivateEndpoint `
-ResourceGroupName $resourceGroupName `
-Name $privateEndpointName `
-Location "East US" `
-Subnet (Get-AzVirtualNetworkSubnetConfig -Name $subnetName -VirtualNetwork (Get-AzVirtualNetwork -Name $vnetName)) `
-PrivateLinkServiceConnection (New-AzPrivateLinkServiceConnection `
-Name "powerbi-connection" `
-PrivateLinkServiceId "/subscriptions/{subscription-id}/providers/Microsoft.PowerBI/privateLinkServicesForPowerBI" `
-GroupId "tenant")
// Function App configuration
{
"ipSecurityRestrictions": [
{
"ipAddress": "Power BI IP Range",
"action": "Allow",
"priority": 100,
"name": "AllowPowerBI"
},
{
"vnetSubnetResourceId": "/subscriptions/{id}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet}",
"action": "Allow",
"priority": 200,
"name": "AllowVNet"
},
{
"ipAddress": "0.0.0.0/0",
"action": "Deny",
"priority": 2147483647,
"name": "DenyAll"
}
]
}
When automation accesses databases:
-- Azure SQL Database firewall rules
-- Only allow Azure Functions and Power BI
-- Add Azure Functions outbound IP
EXEC sp_set_firewall_rule N'AllowAzureFunctions', '52.168.112.0', '52.168.112.255';
-- Allow Azure services (for Power BI)
EXEC sp_set_firewall_rule N'AllowAzureServices', '0.0.0.0', '0.0.0.0';
-- Deny all other traffic (implicit)
What to log:
{
"timestamp": "2025-01-15T14:30:00Z",
"event_type": "api_call",
"user_identity": "sp-powerbi-automation@tenant.com",
"source_ip": "52.168.112.45",
"action": "dataset.refresh",
"resource": {
"type": "dataset",
"id": "abc123-dataset-id",
"workspace": "xyz789-workspace-id"
},
"result": "success",
"duration_ms": 1234,
"correlation_id": "req-456def",
"metadata": {
"triggered_by": "power_automate",
"workflow_name": "Daily Sales Refresh",
"workflow_run_id": "run-789ghi"
}
}
// Azure Function with Application Insights
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
public class SecureAutomationFunction
{
private readonly TelemetryClient _telemetry;
public SecureAutomationFunction(TelemetryClient telemetry)
{
_telemetry = telemetry;
}
[FunctionName("ProcessAutomation")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
ILogger log)
{
var operation = _telemetry.StartOperation<RequestTelemetry>("ProcessAutomation");
try
{
// Parse request
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonConvert.DeserializeObject<AutomationRequest>(requestBody);
// Log authentication
_telemetry.TrackEvent("Authentication", new Dictionary<string, string>
{
{ "user_id", data.UserId },
{ "auth_method", "service_principal" },
{ "source_ip", req.HttpContext.Connection.RemoteIpAddress.ToString() }
});
// Verify authorization
var isAuthorized = await CheckAuthorization(data.UserId, data.ResourceId);
// Log authorization decision
_telemetry.TrackEvent("Authorization", new Dictionary<string, string>
{
{ "user_id", data.UserId },
{ "resource_id", data.ResourceId },
{ "action", data.Action },
{ "result", isAuthorized ? "allowed" : "denied" }
});
if (!isAuthorized)
{
operation.Telemetry.ResponseCode = "403";
operation.Telemetry.Success = false;
return new ForbiddenResult();
}
// Process automation
var result = await ExecuteAutomation(data);
// Log data access
_telemetry.TrackEvent("DataAccess", new Dictionary<string, string>
{
{ "user_id", data.UserId },
{ "dataset_id", data.ResourceId },
{ "rows_accessed", result.RowCount.ToString() },
{ "sensitivity", result.DataClassification }
});
operation.Telemetry.ResponseCode = "200";
operation.Telemetry.Success = true;
return new OkObjectResult(result);
}
catch (Exception ex)
{
_telemetry.TrackException(ex);
operation.Telemetry.Success = false;
throw;
}
finally
{
_telemetry.StopOperation(operation);
}
}
}
# Get Power BI activity logs
# Requires Power BI Admin
Connect-PowerBIServiceAccount
# Get activities for last 30 days
$activities = Get-PowerBIActivityEvent `
-StartDateTime (Get-Date).AddDays(-30) `
-EndDateTime (Get-Date) `
-ActivityType 'ViewReport','RefreshDataset','ExportReport'
# Filter for automation-related activities
$automationActivities = $activities |
Where-Object { $_.User -like "*sp-powerbi*" }
# Export to CSV for audit
$automationActivities | Export-Csv -Path "PowerBI_Automation_Audit_$(Get-Date -Format 'yyyyMMdd').csv"
# Runs hourly, checks for anomalies
Trigger: Recurrence
Frequency: Hourly
# Get automation activity from last hour
HTTP: Query Application Insights
Query:
"""
requests
| where timestamp > ago(1h)
| where cloud_RoleName == "PowerBI-Automation"
| summarize
count(),
avg(duration),
dcount(user_Id),
percentile(duration, 95)
by bin(timestamp, 5m)
"""
Parse JSON: Activity data
# Compare to baseline
HTTP: Check for anomalies
Endpoint: /api/DetectAnomalies
Body: {
"current_data": @{body('Parse_JSON')},
"baseline_period": "30_days"
}
Parse JSON: Anomaly analysis
Condition: Anomalies detected?
IF hasAnomalies = true
THEN
# Alert security team
Post message (Teams)
Channel: Security Operations
Message:
"β Anomalous automation activity detected
Pattern: @{anomalyType}
Severity: @{severity}
Details: @{anomalyDetails}
Actions recommended:
1. Review audit logs
2. Check for unauthorized access
3. Verify automation integrity"
# Create incident
HTTP: Create security incident
Priority: @{severity}
# Alert on failed auth attempts
When event written to Application Insights
Event type: Authentication
Condition: Result = "failed"
THEN
# Increment failure counter
Increment variable: failureCount
# If multiple failures
Condition: failureCount > 5
THEN
# Potential attack - disable service principal
HTTP: Disable service principal
URI: https://graph.microsoft.com/v1.0/applications/@{appId}
Method: PATCH
Body: {
"accountEnabled": false
}
# Alert security team
Send email (priority)
To: security-team@company.com
Subject: SECURITY: Service principal disabled due to failed auth
# Create incident
Create incident ticket
Type: Security Event
Priority: P1
# Monitor for unusual data export volumes
When Power BI export completed
Get export details
Size: @{exportSizeBytes}
User: @{userId}
Dataset: @{datasetId}
Format: @{exportFormat}
# Calculate normal export size for this dataset
HTTP: Get baseline metrics
Dataset: @{datasetId}
Metric: average_export_size
Parse JSON: Baseline
# Compare
Condition: Export size > (baseline * 3)
THEN
# Potential data exfiltration
# Log suspicious activity
Add row to security log
Event: Large Export
Size: @{exportSizeBytes}
Baseline: @{baseline}
Ratio: @{ratio}
User: @{userId}
# Alert SOC
Send alert: Potential data exfiltration attempt
# Require manual approval for export
Post adaptive card (Teams)
To: Data Protection Officer
Card:
Title: Large Export Requires Approval
Size: @{formatBytes(exportSizeBytes)}
Normal Size: @{formatBytes(baseline)}
User: @{userId}
Actions:
- Approve and Allow
- Deny and Investigate
When security alert triggered
# Capture current state
Get snapshot:
- Active sessions
- Recent API calls
- Current permissions
- Audit logs (last 24h)
# Create incident ticket
HTTP: Create incident
System: ServiceNow
Type: Security Incident
Severity: @{alertSeverity}
Description: @{alertDetails}
Snapshot: @{systemSnapshot}
# Notify SOC
Send priority notification
# Requires human approval for severe actions
Condition: Severity = "High" or "Critical"
THEN
# Propose containment actions
Post adaptive card (Teams)
Channel: Security Operations
Card:
Title: Security Incident - Containment Required
Incident: @{incidentId}
Severity: @{severity}
Recommended Actions:
β Disable affected service principal
β Revoke API tokens
β Block IP addresses
β Disable automation workflows
Buttons:
- Execute All Recommended Actions
- Custom Response
- Investigate Further
# Wait for response
Wait for Teams response
# Execute approved actions
Switch (response)
Case "Execute All":
[Execute containment steps]
Case "Custom":
[Allow manual selection]
Case "Investigate":
[Continue monitoring]
# After threat identified and contained
# Rotate compromised credentials
HTTP: Rotate service principal secret
Application: @{affectedAppId}
# Update Key Vault
HTTP: Update Key Vault secret
Secret: @{secretName}
NewValue: @{newSecret}
# Review and revoke suspicious API tokens
HTTP: Revoke tokens
TokenIds: @{suspiciousTokens}
# Update automation workflows with new credentials
HTTP: Update Power Automate connections
[Update connection references]
# Restore normal operations
# Re-enable service principal (with new credentials)
HTTP: Enable service principal
AppId: @{appId}
AccountEnabled: true
# Test automation workflows
For each workflow:
HTTP: Trigger test run
Monitor: Success/Failure
Condition: Test failed
THEN
Alert: Workflow still impacted
ELSE
Log: Workflow recovered
# Monitor closely for 48 hours
Set variable: enhancedMonitoring = true
Set variable: monitoringEndTime = @{addHours(utcnow(), 48)}
# After incident resolved (automated report generation)
# Compile incident data
Get incident details
Incident: @{incidentId}
Timeline: @{incidentTimeline}
Actions Taken: @{actionLog}
Impact: @{impactAssessment}
# Generate post-incident report
HTTP: Create report
Template: Security Incident Review
Data: @{incidentData}
# Schedule review meeting
Create calendar event
Title: Security Incident Review - @{incidentId}
Attendees: Security Team
Time: @{addDays(utcnow(), 3)}
Agenda: [Auto-generated from incident]
Securing Power BI automation requires layered defenses:
Key takeaways:
Getting help: MBIC provides security assessment and implementation services β security architecture review, compliance gap analysis, secure implementation, and ongoing security monitoring. Contact: hello@mbic.us Β· mbic.us
Prefer the PDF? Download the original
Get a free AI & Automation Opportunity Audit β mbic.us/ai-audit.html β or book 15 minutes β mbic.us/contact.html.