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.

  1. Azure CLI version 2.50 or later, installed and authenticated (az login).
  2. 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.
  3. 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
  4. 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
  5. Qualys subscription credentials: Either an Access Token or an OAuth Client ID and Client Secret, and the Gateway URL for your Qualys platform POD.
  6. 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.

  1. 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"
  2. 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"
  3. 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.

  1. 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)
  2. 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)
  3. 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"
  4. 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)
  5. 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.

  1. 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)
  2. 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.

  1. 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.
  2. Confirm the target subscription registered: check the qualys-onboarded-subscriptions Cosmos DB container for a document with the target subscription ID.
  3. 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.
  4. 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.