Inside the Azure Onboarding Script
The Azure onboarding script automates the onboarding of an Azure subscription into ACE. It provisions the required Azure resources and role assignments, enabling AlgoSec to integrate with and monitor the environment.
This page documents each section of the script to help users understand its purpose and functionality.
There are two scripts available.
-
For ACE with license for Cloud App Analyzer
-
For ACE without license for Cloud App Analyzer
#!/bin/bash
# Capture pipe failures
set -o pipefail
#Algosec Cloud tenantId
ALGOSEC_TENANT_ID='<ALGOSEC_TENANT_ID>'
#Algosec Cloud multi-tenant application
APP_ID='<ALGOSEC_ONBOARDING_APP_ID>'
#Algosec Cloud onboarding URL
ALGOSEC_CLOUD_HOST='https://<HOST>'
ALGOSEC_CLOUD_ONBOARDING_URL="$ALGOSEC_CLOUD_HOST<ONBOARDING_PATH>"
#Token
TOKEN='<ONBOARDING_TOKEN>'
ADDITIONALS='<ALGOSEC_ADDITIONALS>'
#Target resource
TARGET_RESOURCE='<AZURE_TARGET_RESOURCE>'
TARGET_ID='<AZURE_TARGET_RESOURCE_ID>'
#Spinner util
spinner() {
local seconds=$1
local text=$2
local start=$SECONDS
local spin='|/-\'
local i=0
tput civis # Hide cursor
printf "%s " "$text"
while [ $((SECONDS - start)) -lt "$seconds" ]; do
printf "\b%c" "${spin:i++%4:1}"
sleep 0.1
done
printf "\b \n"
tput cnorm # Show cursor
}
az_tenant=$(az account show --query tenantId -o tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to retrieve Azure tenant ID: $az_tenant"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Preparing to onboard the target resource [$TARGET_RESOURCE] of [$az_tenant] tenant"
echo "Check if service principal already exists for Algosec Cloud AZ-AD Application"
sp=$(az ad sp list --filter "appId eq '$APP_ID'" --query "length(@)" --output tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to view service principals: $sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
if [[ $sp -eq 0 ]]; then
echo "Service Principal not found"
echo "Creating service principal for Algosec Cloud AZ-AD Application"
create_sp=$(az ad sp create --id "$APP_ID" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create service principal for Algosec Cloud AZ-AD Application: $create_sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Service Principal created successfully"
else
echo "Service Principal found for Algosec Cloud AZ-AD Application"
fi
#Roles array to be assigned to service principal at target resource scope
roles=()
#----------------------------------------------------
#App Analyzer VM Scan custom role
role_hash=$(echo -n "${ALGOSEC_TENANT_ID}#${TARGET_ID}" | md5sum | cut -c1-8)
vm_scan_role="Algosec App Analyzer VM Scan ($role_hash)"
cat > ace_vm_scan_custom_role.json <<EOF
{
"Name": "$vm_scan_role",
"IsCustom": true,
"Description": "Allows Algosec App Analyzer to create/delete resources as needed for VM scanning purposes",
"Actions": [
<actions_vm_scan>
],
"AssignableScopes": [
"$TARGET_RESOURCE"
]
}
EOF
echo "Attempting to create custom role '$vm_scan_role'..."
vm_scan_create_role=$(az role definition create --role-definition ace_vm_scan_custom_role.json 2>&1)
if [[ $? -eq 0 ]]; then
echo "Custom role '$vm_scan_role' created successfully."
spinner 10 "Waiting for Azure to propagate the role..."
else
# Check if error is because role already exists
if [[ "$vm_scan_create_role" == *"RoleDefinitionWithSameNameExists"* ]]; then
echo "Role already exists. Updating to ensure correct configuration..."
vm_scan_update_role=$(az role definition update --role-definition ace_vm_scan_custom_role.json 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to update custom role '$vm_scan_role': $vm_scan_update_role"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Custom role '$vm_scan_role' updated successfully."
else
echo "ERROR: Failed to create custom role '$vm_scan_role': $vm_scan_create_role"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
fi
rm ace_vm_scan_custom_role.json
#Add to roles array
roles+=( "$vm_scan_role" )
#----------------------------------------------------
#Add roles
roles+=( <SERVICE_PRINCIPAL_ROLES> )
for role in "${roles[@]}"; do
echo "Assign a role to the [$TARGET_RESOURCE]: [$role]"
role_assign=$(az role assignment create --role "$role" --assignee "$APP_ID" --scope "$TARGET_RESOURCE" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to assign role: $role to target resource [$TARGET_RESOURCE]: $role_assign"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
done
#----------------------------------------------------
#Azure region
REGION='<AZURE_REGION>'
#Prevasio host
PREVASIO_HOST='<PREVASIO_HOST>'
#Prevasio source code location
SOURCES_URL='<PREVASIO_SOURCES_URL>'
declare CURRENT_PREVASIO_RESOURCES_MD5
rollback_resources() {
local prevasio_hash=$1
local subscription_id=$2
local deploy_group_name=$3
local role_name=$4
local assignee_id=$5
echo "Rolling back resources in [$subscription_id]"
if [ -n "$assignee_id" ] && [ -n "$role_name" ]; then
echo "Removing role assignment..."
az role assignment delete --assignee "$assignee_id" --role "$role_name" --scope "/subscriptions/$subscription_id"
fi
if [ -n "$role_name" ]; then
echo "Deleting custom role definition..."
delete_role_if_exists "$role_name"
fi
if [ -n "$deploy_group_name" ]; then
echo "Deleting deployment group..."
az deployment group delete --name "$deploy_group_name" --resource-group "prevasio-$prevasio_hash-resource-group"
fi
echo "Deleting resource group..."
az group delete --name "prevasio-$prevasio_hash-resource-group" --yes
}
delete_role_assignments(){
local role_name="$1"
az role assignment list --role "$role_name" --output json |
jq -c '.[]' |
while IFS= read -r role_assignment; do
id=$(echo "$role_assignment" | jq -r '.id')
echo "Deleting role assignment: $id"
az role assignment delete --ids "$id"
done
}
delete_role_if_exists() {
local role_name="$1"
get_role_scope() {
az role definition list --query "[?roleName==\`${role_name}\`].[assignableScopes[0]] | [0]" -o tsv
}
role_scope=$(get_role_scope)
if [ -n "$role_scope" ]; then
delete_role_assignments "$role_name"
az role definition delete --name "$role_name" --scope "$role_scope"
echo "Waiting for role to be deleted..."
local max_retries=5
local retries=0
while true; do
sleep 10
role_scope=$(get_role_scope)
if [ -z "$role_scope" ]; then
echo "Role '$role_name' deleted successfully."
break
else
retries=$((retries + 1))
echo "Role still exists. Retry $retries/$max_retries..."
if [ "$retries" -ge "$max_retries" ]; then
echo "Unable to delete role automatically after $max_retries attempts. Please delete manually:"
echo " az role definition delete --name \"$role_name\" --scope \"$role_scope\""
break
fi
fi
done
else
echo "Role '$role_name' does not exist. No deletion needed."
fi
}
delete_kv_if_exists(){
local kv_name="prevasio-$prevasio_hash-kv"
if az keyvault show --name "$kv_name" --query "name" --output tsv 2>/dev/null | grep -q "$kv_name"; then
echo "Purging key vault '$kv_name'..."
az keyvault purge --name "$kv_name" --location "$REGION"
fi
if az keyvault show-deleted --name "$kv_name" --query "name" --output tsv 2>/dev/null | grep -q "$kv_name"; then
echo "Purging key vault '$kv_name'..."
az keyvault purge --name "$kv_name" --location "$REGION"
fi
}
cleanup_existing_resources() {
local subscription_id="$1"
az account set --subscription "$subscription_id"
local rg_name="prevasio-$prevasio_hash-resource-group"
echo "Checking for existing resources to clean in [$subscription_id]..."
# Delete resource group if it exists
if az group exists --name "$rg_name" | grep -q true; then
echo "Deleting resource group '$rg_name'..."
az group delete --name "$rg_name" --yes
fi
delete_kv_if_exists
}
check_az_failure() {
local response="$1"
local context_message="$2"
local json_part
json_part=$(echo "$response" | sed -n '/^{/,$p')
if ! echo "$json_part" | jq -e . > /dev/null 2>&1; then
echo "$context_message failed: $response"
return 1
fi
if echo "$json_part" | jq -e 'has("error")' > /dev/null; then
local error_message
error_message=$(echo "$json_part" | jq -r '.error.message // "Unknown error"')
echo "$context_message failed: $error_message"
return 1
fi
return 0
}
deploy_code_to_subscription() {
local subscription_id=$1
local prevasio_hash="${ALGOSEC_TENANT_ID:0:4}${subscription_id:0:4}"
az account set --subscription "$subscription_id"
echo "Deploying resources to [$subscription_id] subscription"
should_drop_resource_group=false
local resource_group_name="prevasio-$prevasio_hash-resource-group"
if [ "$(az group exists --name $resource_group_name)" == "true" ]; then
resource_group_version=$(az group show --name $resource_group_name --query "tags.version" --output tsv)
if [ -n "$resource_group_version" ] && [ "$resource_group_version" == "$CURRENT_PREVASIO_RESOURCES_MD5" ]; then
echo "The current onboarding in [$subscription_id] subscription is up to date. Skipping the resources creation."
return
fi
should_drop_resource_group=true
fi
if $should_drop_resource_group ; then
echo "Deleting resource group prevasio-$prevasio_hash-resource-group in [$subscription_id] subscription"
cleanup_existing_resources "$prevasio_hash" "$subscription_id"
fi
echo '{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"subscription-id": {
"value": "'$subscription_id'"
},
"tenant-id": {
"value": "'$az_tenant'"
},
"prevasio-hash": {
"value": "'$prevasio_hash'"
},
"prevasio-host": {
"value": "'$PREVASIO_HOST'"
},
"prevasio-additionals": {
"value": "'$ADDITIONALS'"
},
"algosec-cloud-host": {
"value": "'$ALGOSEC_CLOUD_HOST'"
}
}
}' > parameters.json
# Deleting Key Vault if it exists
delete_kv_if_exists
# Creating Resource Group
echo "Creating resource group prevasio-$prevasio_hash-resource-group in [$subscription_id] subscription"
group_response=$(az group create --name prevasio-$prevasio_hash-resource-group --location $REGION --tag version=$CURRENT_PREVASIO_RESOURCES_MD5 2>&1)
if ! check_az_failure "$group_response" "Resource group creation"; then
return
fi
# Creating Deployment Group and checking if the operation is success
echo "Creating application resources in [$subscription_id] subscription"
deploy_group_response=$(az deployment group create --resource-group prevasio-$prevasio_hash-resource-group --template-file template.json --parameters parameters.json 2>&1)
if ! check_az_failure "$deploy_group_response" "Deployment group creation"; then
rollback_resources "$prevasio_hash" "$subscription_id"
return
fi
deploy_group_name=$(echo $deploy_group_response | jq -r '.name')
# Creating and assigning role
echo "Assigning roles to application in [$subscription_id] subscription"
role_name="Prevasio Application Role ($prevasio_hash)"
echo '{
"Name": "'"$role_name"'",
"IsCustom": true,
"Description": "Allows to create EventGrid subscriptions for ACR registries events.",
"Actions": [
"Microsoft.EventGrid/eventSubscriptions/read",
"Microsoft.ContainerRegistry/registries/read",
"Microsoft.EventGrid/eventSubscriptions/write",
"Microsoft.Web/sites/functions/write"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/'$subscription_id'"
]
}' > ./role_def.json
# Delete, if a role with same name already exists.
delete_role_if_exists "$role_name"
# Role creation
role_creation_response=$(az role definition create --role-definition role_def.json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name"
return
fi
generated_role_name=$(echo "$role_creation_response" | sed '/^{/,$!d' | jq -r '.name')
# Role Assignment
assignee_id=$(az ad sp list --display-name prevasio-$prevasio_hash-app --query [].id --output tsv)
role_assignment_create_response=$(az role assignment create --assignee $assignee_id --role "$generated_role_name" --scope /subscriptions/$subscription_id 2>&1)
if ! check_az_failure "$role_assignment_create_response" "Role assignment creation"; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name" "$generated_role_name"
return
fi
# Creating and deploying function app.
echo "Deploying application sources in [$subscription_id] subscription"
functionapp_deploy_response=$(az functionapp deployment source config-zip -g prevasio-$prevasio_hash-resource-group -n prevasio-$prevasio_hash-app --src function.zip --build-remote true 2>&1)
if ! check_az_failure "$functionapp_deploy_response" "Function app creation/deployment"; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name" "$generated_role_name" "$assignee_id"
return
fi
echo "Application sources were deployed successfully"
}
mkdir -p prevasio-onboarding && rm -rf prevasio-onboarding/*
cd prevasio-onboarding
echo "Downloading Prevasio application resources"
wget -O sources.zip --header "user-agent: algosec/1.0" "${SOURCES_URL}?tenant_id=${ALGOSEC_TENANT_ID}"
if [[ -f "sources.zip" ]]; then
CURRENT_PREVASIO_RESOURCES_MD5=$(md5sum "sources.zip" | awk '{print $1}')
else
CURRENT_PREVASIO_RESOURCES_MD5=0
echo "Error: Failed to download sources.zip"
return
fi
unzip sources.zip
echo "Resources were downloaded successfully. Resources version: $CURRENT_PREVASIO_RESOURCES_MD5"
# Single subscription
if [[ $TARGET_RESOURCE == /subscriptions/* ]]; then
deploy_code_to_subscription $TARGET_ID
else
if [[ $TARGET_RESOURCE == /providers/Microsoft.Management/managementGroups/* ]]; then
# Management group - Fetch subscriptions under mg
subscriptions=$(az account management-group subscription show-sub-under-mg --name $TARGET_ID | jq -r '.[].name')
else
# Tenant Root - Fetch all subscriptions in the tenant
subscriptions=$(az account list | jq -r '.[].id')
fi
if [ $? -ne 0 ]; then
echo "WARNING: Failed to retrieve subscriptions — skipping resource creation. The Cloud App Analyzer: CD Mitigation feature will be impacted"
else
# Deploy code to all relevant subscriptions
for subscription in $subscriptions; do
deploy_code_to_subscription $subscription
done
fi
fi
cd ..
rm -rf prevasio-onboarding
#----------------------------------------------------
echo "Sending onboarding request to Algosec Cloud..."
response=$(curl -X POST "$ALGOSEC_CLOUD_ONBOARDING_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: $TOKEN" \
--silent \
-d '{ "azure_tenant":"'"$az_tenant"'", "supportChanges": "<SUPPORT_CHANGES>", "event": { "RequestType": "Create" } }')
status=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.status')
message=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.message')
if [[ "$status" == 200 ]]; then
echo "The onboarding process is finished: $message"
echo "Press CTRL+D to close the terminal session"
else
echo "ERROR: The onboarding process has failed: $message"
fi
Script Sections
1. Shell Options and Initialize Variables
#!/bin/bash
# Capture pipe failures
set -o pipefail
#Algosec Cloud tenantId
ALGOSEC_TENANT_ID='<ALGOSEC_TENANT_ID>'
#Algosec Cloud multi-tenant application
APP_ID='<ALGOSEC_ONBOARDING_APP_ID>'
#Algosec Cloud onboarding URL
ALGOSEC_CLOUD_HOST='https://<HOST>'
ALGOSEC_CLOUD_ONBOARDING_URL="$ALGOSEC_CLOUD_HOST<ONBOARDING_PATH>"
#Token
TOKEN='<ONBOARDING_TOKEN>'
ADDITIONALS='<ALGOSEC_ADDITIONALS>'
#Target resource
TARGET_RESOURCE='<AZURE_TARGET_RESOURCE>'
TARGET_ID='<AZURE_TARGET_RESOURCE_ID>'
Purpose: The script begins by enabling set -o pipefail, which causes a pipeline
to return the exit code of the first failed command rather than the last one.
This ensures that errors within piped commands are not silently swallowed. It
then defines the core variables used throughout the script:
ALGOSEC_TENANT_ID— AlgoSec tenant IDAPP_ID— AlgoSec onboarding Azure AD application IDALGOSEC_CLOUD_HOST— The host of AlgoSec's APIsALGOSEC_CLOUD_ONBOARDING_URL— The URL for AlgoSec's onboarding APITOKEN— An authentication token for the APIADDITIONALS— Base64-encoded login credentialsTARGET_RESOURCE— The Azure resource path to be onboarded (subscription, management group, or tenant root)TARGET_ID— The Azure resource ID to be onboarded
2. Spinner Utility Function
spinner() {
local seconds=$1
local text=$2
local start=$SECONDS
local spin='|/-\'
local i=0
tput civis # Hide cursor
printf "%s " "$text"
while [ $((SECONDS - start)) -lt "$seconds" ]; do
printf "\b%c" "${spin:i++%4:1}"
sleep 0.1
done
printf "\b \n"
tput cnorm # Show cursor
}
Purpose: Defines a visual spinner utility used to display a progress indicator in the terminal while waiting for Azure to propagate changes (such as a newly created custom role). It accepts a duration in seconds and a label text, hides the cursor during the wait, and restores it when done. This improves the user experience during operations that require a short delay.
3. Retrieve Azure Tenant ID
az_tenant=$(az account show --query tenantId -o tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to retrieve Azure tenant ID: $az_tenant"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
Purpose: Uses the Azure CLI to retrieve the tenant ID of
the currently authenticated Azure account. The --query tenantId -o tsv flags extract the value directly
without requiring an additional jq
parse. If the command fails (for example, if the user is not logged in), the
full error output is captured and displayed, and the script exits immediately.
4. Echo Information About the Target Resource
echo "Preparing to onboard the target resource [$TARGET_RESOURCE] of [$az_tenant] tenant"
Purpose: Prints a log line showing the target Azure resource being onboarded and the Azure tenant ID. This is used for visibility and traceability in the terminal session.
5. Retrieve Existing Service Principal
echo "Check if service principal already exists for Algosec Cloud AZ-AD Application"
sp=$(az ad sp list --filter "appId eq '$APP_ID'" --query "length(@)" --output tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to view service principals: $sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
Purpose: Checks whether a service principal already exists
in the tenant for the AlgoSec Azure AD application. The --query "length(@)" flag
counts the results directly in the CLI query rather than piping to jq, making the command more efficient.
If the user lacks permission to list service principals, the error output is
captured and the script exits with a clear message.
6. Create Service Principal
if [[ $sp -eq 0 ]]; then
echo "Service Principal not found"
echo "Creating service principal for Algosec Cloud AZ-AD Application"
create_sp=$(az ad sp create --id "$APP_ID" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create service principal for Algosec Cloud AZ-AD Application: $create_sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Service Principal created successfully"
else
echo "Service Principal found for Algosec Cloud AZ-AD Application"
fi
Purpose: If no existing service principal was found for the
AlgoSec application, this section creates one using az ad sp create. The output of the
create command is captured into create_sp
so that if creation fails, the error detail is included in the failure message.
If a service principal already exists, creation is skipped.
7. Initialize Roles Array
#Roles array to be assigned to service principal at target resource scope
roles=()
Purpose: Initializes an empty array that will accumulate all Azure roles to be assigned to the AlgoSec service principal at the target resource scope. Roles are added to this array by subsequent sections before being assigned in bulk.
8. VM Scan Custom Role Creation (Optional - Onboard VM Scanning enabled)
#App Analyzer VM Scan custom role
role_hash=$(echo -n "${ALGOSEC_TENANT_ID}#${TARGET_ID}" | md5sum | cut -c1-8)
vm_scan_role="Algosec App Analyzer VM Scan ($role_hash)"
cat > ace_vm_scan_custom_role.json <<EOF
{
"Name": "$vm_scan_role",
"IsCustom": true,
"Description": "Allows Algosec App Analyzer to create/delete resources as needed for VM scanning purposes",
"Actions": [
<actions_vm_scan>
],
"AssignableScopes": [
"$TARGET_RESOURCE"
]
}
EOF
echo "Attempting to create custom role '$vm_scan_role'..."
vm_scan_create_role=$(az role definition create --role-definition ace_vm_scan_custom_role.json 2>&1)
if [[ $? -eq 0 ]]; then
echo "Custom role '$vm_scan_role' created successfully."
spinner 10 "Waiting for Azure to propagate the role..."
else
# Check if error is because role already exists
if [[ "$vm_scan_create_role" == *"RoleDefinitionWithSameNameExists"* ]]; then
echo "Role already exists. Updating to ensure correct configuration..."
vm_scan_update_role=$(az role definition update --role-definition ace_vm_scan_custom_role.json 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to update custom role '$vm_scan_role': $vm_scan_update_role"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Custom role '$vm_scan_role' updated successfully."
else
echo "ERROR: Failed to create custom role '$vm_scan_role': $vm_scan_create_role"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
fi
rm ace_vm_scan_custom_role.json
#Add to roles array
roles+=( "$vm_scan_role" )
Purpose: Creates a custom Azure role required for the Cloud App Analyzer VM scanning feature. This section is only present when the VM scanning capability is enabled. A unique, deterministic role name is generated by hashing the AlgoSec tenant ID and the target Azure resource ID (first 8 characters of the MD5 hash), ensuring the role name is consistent across re-runs but scoped per tenant/resource combination.
The role definition JSON is written to a temporary file and submitted to Azure. The script handles two outcomes:
- If the role does not exist, it is created and a short spinner waits for Azure to propagate the new role definition before proceeding.
- If the role already exists (
RoleDefinitionWithSameNameExists), it is updated instead, ensuring the permissions stay current without needing to delete and recreate the role.
On success, the temporary role definition file is removed and the custom
role name is appended to the roles
array for later assignment.
9. Add Roles and Assign to Target Resource
#Add roles
roles+=( <SERVICE_PRINCIPAL_ROLES> )
for role in "${roles[@]}"; do
echo "Assign a role to the [$TARGET_RESOURCE]: [$role]"
role_assign=$(az role assignment create --role "$role" --assignee "$APP_ID" --scope "$TARGET_RESOURCE" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to assign role: $role to target resource [$TARGET_RESOURCE]: $role_assign"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
done
Purpose: The standard service principal roles (e.g., Reader, Network Contributor) are appended to the roles array alongside the custom VM scan
role created in the previous section. The script then iterates over all roles
and assigns each one to the AlgoSec service principal scoped to the target
Azure resource. If any role assignment fails, the error output from the CLI is
included in the failure message and the script exits.
10. Initialize Cloud App Analyzer Variables (Optional - Onboard CM Mitigation enabled)
#Azure region
REGION='<AZURE_REGION>'
#Prevasio host
PREVASIO_HOST='<PREVASIO_HOST>'
#Prevasio source code location
SOURCES_URL='<PREVASIO_SOURCES_URL>'
declare CURRENT_PREVASIO_RESOURCES_MD5
Purpose: Defines variables used exclusively by the Cloud App Analyzer deployment steps:
REGION— The Azure region where Cloud App Analyzer resources will be createdPREVASIO_HOST— The host of AlgoSec's Cloud App Analyzer APIsSOURCES_URL— The URL from which the Cloud App Analyzer application source files are downloadedCURRENT_PREVASIO_RESOURCES_MD5— Declared here for scope; populated later after downloading the source archive
11. Rollback Function (Optional - Onboard CM Mitigation enabled)
rollback_resources() {
local prevasio_hash=$1
local subscription_id=$2
local deploy_group_name=$3
local role_name=$4
local assignee_id=$5
echo "Rolling back resources in [$subscription_id]"
if [ -n "$assignee_id" ] && [ -n "$role_name" ]; then
echo "Removing role assignment..."
az role assignment delete --assignee "$assignee_id" --role "$role_name" --scope "/subscriptions/$subscription_id"
fi
if [ -n "$role_name" ]; then
echo "Deleting custom role definition..."
delete_role_if_exists "$role_name"
fi
if [ -n "$deploy_group_name" ]; then
echo "Deleting deployment group..."
az deployment group delete --name "$deploy_group_name" --resource-group "prevasio-$prevasio_hash-resource-group"
fi
echo "Deleting resource group..."
az group delete --name "prevasio-$prevasio_hash-resource-group" --yes
}
Purpose: Automates the cleanup and rollback of Azure resources associated with a failed deployment. Called with progressively more arguments depending on how far the deployment progressed:
- If an
assignee_idandrole_nameare provided, the role assignment is removed first. - If a
role_nameis provided, the custom role definition is deleted. - If a
deploy_group_nameis provided, the deployment group is deleted. - Finally, the resource group itself is deleted.
Note: Resource group deletion is synchronous (no --no-wait), ensuring the group is fully
removed before the function returns.
12. Delete Role Assignments Function (Optional - Onboard CM Mitigation enabled)
delete_role_assignments(){
local role_name="$1"
az role assignment list --role "$role_name" --output json |
jq -c '.[]' |
while IFS= read -r role_assignment; do
id=$(echo "$role_assignment" | jq -r '.id')
echo "Deleting role assignment: $id"
az role assignment delete --ids "$id"
done
}
Purpose: Deletes all Azure role assignments associated with a specified role name. It queries the full list of assignments for the given role and iterates over each one, deleting them individually by their resource ID. This is called as a prerequisite before deleting a custom role definition, since Azure does not allow deletion of a role that still has active assignments.
13. Delete Role If Exists Function (Optional - Onboard CM Mitigation enabled)
delete_role_if_exists() {
local role_name="$1"
get_role_scope() {
az role definition list --query "[?roleName==\`${role_name}\`].[assignableScopes[0]] | [0]" -o tsv
}
role_scope=$(get_role_scope)
if [ -n "$role_scope" ]; then
delete_role_assignments "$role_name"
az role definition delete --name "$role_name" --scope "$role_scope"
echo "Waiting for role to be deleted..."
local max_retries=5
local retries=0
while true; do
sleep 10
role_scope=$(get_role_scope)
if [ -z "$role_scope" ]; then
echo "Role '$role_name' deleted successfully."
break
else
retries=$((retries + 1))
echo "Role still exists. Retry $retries/$max_retries..."
if [ "$retries" -ge "$max_retries" ]; then
echo "Unable to delete role automatically after $max_retries attempts. Please delete manually:"
echo " az role definition delete --name \"$role_name\" --scope \"$role_scope\""
break
fi
fi
done
else
echo "Role '$role_name' does not exist. No deletion needed."
fi
}
Purpose: Checks whether a specified Azure custom role exists and, if so, removes all its assignments before deleting the role definition itself. Because Azure role propagation can be slow, the function polls every 10 seconds for up to 5 retries to confirm deletion. If the role cannot be deleted automatically after the retry limit, the user is prompted with the manual command to complete the cleanup.
14. Delete Key Vault If Exists Function (Optional - Onboard CM Mitigation enabled)
delete_kv_if_exists(){
local kv_name="prevasio-$prevasio_hash-kv"
if az keyvault show --name "$kv_name" --query "name" --output tsv 2>/dev/null | grep -q "$kv_name"; then
echo "Purging key vault '$kv_name'..."
az keyvault purge --name "$kv_name" --location "$REGION"
fi
if az keyvault show-deleted --name "$kv_name" --query "name" --output tsv 2>/dev/null | grep -q "$kv_name"; then
echo "Purging key vault '$kv_name'..."
az keyvault purge --name "$kv_name" --location "$REGION"
fi
}
Purpose: Checks whether an Azure Key Vault with the
deployment-specific name (prevasio-<hash>-kv)
exists — either as an active vault or in a soft-deleted state — and permanently
purges it if found. Both checks are necessary because Azure Key Vaults in
soft-delete mode block the creation of a new vault with the same name. The
purge operations are synchronous (no --no-wait),
ensuring the vault is fully removed before subsequent steps proceed.
15. Cleanup Existing Resources Function (Optional - Onboard CM Mitigation enabled)
cleanup_existing_resources() {
local subscription_id="$1"
az account set --subscription "$subscription_id"
local rg_name="prevasio-$prevasio_hash-resource-group"
echo "Checking for existing resources to clean in [$subscription_id]..."
# Delete resource group if it exists
if az group exists --name "$rg_name" | grep -q true; then
echo "Deleting resource group '$rg_name'..."
az group delete --name "$rg_name" --yes
fi
delete_kv_if_exists
}
Purpose: Cleans up any existing Cloud App Analyzer
resources in a given subscription before a fresh deployment. It sets the active
subscription context, then checks whether the deployment resource group exists.
If it does, the group is deleted synchronously (no --no-wait) before calling delete_kv_if_exists to also purge any
associated Key Vault. The prevasio_hash
variable is sourced from the enclosing deploy_code_to_subscription
function scope.
16. Azure CLI Error Handling Function (Optional - Onboard CM Mitigation enabled)
check_az_failure() {
local response="$1"
local context_message="$2"
local json_part
json_part=$(echo "$response" | sed -n '/^{/,$p')
if ! echo "$json_part" | jq -e . > /dev/null 2>&1; then
echo "$context_message failed: $response"
return 1
fi
if echo "$json_part" | jq -e 'has("error")' > /dev/null; then
local error_message
error_message=$(echo "$json_part" | jq -r '.error.message // "Unknown error"')
echo "$context_message failed: $error_message"
return 1
fi
return 0
}
Purpose: A shared helper that parses the captured output of
an Azure CLI command and determines whether it represents a failure. It strips
any non-JSON preamble from the response using sed, then uses jq
to validate that the remaining content is valid JSON and does not contain an error field. If either check fails, a
descriptive message is printed that includes the calling context and the error
detail. Callers use this function's return code to decide whether to trigger a
rollback.
17. Deploy Resources to Subscription Function (Optional - Onboard CM Mitigation enabled)
deploy_code_to_subscription() {
local subscription_id=$1
local prevasio_hash="${ALGOSEC_TENANT_ID:0:4}${subscription_id:0:4}"
az account set --subscription "$subscription_id"
echo "Deploying resources to [$subscription_id] subscription"
should_drop_resource_group=false
local resource_group_name="prevasio-$prevasio_hash-resource-group"
if [ "$(az group exists --name $resource_group_name)" == "true" ]; then
resource_group_version=$(az group show --name $resource_group_name --query "tags.version" --output tsv)
if [ -n "$resource_group_version" ] && [ "$resource_group_version" == "$CURRENT_PREVASIO_RESOURCES_MD5" ]; then
echo "The current onboarding in [$subscription_id] subscription is up to date. Skipping the resources creation."
return
fi
should_drop_resource_group=true
fi
if $should_drop_resource_group ; then
echo "Deleting resource group prevasio-$prevasio_hash-resource-group in [$subscription_id] subscription"
cleanup_existing_resources "$prevasio_hash" "$subscription_id"
fi
echo '{...}' > parameters.json # parameters JSON written dynamically at runtime
# Deleting Key Vault if it exists
delete_kv_if_exists
# Creating Resource Group
echo "Creating resource group prevasio-$prevasio_hash-resource-group in [$subscription_id] subscription"
group_response=$(az group create --name prevasio-$prevasio_hash-resource-group --location $REGION --tag version=$CURRENT_PREVASIO_RESOURCES_MD5 2>&1)
if ! check_az_failure "$group_response" "Resource group creation"; then
return
fi
# Creating Deployment Group
echo "Creating application resources in [$subscription_id] subscription"
deploy_group_response=$(az deployment group create --resource-group prevasio-$prevasio_hash-resource-group --template-file template.json --parameters parameters.json 2>&1)
if ! check_az_failure "$deploy_group_response" "Deployment group creation"; then
rollback_resources "$prevasio_hash" "$subscription_id"
return
fi
deploy_group_name=$(echo $deploy_group_response | jq -r '.name')
# Creating and assigning role
echo "Assigning roles to application in [$subscription_id] subscription"
role_name="Prevasio Application Role ($prevasio_hash)"
echo '{
"Name": "'"$role_name"'",
"IsCustom": true,
"Description": "Allows to create EventGrid subscriptions for ACR registries events.",
"Actions": [
"Microsoft.EventGrid/eventSubscriptions/read",
"Microsoft.ContainerRegistry/registries/read",
"Microsoft.EventGrid/eventSubscriptions/write",
"Microsoft.Web/sites/functions/write"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/'$subscription_id'"
]
}' > ./role_def.json
delete_role_if_exists "$role_name"
role_creation_response=$(az role definition create --role-definition role_def.json 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name"
return
fi
generated_role_name=$(echo "$role_creation_response" | sed '/^{/,$!d' | jq -r '.name')
assignee_id=$(az ad sp list --display-name prevasio-$prevasio_hash-app --query [].id --output tsv)
role_assignment_create_response=$(az role assignment create --assignee $assignee_id --role "$generated_role_name" --scope /subscriptions/$subscription_id 2>&1)
if ! check_az_failure "$role_assignment_create_response" "Role assignment creation"; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name" "$generated_role_name"
return
fi
# Creating and deploying function app
echo "Deploying application sources in [$subscription_id] subscription"
functionapp_deploy_response=$(az functionapp deployment source config-zip -g prevasio-$prevasio_hash-resource-group -n prevasio-$prevasio_hash-app --src function.zip --build-remote true 2>&1)
if ! check_az_failure "$functionapp_deploy_response" "Function app creation/deployment"; then
rollback_resources "$prevasio_hash" "$subscription_id" "$deploy_group_name" "$generated_role_name" "$assignee_id"
return
fi
echo "Application sources were deployed successfully"
}
Purpose: The core deployment function. It orchestrates the full Cloud App Analyzer resource provisioning for a single Azure subscription. The steps it performs are:
- Set subscription context — Switches the active Azure CLI subscription to the target.
- Generate a unique hash — Combines the first 4 characters of the AlgoSec tenant ID and the subscription ID to create a short, consistent namespace for all resources in this subscription.
- Version check — If the deployment
resource group already exists and its
versiontag matches the MD5 of the current source archive, the subscription is skipped as already up to date. - Cleanup stale resources — If the resource group exists but is outdated, it and its associated Key Vault are deleted before proceeding.
- Write deployment
parameters
— A
parameters.jsonfile is generated dynamically with subscription ID, tenant ID, hash, hosts, and credentials. - Purge Key Vault — Any existing or soft-deleted Key Vault with the expected name is purged to avoid naming conflicts.
- Create resource group — A new resource group tagged with the source version is created.
- Deploy ARM template — The Cloud App
Analyzer infrastructure is deployed via an ARM template (
template.json). On failure, a rollback is triggered. - Create and assign custom
EventGrid role
— A custom role (
Prevasio Application Role) granting EventGrid and Container Registry permissions is defined, any pre-existing role with the same name is deleted first, and then the new role is assigned to the Cloud App Analyzer service principal. On failure, a rollback is triggered. - Deploy function app — The Cloud App
Analyzer application sources (
function.zip) are deployed to the Azure Function App. On failure, a full rollback including role cleanup is triggered.
18. Download and Unpack Application Sources (Optional - Onboard CM Mitigation enabled)
mkdir -p prevasio-onboarding && rm -rf prevasio-onboarding/*
cd prevasio-onboarding
echo "Downloading Prevasio application resources"
wget -O sources.zip --header "user-agent: algosec/1.0" "${SOURCES_URL}?tenant_id=${ALGOSEC_TENANT_ID}"
if [[ -f "sources.zip" ]]; then
CURRENT_PREVASIO_RESOURCES_MD5=$(md5sum "sources.zip" | awk '{print $1}')
else
CURRENT_PREVASIO_RESOURCES_MD5=0
echo "Error: Failed to download sources.zip"
return
fi
unzip sources.zip
echo "Resources were downloaded successfully. Resources version: $CURRENT_PREVASIO_RESOURCES_MD5"
Purpose: Creates a clean temporary working directory (prevasio-onboarding), then downloads the
Cloud App Analyzer source archive from SOURCES_URL,
passing the AlgoSec tenant ID as a query parameter. The user-agent: algosec/1.0 header is
included so AlgoSec's servers can identify the request origin. If the download
succeeds, the MD5 checksum of the archive is computed and stored in CURRENT_PREVASIO_RESOURCES_MD5 — this
value is later used to tag deployed resource groups so the script can detect
whether a re-run needs to update existing resources. The archive is then
extracted in place.
19. Deploy to Target Resource (Subscription, Management Group, or Tenant Root) (Optional - Onboard CM Mitigation enabled)
# Single subscription
if [[ $TARGET_RESOURCE == /subscriptions/* ]]; then
deploy_code_to_subscription $TARGET_ID
else
if [[ $TARGET_RESOURCE == /providers/Microsoft.Management/managementGroups/* ]]; then
# Management group - Fetch subscriptions under mg
subscriptions=$(az account management-group subscription show-sub-under-mg --name $TARGET_ID | jq -r '.[].name')
else
# Tenant Root - Fetch all subscriptions in the tenant
subscriptions=$(az account list | jq -r '.[].id')
fi
if [ $? -ne 0 ]; then
echo "WARNING: Failed to retrieve subscriptions — skipping resource creation. The Cloud App Analyzer: CD Mitigation feature will be impacted"
else
# Deploy code to all relevant subscriptions
for subscription in $subscriptions; do
deploy_code_to_subscription $subscription
done
fi
fi
Purpose: Routes the deployment based on the type of Azure resource being onboarded:
- Single subscription — If
TARGET_RESOURCEis a subscription path (/subscriptions/...),deploy_code_to_subscriptionis called once for that subscription. - Management group — If
TARGET_RESOURCEis a management group path, all subscriptions under the management group are retrieved anddeploy_code_to_subscriptionis called for each one. - Tenant root — If
TARGET_RESOURCEis neither of the above (tenant root), all subscriptions in the tenant are retrieved viaaz account listand the function is called for each.
If subscription retrieval fails for a management group or tenant root
scenario, a WARNING is
printed and resource creation is skipped rather than causing a hard exit. This
is intentional — the core onboarding can still succeed, but the Cloud App
Analyzer CD Mitigation feature will not be active.
20. Cleanup Resources (Optional - Onboard CM Mitigation enabled)
cd ..
rm -rf prevasio-onboarding
Purpose: Removes the temporary prevasio-onboarding working directory
created in step 17, along with all downloaded and extracted files (source
archive, ARM templates, parameters files). This ensures no sensitive files or
large artifacts are left behind on the machine running the script.
21. Onboarding API Call
echo "Sending onboarding request to Algosec Cloud..."
response=$(curl -X POST "$ALGOSEC_CLOUD_ONBOARDING_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: $TOKEN" \
--silent \
-d '{ "azure_tenant":"'"$az_tenant"'", "supportChanges": "<SUPPORT_CHANGES>", "event": { "RequestType": "Create" } }')
Purpose: Makes a POST request to the AlgoSec Cloud onboarding API to register the Azure tenant. The request includes the Azure tenant ID and the onboarding token. A log line is printed before the request is sent. The --silent flag suppresses curl's default progress output so that only the response body is captured in response.
22. Response Handling
status=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.status')
message=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.message')
if [[ "$status" == 200 ]]; then
echo "The onboarding process is finished: $message"
echo "Press CTRL+D to close the terminal session"
else
echo "ERROR: The onboarding process has failed: $message"
fi
Purpose: Parses the JSON response from the onboarding API
to extract the status code
and message from the initialOnboardResult field. If the
status is 200, a success
message is displayed along with a prompt to close the terminal. Any other
status is treated as a failure and the error message from the API is displayed.
The comparison uses the [[
double-bracket construct for more robust string matching.
1. Shell Options and Initialize Variables
#!/bin/bash
# Capture pipe failures
set -o pipefail
#Algosec Cloud tenantId
ALGOSEC_TENANT_ID='<ALGOSEC_TENANT_ID>'
#Algosec Cloud multi-tenant application
APP_ID='<ALGOSEC_ONBOARDING_APP_ID>'
#Algosec Cloud onboarding URL
ALGOSEC_CLOUD_HOST='https://<HOST>'
ALGOSEC_CLOUD_ONBOARDING_URL="$ALGOSEC_CLOUD_HOST<ONBOARDING_PATH>"
#Token
TOKEN='<ONBOARDING_TOKEN>'
ADDITIONALS='<ALGOSEC_ADDITIONALS>'
#Target resource
TARGET_RESOURCE='<AZURE_TARGET_RESOURCE>'
TARGET_ID='<AZURE_TARGET_RESOURCE_ID>'
Purpose: The script begins by enabling set -o pipefail, which causes a pipeline
to return the exit code of the first failed command rather than the last one.
This ensures that errors within piped commands are not silently swallowed. It
then defines the core variables used throughout the script:
ALGOSEC_TENANT_ID— AlgoSec tenant IDAPP_ID— AlgoSec onboarding Azure AD application IDALGOSEC_CLOUD_HOST— The host of AlgoSec's APIsALGOSEC_CLOUD_ONBOARDING_URL— The URL for AlgoSec's onboarding APITOKEN— An authentication token for the APIADDITIONALS— Base64-encoded login credentialsTARGET_RESOURCE— The Azure resource path to be onboarded (subscription, management group, or tenant root)TARGET_ID— The Azure resource ID to be onboarded
2. Retrieve Azure Tenant ID
az_tenant=$(az account show --query tenantId -o tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to retrieve Azure tenant ID: $az_tenant"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
Purpose: Uses the Azure CLI to retrieve the tenant ID of
the currently authenticated Azure account. The --query tenantId -o tsv flags extract the value directly
without requiring an additional jq
parse. If the command fails (for example, if the user is not logged in), the
full error output is captured and displayed, and the script exits immediately.
3. Echo Information About the Target Resource
echo "Preparing to onboard the target resource [$TARGET_RESOURCE] of [$az_tenant] tenant"
Purpose: Prints a log line showing the target Azure resource being onboarded and the Azure tenant ID. This is used for visibility and traceability in the terminal session.
4. Retrieve Existing Service Principal
echo "Check if service principal already exists for Algosec Cloud AZ-AD Application"
sp=$(az ad sp list --filter "appId eq '$APP_ID'" --query "length(@)" --output tsv 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Unable to view service principals: $sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
Purpose: Checks whether a service principal already exists
in the tenant for the AlgoSec Azure AD application. The --query "length(@)" flag
counts the results directly in the CLI query rather than piping to jq, making the command more efficient.
If the user lacks permission to list service principals, the error output is
captured and the script exits with a clear message.
5. Create Service Principal
if [[ $sp -eq 0 ]]; then
echo "Service Principal not found"
echo "Creating service principal for Algosec Cloud AZ-AD Application"
create_sp=$(az ad sp create --id "$APP_ID" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create service principal for Algosec Cloud AZ-AD Application: $create_sp"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
echo "Service Principal created successfully"
else
echo "Service Principal found for Algosec Cloud AZ-AD Application"
fi
Purpose: If no existing service principal was found for the
AlgoSec application, this section creates one using az ad sp create. The output of the
create command is captured into create_sp
so that if creation fails, the error detail is included in the failure message.
If a service principal already exists, creation is skipped.
6. Initialize Roles Array
#Roles array to be assigned to service principal at target resource scope
roles=()
Purpose: Initializes an empty array that will accumulate all Azure roles to be assigned to the AlgoSec service principal at the target resource scope. Roles are added to this array by subsequent sections before being assigned in bulk.
7. Add Roles and Assign to Target Resource
#Add roles
roles+=( <SERVICE_PRINCIPAL_ROLES> )
for role in "${roles[@]}"; do
echo "Assign a role to the [$TARGET_RESOURCE]: [$role]"
role_assign=$(az role assignment create --role "$role" --assignee "$APP_ID" --scope "$TARGET_RESOURCE" 2>&1)
if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to assign role: $role to target resource [$TARGET_RESOURCE]: $role_assign"
echo "The onboarding process has failed — please review the error message above for details and verify your Azure configuration or permissions"
exit 1
fi
done
Purpose: The standard service principal roles (e.g., Reader, Network Contributor) are appended to the roles array alongside the custom VM scan
role created in the previous section. The script then iterates over all roles
and assigns each one to the AlgoSec service principal scoped to the target
Azure resource. If any role assignment fails, the error output from the CLI is
included in the failure message and the script exits.
8. Onboarding API Call
echo "Sending onboarding request to Algosec Cloud..."
response=$(curl -X POST "$ALGOSEC_CLOUD_ONBOARDING_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: $TOKEN" \
--silent \
-d '{ "azure_tenant":"'"$az_tenant"'", "supportChanges": "<SUPPORT_CHANGES>", "event": { "RequestType": "Create" } }')
Purpose: Makes a POST request to the AlgoSec Cloud onboarding API to register the Azure tenant. The request includes the Azure tenant ID and the onboarding token. A log line is printed before the request is sent. The --silent flag suppresses curl's default progress output so that only the response body is captured in response.
9. Response Handling
status=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.status')
message=$(echo $response | jq -r '.initialOnboardResult' | jq -r '.message')
if [[ "$status" == 200 ]]; then
echo "The onboarding process is finished: $message"
echo "Press CTRL+D to close the terminal session"
else
echo "ERROR: The onboarding process has failed: $message"
fi
Purpose: Parses the JSON response from the onboarding API
to extract the status code
and message from the initialOnboardResult field. If the
status is 200, a success
message is displayed along with a prompt to close the terminal. Any other
status is treated as a failure and the error message from the API is displayed.
The comparison uses the [[
double-bracket construct for more robust string matching.