Scan Serverless Azure Function Apps
This topic explains how to deploy the Qualys Azure Function scanning infrastructure using Bicep templates. The deployment consists of a service subscription stack (scanning infrastructure) and one target subscription stack per Azure subscription you want to scan.
Azure Function scan results are available from:
Container Security > Assets > Serverless Functions
- For an overview of how Azure Function scanning works, see Azure Function Scanning — Overview.
- Qualys Container Security supports cross subscription scanning under the same tenant.
Prerequisites
Before you begin, make sure you have the following.
- Azure CLI version 2.50 or later, installed and authenticated (
az login). - Azure Container Registry (ACR) accessible from the Container App. The Qualys Bicep templates use managed-identity-based image pulls, which require an ACR. Alternatively, supply containerRegistryUsername and containerRegistryPassword for a private non-ACR registry, or leave registry credentials empty for a public image.
- Service subscription resource group: Create this resource group before deployment. The service subscription Bicep template (qualys-cs-service-account-scanning.bicep) is resource-group scoped and cannot create its own resource group.
qualys-cs-service-account-scanning.bicep is present here: https://github.com/Qualys/Qualys-serverless-scanning-deployment/blob/main/azure-bicep/cs-cloud-deployment-templates-azure-bicep-1.0.0-0.zip
OR
Use this Curl command to download the Bicep:curl -L -o cs-cloud-deployment-templates-azure-bicep-1.0.0-0.zip \ https://github.com/Qualys/Qualys-serverless-scanning-deployment/blob/main/azure-bicep/cs-cloud-deployment-templates-azure-bicep-1.0.0-0.zip - Qualys cloud-operator image URI: Pull the Qualys Cloud Operator image and push it to the ACR repository:
$sudo docker pull qualys/cloud-operator:latest - Qualys subscription credentials: Either an Access Token or an OAuth Client ID and Client Secret, and the Gateway URL for your Qualys platform POD.
- Permissions: The account running the deployment commands requires the following.
Scope Role Required Reason Service subscription Contributor (minimum) Create Service Bus, Cosmos DB, Container App, Event Grid, and Monitor resources Service subscription (Service Bus namespace) User Access Administrator or Owner The service template creates role assignments for the cloud-operator managed identity Target subscription Contributor (minimum); Owner recommended Target template is subscription-scoped and creates its own resource group and role assignments Both subscriptions Microsoft.Authorization/roleAssignments/write Required for cross-subscription role assignment steps
Step 1: Push the cloud-operator Image to ACR
Skip this step if the cloud-operator image is already available in an ACR or another accessible registry. Set CLOUD_OPERATOR_IMAGE_URI directly and proceed to Step 2.
If you are using ACR, create the registry, tag the image, and push it.
- Set the required variables.
SERVICE_SUBSCRIPTION_ID="<service-subscription-id>" SERVICE_RESOURCE_GROUP="qualys-cloud-scanning-rg" LOCATION="eastus" ACR_NAME="qualyscloudscanningacr" SOURCE_IMAGE="<existing-registry>/cloud-operator:latest" IMAGE_TAG="cloud-operator:latest" - Create the resource group and ACR.
az group create --name "$SERVICE_RESOURCE_GROUP" --location "$LOCATION" --subscription "$SERVICE_SUBSCRIPTION_ID" az acr create \ --name "$ACR_NAME" \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --sku Basic \ --location "$LOCATION" - Tag and push the image.
ACR_LOGIN_SERVER=$(az acr show --name "$ACR_NAME" --subscription "$SERVICE_SUBSCRIPTION_ID" --query loginServer -o tsv) docker pull "$SOURCE_IMAGE" docker tag "$SOURCE_IMAGE" "$ACR_LOGIN_SERVER/$IMAGE_TAG" az acr login --name "$ACR_NAME" --subscription "$SERVICE_SUBSCRIPTION_ID" docker push "$ACR_LOGIN_SERVER/$IMAGE_TAG" CLOUD_OPERATOR_IMAGE_URI="$ACR_LOGIN_SERVER/$IMAGE_TAG"
Step 2: Deploy the Service Subscription Stack
The service subscription stack deploys the cloud-operator Container App, Service Bus queues, Cosmos DB, Event Grid, and associated infrastructure. Deploy it once in the designated service subscription.
Azure allows only one subscription-scoped Event Grid system topic per subscription. Before deploying, check whether the service subscription already has one. If it does, pass its resource ID via existingEventGridSystemTopicResourceId so the template reuses it instead of creating a duplicate.
- Check for an existing subscription-scoped Event Grid system topic.
EXISTING_EVENT_GRID_TOPIC_ID=$(az eventgrid system-topic list \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --query "[?topicType=='Microsoft.Resources.Subscriptions' && source=='/subscriptions/${SERVICE_SUBSCRIPTION_ID}'].id | [0]" \ -o tsv) - Retrieve the ACR resource ID (if using ACR).
ACR_RESOURCE_ID=$(az acr show --name "$ACR_NAME" --subscription "$SERVICE_SUBSCRIPTION_ID" --query id -o tsv)
- Deploy the service subscription stack. The deployment includes a retry loop to handle occasional Azure RBAC propagation delays.
az deployment group create \ --name "qualys-cloud-scanning" \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --template-file qualys-cs-service-account-scanning.bicep \ --parameters qualys-cs-service-account-params.bicepparam \ --parameters \ cloudOperatorImageUri=$CLOUD_OPERATOR_IMAGE_URI \ acrResourceId="$ACR_RESOURCE_ID" \ gatewayUrl="<your-qualys-gateway-url>" \ qualysAccessToken=$QUALYS_ACCESS_TOKEN \ existingEventGridSystemTopicResourceId="$EXISTING_EVENT_GRID_TOPIC_ID" - After deployment, capture the outputs needed for the target subscription stack.
OPERATOR_PRINCIPAL_ID=$(az deployment group show \ --name "qualys-cloud-scanning" \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --query "properties.outputs.cloudOperatorIdentityPrincipalId.value" \ --output tsv) FUNCTION_EVENT_QUEUE_ID=$(az deployment group show \ --name "qualys-cloud-scanning" \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --query "properties.outputs.functionEventQueueId.value" \ --output tsv) SERVICE_BUS_NAMESPACE_NAME=$(az deployment group show \ --name "qualys-cloud-scanning" \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --query "properties.outputs.serviceBusNamespaceName.value" \ --output tsv) - Retrieve the send-only connection string for the onboarding queue.
ONBOARDING_QUEUE_CONNECTION_STRING=$(az servicebus queue authorization-rule keys list \ --resource-group "$SERVICE_RESOURCE_GROUP" \ --namespace-name "$SERVICE_BUS_NAMESPACE_NAME" \ --queue-name qualys-onboarding-events \ --name qualys-target-onboarding-sender \ --subscription "$SERVICE_SUBSCRIPTION_ID" \ --query primaryConnectionString -o tsv)
Step 3: Deploy the Target Subscription Stack
Run the target subscription stack once per Azure subscription you want to scan. The stack deploys Event Grid forwarding rules and an onboarding notifier that automatically registers the subscription with the service subscription.
The target subscription Bicep template is deployed at subscription scope (az deployment sub create), not resource group scope. It creates its own resource group. Check the target subscription for an existing subscription-scoped Event Grid system topic before deploying, using the same constraint that applies in Step 2.
- Check the target subscription for an existing subscription-scoped Event Grid system topic.
TARGET_SUBSCRIPTION_ID="<target-subscription-id>" TARGET_REGION="eastus" EXISTING_TARGET_EVENT_GRID_TOPIC_ID=$(az eventgrid system-topic list \ --subscription "$TARGET_SUBSCRIPTION_ID" \ --query "[?topicType=='Microsoft.Resources.Subscriptions' && source=='/subscriptions/${TARGET_SUBSCRIPTION_ID}'].id | [0]" \ -o tsv) - Deploy the target subscription stack. The deployment includes a retry loop for RBAC propagation delays.
az deployment sub create \ --name "qualys-target-subscription-resources" \ --location "$TARGET_REGION" \ --subscription "$TARGET_SUBSCRIPTION_ID" \ --template-file qualys-cs-target-subscription-resources.bicep \ --parameters qualys-cs-target-subscription-resources.bicepparam \ --parameters \ cloudOperatorPrincipalId="$OPERATOR_PRINCIPAL_ID" \ targetRegion="$TARGET_REGION" \ functionEventQueueResourceId="$FUNCTION_EVENT_QUEUE_ID" \ existingEventGridSystemTopicResourceId="$EXISTING_TARGET_EVENT_GRID_TOPIC_ID" \ serviceSubscriptionId="$SERVICE_SUBSCRIPTION_ID" \ serviceSubscriptionRegion="$SERVICE_SUBSCRIPTION_REGION" \ onboardingQueueConnectionString="$ONBOARDING_QUEUE_CONNECTION_STRING"On successful deployment, the onboarding notifier fires automatically and registers the subscription with the service subscription. No separate trigger step is needed.
Verify the Deployment
After deployment, verify that the infrastructure is working correctly.
- Confirm the onboarding message was sent and consumed: go to your Service Bus namespace > Queues > qualys-onboarding-events and confirm the active message count incremented then returned to zero.
- Confirm the target subscription registered: check the qualys-onboarded-subscriptions Cosmos DB container for a document with the target subscription ID.
- Create a test Function App in the target subscription and confirm a scan job appears in the qualys-azure-function-scan-jobs Cosmos DB container and in the cloud-operator Container App logs.
- View scan results in Container Security > Assets > Serverless Functions.
To tail the cloud-operator Container App logs in real time:
az containerapp logs show \
--name qualys-cloud-operator \
--resource-group "$SERVICE_RESOURCE_GROUP" \
--subscription "$SERVICE_SUBSCRIPTION_ID" \
--follow \
--format text
Parameters Reference
Service Subscription Stack (qualys-cs-service-account-scanning.bicep)
| Parameter | Required | Default | Description |
|---|---|---|---|
| cloudOperatorImageUri | Yes | — | Container image URI for the cloud-operator Container App. |
| acrResourceId | Yes (if using ACR) | (empty) | Resource ID of the ACR holding the cloud-operator image. Grants AcrPull to the Container App managed identity. Leave empty for non-ACR registries. |
| containerRegistryUsername | Yes (if using private non-ACR registry) | (empty) | Username for a private non-ACR registry. Ignored when acrResourceId is set. |
| containerRegistryPassword | Yes (if using private non-ACR registry) | (empty) | Password or access token for a private non-ACR registry. Ignored when acrResourceId is set. |
| gatewayUrl | Yes | — | Qualys platform gateway URL for your POD. |
| qualysAccessToken | Yes (or OAuth) | — | Qualys access token for authentication. |
| qualysClientId | Yes (or token) | — | OAuth client ID. |
| qualysClientSecret | Yes (or token) | — | OAuth client secret. |
| scanServiceSubscription | No | true | When true, the cloud-operator scans Function Apps in the service subscription in addition to target subscriptions. |
| existingEventGridSystemTopicResourceId | No | (empty) | Resource ID of an existing subscription-scoped Event Grid system topic in the service subscription. Pass this to prevent duplicate system topic creation. Leave empty to create a new one. |
| reconciliationIntervalMin | No | 1440 | Minutes between reconciliation passes that detect and clean up deleted target subscription stacks. Set to 0 to disable. |
| scanTypes | No | pkg,fileinsight,aiinsight | qscanner scan types to run. |
| logLevel | No | info | Log level for the cloud-operator Container App. |
| proxy | No | — | HTTP proxy URL, if required. |
Target Subscription Stack (qualys-cs-target-subscription-resources.bicep)
| Parameter | Required | Default | Description |
|---|---|---|---|
| cloudOperatorPrincipalId | Yes | — | Managed identity principal ID of the cloud-operator from the service subscription deployment output. |
| targetRegion | Yes | — | Azure region where the target subscription resource group is created. |
| functionEventQueueResourceId | Yes | — | Resource ID of the Function Events Service Bus queue in the service subscription, from the service subscription deployment output. |
| serviceSubscriptionId | Yes | — | Azure Subscription ID of the service subscription. |
| serviceSubscriptionRegion | Yes | — | Azure region where the service subscription stack is deployed. |
| onboardingQueueConnectionString | Yes | — | Send-only SAS connection string for the qualys-onboarding-events Service Bus queue in the service subscription. Used by the onboarding notifier to register this target subscription. |
| targetResourceGroupName | No | qualys-target-scanning-rg | Name of the resource group the target template creates in the target subscription. |
| existingEventGridSystemTopicResourceId | No | (empty) | Resource ID of an existing subscription-scoped Event Grid system topic in the target subscription. Pass this to prevent duplicate system topic creation. |
Destroy the Stacks
This section explains how you can destroy the Service and Target account stack. Always destroy the target account stack first, then the service account stack.
- This action is destructive and irreversible. Verify the resource group name and subscription ID before running.
To delete a target account stack,
TARGET_RESOURCE_GROUP="qualys-target-scanning-rg"
az group delete \
--name "$TARGET_RESOURCE_GROUP" \
--subscription "$TARGET_SUBSCRIPTION_ID" \
--yes --no-wait
The cloud-operator runs a reconciliation loop every 24 hours (configurable via reconciliationIntervalMin) that detects deleted target stacks and removes the corresponding entries from Cosmos DB automatically. No separate offboarding step is required.
To delete a service account stack,
az group delete \
--name "$SERVICE_RESOURCE_GROUP" \
--subscription "$SERVICE_SUBSCRIPTION_ID" \
--yes --no-wait
Deleting the service resource group removes the cloud-operator Container App, Service Bus, Cosmos DB, and Event Grid resources. If an existing Event Grid system topic was reused (existingEventGridSystemTopicResourceId), that system topic is not deleted; only the Event Grid subscription on it is removed.