✨ Readme✨
Also refer to Blog Article Automate Azure Bastion with Drasi Realtime RBAC Monitoring.
Imagine you work at a company where people frequently need access to virtual machines (VMs) in Azure. Traditionally, an admin would:
- Manually assign VM access permissions to users
- Manually create a secure connection tool (Azure Bastion) for each VM
- Manually clean up these resources when access is no longer needed
This project automates all of that! It watches for permission changes and automatically creates or removes the necessary infrastructure.
When Sarah from Marketing needs access to a VM:
- An admin assigns her "VM Administrator Login" role
- Automatically, this system detects the change
- Automatically, it creates a secure Bastion host for that VM
- Sarah can now securely connect to the VM
- When her access is revoked, the Bastion is automatically cleaned up
Think of Azure Functions like "mini-programs" that run in the cloud. They only execute when triggered by an event (like receiving a notification). You don't need to manage servers - Azure handles all the infrastructure.
Drasi is a platform that watches for changes in your data and reacts instantly. It's like having a super-smart assistant that monitors everything and takes action when specific things happen.
Drasi has three parts:
- Sources: Where data comes from (in our case, Azure Activity Logs)
- Continuous Queries: What changes to watch for (role assignments)
- Reactions: What to do when changes happen (notify our Azure Function)
A secure way to connect to VMs without exposing them to the internet. Think Azure Bastion it as a secure "bridge" that lets users safely access VMs through their web browser.
A scripting language that's excellent for automating Azure tasks. Don't worry if you're new to it - our code is well-commented and modular!
📋 Azure Activity Logs → 📨 Event Hub → 🔍 Drasi → 📧 Event Grid → ⚡ Azure Function → 🛡️ Bastion
- Azure Activity Logs: Every action in Azure (like assigning roles) gets logged
- Event Hub: Collects these logs in real-time
- Drasi Source: Reads events from the Event Hub
- Drasi Continuous Query: Filters for role assignment events we care about
- Drasi Reaction: Sends notifications to Event Grid when matches are found
- Azure Function: Receives the notification and takes action
- Bastion Management: Creates or removes Azure Bastion hosts as needed
📁 Sources/ # Drasi configuration for reading Azure Event Hub
📁 Queries/ # Drasi query that watches for role changes
📁 Reactions/ # Drasi reaction that sends notifications
📁 AzureFunction/ # PowerShell code that manages Azure resources
📄 run.ps1 # Main function entry point
📄 ActionHandlers.ps1 # Classes that perform specific actions
📄 EventProcessor.ps1 # Parses incoming events
📄 config.json # Configuration for actions and roles
- Azure CLI: Tool for managing Azure resources (install guide)
- kubectl: Tool for managing Kubernetes clusters (install guide)
- Drasi CLI: Tool for managing Drasi (install guide)
- Azure Event Hub: Where Azure Activity Logs will be sent
- Azure Event Grid Topic: For receiving notifications from Drasi
- Azure Function App: With PowerShell 7 runtime to run our automation code
- Managed Identity: Special account that allows secure access to Azure resources
Your Managed Identity needs these roles:
Network Contributor(to create/delete Bastion hosts)Virtual Machine Contributor(to work with VMs)Reader(to discover existing resources)
Choose how you want to run Drasi:
# Initialize Drasi with Docker support
drasi init --docker# Initialize Drasi on your Kubernetes cluster
drasi initEdit AzureFunction/config.json with your Azure details:
{
"global": {
"enableLogging": true,
"defaultSubscriptionId": "YOUR-SUBSCRIPTION-ID",
"defaultResourceGroupName": "YOUR-RESOURCE-GROUP",
"tags": {
"CreatedBy": "Drasi-AutoBastion",
"Purpose": "Automated-RBAC-Response"
}
},
"actions": {
"CreateBastion": {
"enabled": true,
"parameters": {
"bastionNamePrefix": "bastion-auto",
"subnetAddressPrefix": "10.0.1.0/26",
"publicIpNamePrefix": "pip-bastion-auto"
}
}
}
}- Create an Azure Function App with PowerShell 7 runtime
- Enable Managed Identity for the Function App
- Upload the contents of the
AzureFunction/folder - Configure Event Grid subscription to trigger the function
# Update Sources/eventhubsource.yaml with your Event Hub details
drasi apply -f Sources/eventhubsource.yaml# This watches for role assignment changes
drasi apply -f Queries/azure-role-change-vmadminlogin.yaml# Update Reactions/azure-role-change-vmadminloginaction.yaml with your Event Grid details
drasi apply -f Reactions/azure-role-change-vmadminloginaction.yamlCreate a test role assignment:
# Assign VM Administrator Login role to test
az role assignment create \
--assignee "[email protected]" \
--role "Virtual Machine Administrator Login" \
--scope "/subscriptions/YOUR-SUB-ID/resourceGroups/YOUR-RG/providers/Microsoft.Compute/virtualMachines/YOUR-VM"Watch the logs in your Azure Function to see the automation in action!
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Step 1 │ │ Step 2 │ │ Step 3 │ │ Step 4 │
│ │ │ │ │ │ │ │
│ Install │───▶│ Configure │───▶│ Deploy │───▶│ Test │
│ Drasi CLI │ │ Azure │ │ Components │ │ System │
│ │ │ Function │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
The Azure Function is the "brain" of our automation. Here's how it's organized:
This file receives Event Grid notifications and orchestrates the response:
# 1. Validates the incoming event
# 2. Parses role assignment details
# 3. Determines what actions to take
# 4. Executes the actions
# 5. Logs the resultsThis file contains smart logic to understand different types of role assignment events:
# Extracts information like:
# - What role was assigned/removed?
# - Who got the role?
# - What resource is involved?
# - When did it happen?This file contains "action classes" that do the actual work:
# CreateBastionAction: Creates Bastion hosts
# CleanupBastionAction: Removes Bastion hosts
# Future actions can be added here!This file defines:
- Which roles trigger which actions
- Configuration parameters for each action
- Global settings like logging and tagging
Want to add new automation? Here's how:
- Find the role definition ID in Azure:
az role definition list --name "Your Role Name" --query "[].name"- Add to
config.json:
"roleMappings": {
"/providers/Microsoft.Authorization/roleDefinitions/YOUR-ROLE-ID": "Your Role Name"
},
"actions": {
"YourNewAction": {
"enabled": true,
"parameters": {
"setting1": "value1"
}
}
}- Create a new class in
ActionHandlers.ps1:
class YourNewAction : BaseAction {
YourNewAction([hashtable]$config, [hashtable]$globalConfig) : base($config, $globalConfig) {}
[ActionResult] Execute([hashtable]$context) {
$this.LogInfo("Starting your new action...")
try {
# Your automation logic here
# For example: Create storage account, send email, etc.
return [ActionResult]::new($true, "Action completed successfully", @{})
}
catch {
return [ActionResult]::new($false, "Action failed: $($_.Exception.Message)", @{})
}
}
}- Register your action in the factory function:
function New-Action {
# ... existing code ...
switch ($ActionName) {
"YourNewAction" {
return [YourNewAction]::new($Config, $GlobalConfig)
}
# ... other actions ...
}
}Want to watch for different events? Edit Queries/azure-role-change-vmadminlogin.yaml:
# Change the filter to watch for different operations
selector: $.records[?(@.operationName == 'YOUR.OPERATION/HERE')]
# Or modify the query to return different data
query: |
MATCH (r:RoleAssignment)
WHERE r.operationName CONTAINS 'YOUR_FILTER'
RETURN r.customField AS customData- Check Event Grid subscription is pointing to your function
- Verify Drasi reaction has correct Event Grid URL and key
- Look at Azure Function logs for errors
- Ensure Managed Identity has required roles assigned
- Check that the identity is enabled on your Function App
- Verify your VNet has available IP address space
- Check that the subnet CIDR doesn't conflict with existing subnets
- Ensure you have sufficient quota in your Azure subscription
Enable detailed logging in config.json:
"global": {
"enableLogging": true
}Use the sample event in AzureFunction/sample-events.json to test your function locally.
- Always use Managed Identity (never store credentials in code)
- Regularly rotate Event Grid access keys
- Use the least-privilege principle for role assignments
All created resources are automatically tagged for tracking:
"tags": {
"CreatedBy": "Drasi-AutoBastion",
"Purpose": "Automated-RBAC-Response"
}- Dry-run mode available for testing
- Cleanup actions check for other dependencies before deleting
- All operations are logged for audit trails
"CreateBastion": {
"parameters": {
"bastionNamePrefix": "custom-bastion",
"subnetAddressPrefix": "10.1.0.0/26", // Customize IP range
"publicIpNamePrefix": "pip-custom",
"bastionSku": "Standard", // or "Basic"
"scaleUnits": 2 // Number of scale units
}
}"CleanupBastion": {
"parameters": {
"preserveIfOtherAssignments": true, // Safety check
"gracePeriodMinutes": 10, // Wait before cleanup
"forceCleanup": false // Emergency override
}
}Monitor these key metrics in the Azure Portal:
- Function execution count
- Success/failure rates
- Duration and performance
- Error frequency
The function provides structured logging:
[INFO]- Normal operations[WARNING]- Non-critical issues[ERROR]- Failures requiring attention
Set up Azure Monitor alerts for:
- Function execution failures
- Bastion creation/deletion events
- Permission-related errors
- Standard SKU: ~$140/month per instance
- Basic SKU: ~$87/month per instance
- Consider cleanup automation to minimize costs
- Use tags to track automation-created resources
- Implement cost alerts for your resource groups
- Regular audit of created Bastion hosts
Want to improve this project? Here's how:
- Check existing issues first
- Provide detailed error messages and logs
- Include your configuration (sanitized)
- Fork the repository
- Create a feature branch
- Add your new action classes
- Update configuration examples
- Test thoroughly
- Submit a pull request
Help improve this README by:
- Adding more examples
- Clarifying complex concepts
- Fixing typos or errors
- Drasi Documentation
- Azure Functions PowerShell Guide
- Azure Bastion Documentation
- PowerShell for Azure
- [Drasi GitHub](https://github.com/orgs/drasi-project/discussions](https://github.com/drasi-project)
- Azure PowerShell Community
Happy Automating! 🚀
This project demonstrates the power of event-driven automation using Drasi and Azure Functions. Start small, learn as you go, and gradually add more sophisticated automation to your environment.
New to this project? We've made it super easy:
- 📋 Run the setup checker:
./setup.sh- Verifies you have everything installed - 📝 Use the config template: Copy
AzureFunction/config.template.jsontoAzureFunction/config.json - 🆘 Having issues? Check
TROUBLESHOOTING.mdfor common problems and solutions - 📚 Follow the detailed guide below for step-by-step instructions
During the development of this Azure Role Assignment Monitor, we encountered several common issues with Drasi queries and Event Hub integration. This section documents these challenges and their solutions to help future developers avoid the same pitfalls.
Problem: Drasi continuous queries failed with parser errors when using the > operator for multiline Cypher queries.
Root Cause: The > operator in YAML folds line breaks into spaces, causing the Cypher parser to receive malformed syntax.
Solution: Use the | operator to preserve literal line breaks:
# ❌ Wrong - causes parser errors
query: >
MATCH (r:RoleAssignment)
WHERE r.requestBody CONTAINS 'role-id'
RETURN r.correlationId
# ✅ Correct - preserves line structure
query: |
MATCH (r:RoleAssignment)
WHERE r.requestBody CONTAINS 'role-id'
RETURN r.correlationIdIssues Fixed: #9, #10
Problem: Parser errors when using standard Cypher functions like toString().
Root Cause: Drasi Query Language (DQL) doesn't support all standard Cypher functions.
Solution: Use DQL-compatible syntax and avoid unsupported functions:
-- ❌ Wrong - toString() not supported
WHERE r.requestBody IS NOT NULL AND toString(r.requestBody) CONTAINS 'role-id'
-- ✅ Correct - direct string operation
WHERE r.requestBody IS NOT NULL AND r.requestBody CONTAINS 'role-id'Issues Fixed: #11, #12
Problem: Continuous queries couldn't access role assignment properties from Event Hub data.
Root Cause: Event Hub sends requestbody as a JSON string, not a parsed object, requiring different JSONPath selectors.
Event Hub Data Structure:
{
"records": [{
"properties": {
"requestbody": "{\"Id\":\"...\",\"Properties\":{\"PrincipalId\":\"...\"}}"
}
}]
}Solution: Use correct JSONPath selectors for the actual data structure:
# ❌ Wrong - assumes parsed object
principalId: $.properties.responseBody.properties.principalId
# ✅ Correct - extracts from JSON string
requestBody: $.properties.requestbodyIssues Fixed: #5, #6, #7, #8
Problem: Continuous queries only returned correlationId, missing other important properties.
Root Cause: Middleware configuration wasn't extracting all available Event Hub properties.
Solution: Comprehensive middleware property extraction:
properties:
time: $.time
resourceId: $.resourceId
operationName: $.operationName
correlationId: $.correlationId
caller: $.identity.claims.name
callerIpAddress: $.callerIpAddress
tenantId: $.tenantId
properties: $.properties
requestBody: $.properties.requestbodyIssues Fixed: #13, #14
Problem: WHERE clauses using exact field matching failed because required fields weren't extracted.
Root Cause: Trying to filter on roleDefinitionId field that wasn't properly extracted from the JSON string.
Solution: Use string contains matching on the raw requestBody:
-- ❌ Wrong - field not available
WHERE r.roleDefinitionId = '/providers/Microsoft.Authorization/roleDefinitions/1c0163c0-47e6-4577-8991-ea5c82e286e4'
-- ✅ Correct - string contains on raw data
WHERE r.requestBody CONTAINS '1c0163c0-47e6-4577-8991-ea5c82e286e4'Issues Fixed: #1, #2
Problem: Source named 'my-source' didn't reflect its purpose, making configuration unclear.
Root Cause: Poor naming conventions that don't describe functionality.
Solution: Use descriptive, purpose-driven names:
# ❌ Wrong - generic name
name: my-source
# ✅ Correct - descriptive name
name: azure-role-eventhub-sourceBenefits:
- Immediately clear what the source does
- Easier maintenance and debugging
- Better documentation and understanding
Issues Fixed: #3, #4
Problem: Initial implementations were specific to single use cases, limiting extensibility.
Root Cause: Not considering future requirements for multiple roles and actions.
Solution: Implemented modular, configuration-driven architecture:
{
"roleActions": {
"/providers/Microsoft.Authorization/roleDefinitions/role-id": {
"name": "Role Name",
"actions": {
"create": ["CreateAction"],
"delete": ["CleanupAction"]
}
}
}
}Benefits:
- Easy to add new roles without code changes
- Extensible action system
- Configuration-driven behavior
- Better testability and maintenance
Issues Fixed: #15, #18, #19
-
Always Use Drasi Query Language Reference: Consult https://drasi.io/reference/query-language/ for syntax and supported functions
-
Understand Your Data Structure: Examine actual Event Hub payloads before writing JSONPath selectors
-
Test Incrementally: Start with simple property extraction before adding complex filtering logic
-
Use Descriptive Naming: Names should immediately convey purpose and functionality
-
Plan for Extensibility: Design modular systems that can grow with requirements
-
Validate YAML Syntax: Use
yamllintto catch formatting issues early -
Monitor Drasi Logs: Use
drasi list queriesand check status for early problem detection
When encountering Drasi query issues:
-
Check Query Status:
drasi list queries # Look for TerminalError status -
Examine Raw Event Hub Data:
# Check what data structure you're actually receiving -
Test JSONPath Selectors:
# Use online JSONPath evaluators to test selectors -
Validate YAML:
yamllint Queries/*.yaml -
Start Simple:
- Begin with basic property extraction
- Add filtering incrementally
- Test each change separately
- Drasi Query Language Reference
- Event Hub Schema Documentation
- TROUBLESHOOTING.md - Common deployment and runtime issues
These lessons learned represent real challenges encountered during development. By documenting them, we hope to accelerate future development and reduce common mistakes.