├── applications ├── .funcignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── PolicyStates │ ├── host.json │ ├── function.json │ └── __init__.py ├── host.json ├── requirements.txt ├── NotificationHandler │ ├── function.json │ ├── test_event_ok.json │ ├── test_event_failure.json │ ├── __init__.py │ └── test_func.py ├── .gitignore ├── getting_started.md └── README.md ├── images ├── diagram_policy.png ├── function_name.jpg ├── managed_sp_config.jpg ├── deployed_resources.jpg ├── notification_endpoint.png └── notification_endpoint_url.jpg ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── SUPPORT.md ├── .github └── workflows │ ├── infrastructure-deployment.yml │ └── code-deployment.yml ├── SECURITY.md ├── azure ├── main.bicep └── function.bicep ├── .gitignore └── README.md /applications/.funcignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /images/diagram_policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/diagram_policy.png -------------------------------------------------------------------------------- /images/function_name.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/function_name.jpg -------------------------------------------------------------------------------- /images/managed_sp_config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/managed_sp_config.jpg -------------------------------------------------------------------------------- /images/deployed_resources.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/deployed_resources.jpg -------------------------------------------------------------------------------- /images/notification_endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/notification_endpoint.png -------------------------------------------------------------------------------- /images/notification_endpoint_url.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-marketplace-management-extras/HEAD/images/notification_endpoint_url.jpg -------------------------------------------------------------------------------- /applications/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /applications/PolicyStates/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[2.*, 3.0.0)" 6 | } 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "applications" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /applications/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Python Functions", 6 | "type": "python", 7 | "request": "attach", 8 | "port": 9091, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /applications/PolicyStates/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "entryPoint": "main", 4 | "bindings": [ 5 | { 6 | "authLevel": "Anonymous", 7 | "name": "mytimer", 8 | "type": "timerTrigger", 9 | "direction": "in", 10 | "schedule": "0 0 */6 * * *" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /applications/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.3.0, 4.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /applications/requirements.txt: -------------------------------------------------------------------------------- 1 | # Do not include azure-functions-worker as it may conflict with the Azure Functions platform 2 | aiohttp~=3.8.4 3 | aiolimiter~=1.0.0 4 | azure-data-tables~=12.4.2 5 | azure-functions~=1.13.2 6 | azure-identity~=1.12.0 7 | azure-mgmt-policyinsights~=1.0.0 8 | azure-core~=1.26.4 9 | azure-mgmt-resource~=22.0.0 10 | azure-monitor-ingestion~=1.0.1 11 | requests~=2.28.2 -------------------------------------------------------------------------------- /applications/NotificationHandler/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "Anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": [ 10 | "post" 11 | ], 12 | "route": "resource" 13 | }, 14 | { 15 | "type": "http", 16 | "direction": "out", 17 | "name": "$return" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /applications/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.scmDoBuildDuringDeployment": true, 4 | "azureFunctions.pythonVenv": ".venv", 5 | "azureFunctions.projectLanguage": "Python", 6 | "azureFunctions.projectRuntime": "~4", 7 | "debug.internalConsoleOptions": "neverOpen", 8 | "python.testing.pytestArgs": [ 9 | "PolicyStates" 10 | ], 11 | "python.testing.unittestEnabled": false, 12 | "python.testing.pytestEnabled": true 13 | } -------------------------------------------------------------------------------- /applications/NotificationHandler/test_event_ok.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventType": "PUT", 3 | "applicationId": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test/providers/Microsoft.Solutions/applications/test3", 4 | "eventTime": "2022-03-14T19:20:08.1707163Z", 5 | "provisioningState": "Succeeded", 6 | "plan": { 7 | "name": "msft-insights-poc-managed", 8 | "product": "msft-insights-poc-preview", 9 | "publisher": "test_test_agcicemarketplace1616064700629", 10 | "version": "0.1.20" 11 | } 12 | } -------------------------------------------------------------------------------- /applications/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | .pytest_cache/ 17 | 18 | /tools/NuGet.exe 19 | /App_Data 20 | /secrets 21 | /data 22 | .secrets 23 | appsettings.json 24 | local.settings.json 25 | 26 | node_modules 27 | dist 28 | 29 | # Local python packages 30 | .python_packages/ 31 | 32 | # Python Environments 33 | .env 34 | .venv 35 | env/ 36 | venv/ 37 | ENV/ 38 | env.bak/ 39 | venv.bak/ 40 | 41 | # Byte-compiled / optimized / DLL files 42 | __pycache__/ 43 | *.py[cod] 44 | *$py.class 45 | -------------------------------------------------------------------------------- /applications/NotificationHandler/test_event_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventType": "PUT", 3 | "applicationId": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test/providers/Microsoft.Solutions/applications/test3", 4 | "eventTime": "2022-03-14T19:20:08.1707163Z", 5 | "provisioningState": "Failed", 6 | "plan": { 7 | "name": "msft-insights-poc-managed", 8 | "product": "msft-insights-poc-preview", 9 | "publisher": "test_test_agcicemarketplace1616064700629", 10 | "version": "0.1.20" 11 | }, 12 | "error": { 13 | "code": "ErrorCode", 14 | "message": "error message", 15 | "details": [ 16 | { 17 | "code": "DetailedErrorCode", 18 | "message": "error message" 19 | } 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /applications/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-python-watch", 8 | "isBackground": true, 9 | "dependsOn": "pip install (functions)" 10 | }, 11 | { 12 | "label": "pip install (functions)", 13 | "type": "shell", 14 | "osx": { 15 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 16 | }, 17 | "windows": { 18 | "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" 19 | }, 20 | "linux": { 21 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 22 | }, 23 | "problemMatcher": [] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /.github/workflows/infrastructure-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Infrastructure deployment 2 | # description: Deploys the infrastructure for apps 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | concurrency: 8 | # Serializing this workflow. 9 | group: ${{ github.workflow }} 10 | 11 | jobs: 12 | infra: 13 | name: Deploy infrastructure 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: 'Az CLI login' 19 | uses: azure/login@v1 20 | with: 21 | creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}' 22 | 23 | - name: Set subscription 24 | run: | 25 | az account set -s ${{ vars.SUBSCRIPTION_NAME }} 26 | 27 | - name: Get git short sha 28 | run: | 29 | echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 30 | 31 | - uses: supplypike/setup-bin@v3 32 | name: Install bicep 33 | with: 34 | uri: https://github.com/Azure/bicep/releases/download/v0.15.31/bicep-linux-x64 35 | name: bicep 36 | version: "0.15.31" 37 | 38 | - name: Build Bicep templates 39 | working-directory: ./azure 40 | run: | 41 | bicep build main.bicep 42 | 43 | - name: Create resource group 44 | run: | 45 | az group create \ 46 | -n ${{ vars.RESOURCE_GROUP_NAME }} \ 47 | -l ${{ vars.LOCATION }} 48 | 49 | - name: Deploy infrastructure template 50 | uses: azure/arm-deploy@v1 51 | id: deploy 52 | with: 53 | scope: "resourcegroup" 54 | resourceGroupName: ${{ vars.RESOURCE_GROUP_NAME }} 55 | template: ./azure/main.json 56 | parameters: > 57 | appName=${{ vars.APP_NAME }} 58 | spClientId=${{ secrets.SP_CLIENT_ID }} 59 | spClientSecret=${{ secrets.SP_CLIENT_SECRET }} 60 | spTenantId=${{ secrets.SP_TENANT_ID }} 61 | storageAccountTableName=${{ vars.STORAGE_ACCOUNT_TABLE_NAME }} 62 | emailAddress=${{ vars.EMAIL_ADDRESS }} 63 | receiversName=${{ vars.RECEIVERS_NAME }} 64 | deploymentName: git-${{ env.SHA_SHORT }} 65 | 66 | - name: Show function name 67 | run: | 68 | echo "✅ Function name: ${{ steps.deploy.outputs.policyStatesCollectorFunctionName }}" -------------------------------------------------------------------------------- /applications/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Azure Function 2 | 3 | ## Last updated: March 8th 2021 4 | 5 | ### Project Structure 6 | 7 | The main project folder () can contain the following files: 8 | 9 | * **local.settings.json** - Used to store app settings and connection strings when running locally. 10 | This file doesn't get published to Azure. To learn more, see [local.settings.file](https://aka.ms/azure-functions/python/local-settings). 11 | * **requirements.txt** - Contains the list of Python packages the system installs when publishing to Azure. 12 | * **host.json** - Contains global configuration options that affect all functions in a function app. 13 | This file does get published to Azure. Not all options are supported when running locally. To learn more, see [host.json](https://aka.ms/azure-functions/python/host.json). 14 | * **.vscode/** - (Optional) Contains store VSCode configuration. To learn more, see [VSCode setting](https://aka.ms/azure-functions/python/vscode-getting-started). 15 | * **.venv/** - (Optional) Contains a Python virtual environment used by local development. 16 | * **Dockerfile** - (Optional) Used when publishing your project in a [custom container](https://aka.ms/azure-functions/python/custom-container). 17 | * **tests/** - (Optional) Contains the test cases of your function app. For more information, see [Unit Testing](https://aka.ms/azure-functions/python/unit-testing). 18 | * **.funcignore** - (Optional) Declares files that shouldn't get published to Azure. Usually, 19 | this file contains .vscode/ to ignore your editor setting, .venv/ to ignore local Python virtual environment, tests/ 20 | to ignore test cases, and local.settings.json to prevent local app settings being published. 21 | 22 | Each function has its own code file and binding configuration file ([**function.json**](https://aka.ms/azure-functions/python/function.json)). 23 | 24 | ### Developing your first Python function using VS Code 25 | 26 | If you have not already, please checkout our [quickstart](https://aka.ms/azure-functions/python/quickstart) 27 | to get you started with Azure Functions developments in Python. 28 | 29 | ### Publishing your function app to Azure 30 | 31 | For more information on deployment options for Azure Functions, please visit this [guide](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python#publish-the-project-to-azure). 32 | 33 | ### Next Steps 34 | 35 | * To learn more about developing Azure Functions, please visit [Azure Functions Developer Guide](https://aka.ms/azure-functions/python/developer-guide). 36 | 37 | * To learn more specific guidance on developing Azure Functions with Python, 38 | please visit [Azure Functions Developer Python Guide](https://aka.ms/azure-functions/python/python-developer-guide). 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/code-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Code deployment 2 | # description: Deploys Notification Endpoint and Policy States code 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | functionAppName: 8 | description: "function app name that was created by infrastructure deployment workflow" 9 | required: true 10 | type: string 11 | env: 12 | PYTHON_VERSION: "3.9" 13 | 14 | concurrency: 15 | # Serializing this workflow. 16 | group: ${{ github.workflow }} 17 | 18 | jobs: 19 | test: 20 | name: Test NotificationHandler function app 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ env.PYTHON_VERSION }} 29 | cache: 'pip' 30 | cache-dependency-path: 'applications/requirements.txt' 31 | 32 | - name: Install dependencies 33 | working-directory: ./applications 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install pytest 37 | pip install -r requirements.txt 38 | 39 | - name: Test with pytest 40 | working-directory: ./applications 41 | run: | 42 | pytest NotificationHandler -v 43 | 44 | app: 45 | name: Deploy function apps 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | 50 | - name: 'Az CLI login' 51 | uses: azure/login@v1 52 | with: 53 | creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}' 54 | 55 | - name: Set subscription 56 | run: | 57 | az account set -s ${{ vars.SUBSCRIPTION_NAME }} 58 | 59 | - name: Set up Python 60 | uses: actions/setup-python@v4 61 | with: 62 | python-version: ${{ env.PYTHON_VERSION }} 63 | 64 | - name: Resolve function dependencies 65 | working-directory: ./applications 66 | run: | 67 | python -m pip install --upgrade pip 68 | pip install pytest 69 | pip install -r requirements.txt --target=".python_packages/lib/site-packages" 70 | 71 | - name: Deploy notification endpoint function app 72 | uses: Azure/functions-action@v1 73 | with: 74 | app-name: ${{ github.event.inputs.functionAppName }} 75 | package: ./applications 76 | 77 | - name: Show function invoke URL 78 | run: | 79 | invoke_url=$(az functionapp function show \ 80 | -g ${{ vars.RESOURCE_GROUP_NAME }} \ 81 | -n ${{ github.event.inputs.functionAppName }} \ 82 | --function-name NotificationHandler \ 83 | -o tsv --query invokeUrlTemplate) 84 | 85 | echo "✅ Function invoke URL: $invoke_url" 86 | -------------------------------------------------------------------------------- /azure/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | var logAnalyticsWorkspaceName = 'logAnalyticsMarketplace' 4 | var policyStatesTableName = 'PolicyComplianceStates_CL' 5 | var streamDeclaration = 'Custom-${policyStatesTableName}' 6 | 7 | param appName string 8 | param location string = resourceGroup().location 9 | param storageAccountTableName string 10 | param emailAddress string 11 | param receiversName string 12 | 13 | @secure() 14 | param spClientId string 15 | @secure() 16 | param spClientSecret string 17 | @secure() 18 | param spTenantId string 19 | 20 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 21 | name: logAnalyticsWorkspaceName 22 | location: location 23 | properties: { 24 | sku: { 25 | name: 'PerGB2018' 26 | } 27 | retentionInDays: 30 28 | } 29 | } 30 | 31 | resource containerLogTable 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = { 32 | parent: logAnalyticsWorkspace 33 | name: 'ContainerLog' 34 | properties: { 35 | plan: 'Analytics' 36 | retentionInDays: 7 37 | totalRetentionInDays: 7 38 | policyStatesTableName: policyStatesTableName 39 | } 40 | } 41 | 42 | resource policyContainerLogTable 'Microsoft.OperationalInsights/workspaces/tables@2021-12-01-preview' = { 43 | parent: logAnalyticsWorkspace 44 | name: policyStatesTableName 45 | properties: { 46 | plan: 'Analytics' // Needs to be Analytics plan to configure alerts on this data 47 | schema: { 48 | name: policyStatesTableName 49 | columns: [ 50 | { 51 | name: 'Policy_assignment_id' 52 | type: 'string' 53 | } 54 | { 55 | name: 'Policy_assignment_name' 56 | type: 'string' 57 | } 58 | { 59 | name: 'Is_compliant' 60 | type: 'boolean' 61 | } 62 | { 63 | name: 'TimeGenerated' 64 | type: 'dateTime' 65 | } 66 | ] 67 | } 68 | retentionInDays: 7 69 | totalRetentionInDays: 7 70 | } 71 | } 72 | // action groups 73 | resource actionGroupAlerts 'Microsoft.Insights/actionGroups@2022-06-01' = { 74 | name: 'actionGroup' 75 | location: 'global' 76 | properties: { 77 | enabled: true 78 | groupShortName: 'shortNsme' 79 | emailReceivers: [ 80 | { 81 | emailAddress: emailAddress 82 | name: receiversName 83 | useCommonAlertSchema: false 84 | } 85 | ] 86 | } 87 | } 88 | 89 | // non compliance policy 90 | resource nonCompliantPoliciesAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { 91 | name: 'nonCompliantPoliciesAlert' 92 | location: location 93 | kind: 'LogAlert' 94 | properties: { 95 | actions: { 96 | actionGroups: [ 97 | actionGroupAlerts.id 98 | ] 99 | } 100 | criteria: { 101 | allOf: [ 102 | { 103 | operator: 'LessThanOrEqual' 104 | query: 'PolicyComplianceStates_CL | summarize arg_max(TimeGenerated,*) by Policy_assignment_id | where Is_compliant==false' 105 | threshold: 0 106 | timeAggregation: 'Count' 107 | } 108 | ] 109 | } 110 | displayName: 'Non-compliant policies' 111 | enabled: true 112 | evaluationFrequency: 'PT5M' 113 | skipQueryValidation: true 114 | scopes: [ 115 | logAnalyticsWorkspace.id 116 | ] 117 | severity: 1 118 | windowSize: 'PT5M' 119 | } 120 | } 121 | 122 | module policyStatesCollectorFunction 'function.bicep' = { 123 | name: 'marketplacefunction' 124 | params: { 125 | location: location 126 | logAnalyticsWorkspaceName: logAnalyticsWorkspace.name 127 | logAnalyticsWorkspaceId: logAnalyticsWorkspace.id 128 | streamDeclaration: streamDeclaration 129 | streamName: streamDeclaration 130 | spClientId: spClientId 131 | spClientSecret: spClientSecret 132 | spTenantId: spTenantId 133 | storageAccountTableName: storageAccountTableName 134 | appName: appName 135 | } 136 | dependsOn: [ 137 | containerLogTable 138 | ] 139 | } 140 | 141 | output policyStatesCollectorFunctionName string = policyStatesCollectorFunction.outputs.functionAppName 142 | -------------------------------------------------------------------------------- /applications/PolicyStates/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from azure.mgmt.policyinsights.aio import PolicyInsightsClient 3 | from azure.mgmt.policyinsights.models import QueryOptions 4 | from azure.identity.aio import ManagedIdentityCredential 5 | from azure.identity.aio import ClientSecretCredential 6 | import azure.functions as func 7 | import asyncio 8 | from collections import AsyncIterable 9 | 10 | from azure.monitor.ingestion.aio import LogsIngestionClient 11 | from azure.core.exceptions import HttpResponseError 12 | from azure.data.tables.aio import TableClient 13 | 14 | import logging 15 | import os 16 | import json 17 | from typing import List 18 | 19 | DATA_COLLECTION_ENDPOINT = str(os.environ["DATA_COLLECTION_ENDPOINT"]) 20 | DATA_COLLECTION_IMMUTABLE_ID = str( 21 | os.environ["DATA_COLLECTION_IMMUTABLE_ID"]) 22 | STREAM_NAME = str(os.environ["STREAM_NAME"]) 23 | AZURE_TENANT_ID = str(os.environ["AZURE_TENANT_ID"]) 24 | AZURE_CLIENT_ID = str(os.environ["AZURE_CLIENT_ID"]) 25 | AZURE_CLIENT_SECRET = str(os.environ["AZURE_CLIENT_SECRET"]) 26 | CONNECTION_STRING = str(os.environ["AzureWebJobsStorage"]) 27 | TABLE_NAME = str(os.environ["TABLE_NAME"]) 28 | 29 | 30 | async def get_resource_group_policies(policy_client, subscription_id, resource_group_name) -> AsyncIterable: 31 | # Do not change or remove filter. It is used to query policies specifically assigned for RG 32 | scope = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" 33 | filter = "PolicyAssignmentScope eq '{}'".format(scope) 34 | query_options = QueryOptions(filter=filter) 35 | 36 | # Only need latest evaluated policies 37 | return policy_client.policy_states.list_query_results_for_resource_group( 38 | policy_states_resource='latest', 39 | subscription_id=subscription_id, 40 | resource_group_name=resource_group_name, 41 | query_options=query_options) 42 | 43 | 44 | async def get_policies(client_credential, subscription_id, resource_group_name) -> List[dict] | None: 45 | try: 46 | async with PolicyInsightsClient( 47 | client_credential, subscription_id=subscription_id) as policy_client: 48 | 49 | policy_assignment_states = await get_resource_group_policies(policy_client, subscription_id, resource_group_name) 50 | 51 | # Build up body object with only necessary values 52 | policies: List[dict] = [] 53 | 54 | async for policy in policy_assignment_states: 55 | policies.append({ 56 | 'Policy_assignment_name': policy.policy_assignment_name, 57 | 'Policy_assignment_id': policy.policy_assignment_id, 58 | 'Is_compliant': policy.is_compliant, 59 | 'TimeGenerated': json.dumps(policy.timestamp, default=str) 60 | }) 61 | 62 | logging.info( 63 | f"Policies amount: {str(len(policies))} for RG {resource_group_name}") 64 | 65 | if len(policies) == 0: 66 | logging.warning("There are not any policies") 67 | else: 68 | return policies 69 | except Exception as e: 70 | msg = f"Failed to get/filter policies for RG {resource_group_name}, error: {e}" 71 | logging.error(msg) 72 | 73 | 74 | async def run() -> None: 75 | all_applications_policies_to_upload = [] 76 | async with ClientSecretCredential( 77 | AZURE_TENANT_ID, 78 | AZURE_CLIENT_ID, AZURE_CLIENT_SECRET 79 | ) as client_credential, TableClient.from_connection_string( 80 | CONNECTION_STRING, TABLE_NAME 81 | ) as table_client: 82 | managed_applications = table_client.list_entities() 83 | 84 | async for application in managed_applications: 85 | result = get_policies( 86 | client_credential, application["subscription_id"], application["mrg_name"]) 87 | all_applications_policies_to_upload.append(result) 88 | 89 | policies_upload = await asyncio.gather(*all_applications_policies_to_upload, return_exceptions=True) 90 | 91 | # Upload policies 92 | async with ManagedIdentityCredential() as ingestion_credential, LogsIngestionClient( 93 | endpoint=DATA_COLLECTION_ENDPOINT, credential=ingestion_credential, logging_enable=True) as logs_client: 94 | try: 95 | await logs_client.upload( 96 | rule_id=DATA_COLLECTION_IMMUTABLE_ID, stream_name=STREAM_NAME, logs=policies_upload) 97 | logging.info(f'Uploaded {len(policies_upload)} policies') 98 | 99 | except HttpResponseError as e: 100 | logging.error(f"Upload failed: {e}") 101 | 102 | 103 | def main(mytimer: func.TimerRequest) -> None: 104 | asyncio.run(run()) 105 | -------------------------------------------------------------------------------- /applications/NotificationHandler/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import os 4 | 5 | import azure.functions as func 6 | from azure.identity import DefaultAzureCredential 7 | from azure.mgmt.resource import ApplicationClient 8 | from azure.data.tables import TableClient 9 | from azure.core.exceptions import ResourceExistsError, HttpResponseError 10 | 11 | 12 | def parse_resource_id(resource_id: str): 13 | 14 | pattern = "\/?subscriptions\/(?P[0-9a-z-]+)\/resourceGroups\/(?P[a-zA-Z0-9-_.()]+)(|\/providers\/Microsoft\.Solutions\/applications\/(?P[a-zA-Z0-9-_.()]+))$" 15 | m = re.match(pattern, resource_id) 16 | 17 | if not m: 18 | raise ValueError("Could not parse resource id") 19 | return ( 20 | m.group("subscription_id"), 21 | m.group("resource_group"), 22 | m.group("application_name") 23 | ) 24 | 25 | 26 | def main(req: func.HttpRequest) -> func.HttpResponse: 27 | CONNECTION_STRING = str(os.environ["AzureWebJobsStorage"]) 28 | TABLE_NAME = str(os.environ["TABLE_NAME"]) 29 | 30 | logging.info("Received webhook call from marketplace deployment") 31 | 32 | # Azure will only send POST requests 33 | if req.method != "POST": 34 | return func.HttpResponse(status_code=405) 35 | 36 | try: 37 | # Acquire a credential object for the app identity. When running in the cloud, 38 | # DefaultAzureCredential uses the app's managed identity (MSI) or user-assigned service principal. 39 | # When run locally, DefaultAzureCredential relies on environment variables named 40 | # AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, and AZURE_TENANT_ID. 41 | # 42 | # Make sure the identity used has been authorized in Partner Center to manage 43 | # the application. Check the following link for more information: 44 | # https://docs.microsoft.com/en-us/azure/marketplace/plan-azure-app-managed-app#choose-who-can-manage-the-application 45 | credential = DefaultAzureCredential() 46 | 47 | except Exception as e: 48 | msg = f"Could not authenticate: {e}" 49 | logging.error(msg) 50 | return func.HttpResponse(msg, status_code=500) 51 | 52 | try: 53 | req_body = req.get_json() 54 | logging.debug(f"Request body: {req_body}") 55 | application_id = req_body["applicationId"] 56 | event_type = req_body["eventType"] 57 | provisioning_state = req_body["provisioningState"] 58 | except (ValueError, KeyError, AttributeError) as e: 59 | msg = f"Could not parse request: {e}" 60 | logging.error(msg) 61 | return func.HttpResponse(msg, status_code=400) 62 | 63 | # Obtain app subscription id and resource group from webhook call 64 | try: 65 | ( 66 | app_subscription_id, 67 | app_resource_group, 68 | app_name 69 | ) = parse_resource_id(application_id) 70 | except ValueError as e: 71 | msg = f"Error obtaining app subscription and resource group: {e}" 72 | logging.error(msg) 73 | return func.HttpResponse(msg, status_code=500) 74 | 75 | logging.info( 76 | f""" 77 | event_type={event_type}, 78 | provisioning_state={provisioning_state}, 79 | app_subscription_id={app_subscription_id}, 80 | app_resource_group={app_resource_group}, 81 | app_name={app_name} 82 | """ 83 | ) 84 | 85 | # Check out this docs to understand all the different combinations of event type and 86 | # provisioning state and what events originate them. 87 | # https://docs.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/publish-notifications#event-triggers 88 | # 89 | # The Notification service expects a 200 OK response. Otherwise, it will consider 90 | # the request as failed and it will keep retrying it. Therefore, even if the provisioning 91 | # state is Failed, we need to return a 200 OK if the message has been processed in the function. 92 | # https://docs.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/publish-notifications#notification-retries 93 | 94 | if provisioning_state not in ("Succeeded", "Deleted"): 95 | msg = f"Provisioning state is '{provisioning_state}'. Ignoring event..." 96 | logging.info(msg) 97 | return func.HttpResponse(msg, status_code=200) 98 | 99 | with TableClient.from_connection_string(conn_str=CONNECTION_STRING, table_name=TABLE_NAME) as table_client: 100 | if provisioning_state == "Succeeded" and event_type == "PUT": 101 | # At this point you can obtain data from the Managed Application 102 | # instance, like the input parameters, deployment outputs, plan version, etc. 103 | # You can check all the Application object attributes here: 104 | # https://github.com/Azure/azure-sdk-for-python/blob/830ccf6ab129bdd6b7343cfae39e4e8e4b3bfd4d/sdk/resources/azure-mgmt-resource/azure/mgmt/resource/managedapplications/models/_models_py3.py#L191 105 | try: 106 | app_client = ApplicationClient(credential, app_subscription_id) 107 | app_details = app_client.applications.get_by_id(application_id) 108 | except Exception as e: 109 | msg = f"Failed to obtain managed application details: {e}" 110 | logging.error(msg) 111 | return func.HttpResponse(msg, status_code=500) 112 | 113 | logging.info(app_details) 114 | try: 115 | (_, mrg_name, _) = parse_resource_id( 116 | app_details.managed_resource_group_id) 117 | logging.info(f"Managed resource group name: {mrg_name}") 118 | except ValueError as e: 119 | msg = f"Error obtaining the mrg name: {e}" 120 | logging.error(msg) 121 | return func.HttpResponse(msg, status_code=500) 122 | 123 | entity = { 124 | "subscription_id": app_subscription_id, 125 | "app_resource_group_name": app_resource_group, 126 | "app_name": app_name, 127 | "mrg_name": mrg_name, 128 | "PartitionKey": app_subscription_id, 129 | "RowKey": app_name 130 | } 131 | try: 132 | table_client.create_table() 133 | except HttpResponseError: 134 | logging.info("Table has already exists") 135 | 136 | try: 137 | resp = table_client.create_entity(entity=entity) 138 | logging.info(f"Entity successfully added. {resp}") 139 | except ResourceExistsError: 140 | logging.error("Entity already exists") 141 | except Exception as e: 142 | msg = f"Error trying to add entity: {e}" 143 | logging.error(msg) 144 | return func.HttpResponse(msg, status_code=500) 145 | 146 | if provisioning_state == "Deleted" and event_type == "DELETE": 147 | try: 148 | table_client.delete_entity( 149 | row_key=app_name, partition_key=app_subscription_id) 150 | logging.info("Entity successfully deleted.") 151 | except Exception as e: 152 | msg = f"Error trying to delete entity: {e}" 153 | logging.error(msg) 154 | return func.HttpResponse(msg, status_code=500) 155 | 156 | return func.HttpResponse("OK", status_code=200) 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /applications/NotificationHandler/test_func.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import json 4 | 5 | import azure.functions as func 6 | from . import main 7 | 8 | 9 | class TestFunction(unittest.TestCase): 10 | 11 | succeeded_event = { 12 | "eventType": "PUT", 13 | "applicationId": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test/providers/Microsoft.Solutions/applications/test3", 14 | "eventTime": "2022-03-14T19:20:08.1707163Z", 15 | "provisioningState": "Succeeded", 16 | "plan": { 17 | "name": "msft-insights-poc-managed", 18 | "product": "msft-insights-poc-preview", 19 | "publisher": "test_test_agcicemarketplace1616064700629", 20 | "version": "0.1.20", 21 | }, 22 | } 23 | 24 | failed_event = { 25 | "eventType": "PUT", 26 | "applicationId": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test/providers/Microsoft.Solutions/applications/test3", 27 | "eventTime": "2022-03-14T19:20:08.1707163Z", 28 | "provisioningState": "Failed", 29 | "plan": { 30 | "name": "msft-insights-poc-managed", 31 | "product": "msft-insights-poc-preview", 32 | "publisher": "test_test_agcicemarketplace1616064700629", 33 | "version": "0.1.20", 34 | }, 35 | "error": { 36 | "code": "ErrorCode", 37 | "message": "error message", 38 | "details": [{"code": "DetailedErrorCode", "message": "error message"}], 39 | }, 40 | } 41 | 42 | deleted_event = { 43 | "eventType": "DELETE", 44 | "applicationId": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test/providers/Microsoft.Solutions/applications/test3", 45 | "eventTime": "2022-03-14T19:20:08.1707163Z", 46 | "provisioningState": "Deleted", 47 | "plan": { 48 | "name": "msft-insights-poc-managed", 49 | "product": "msft-insights-poc-preview", 50 | "publisher": "test_test_agcicemarketplace1616064700629", 51 | "version": "0.1.20", 52 | } 53 | } 54 | 55 | @patch("NotificationHandler.DefaultAzureCredential") 56 | @patch("NotificationHandler.os") 57 | def test_ignore_get_method(self, os_env, mock_credential): 58 | req = func.HttpRequest(method="GET", url="/api/resource", body=None) 59 | resp = main(req) 60 | self.assertEqual( 61 | resp.status_code, 62 | 405, 63 | ) 64 | 65 | @patch("NotificationHandler.DefaultAzureCredential") 66 | @patch("NotificationHandler.os") 67 | def test_ignore_put_method(self, os_env, mock_credential): 68 | req = func.HttpRequest(method="PUT", url="/api/resource", body=None) 69 | resp = main(req) 70 | self.assertEqual( 71 | resp.status_code, 72 | 405, 73 | ) 74 | 75 | @patch("NotificationHandler.DefaultAzureCredential") 76 | @patch("NotificationHandler.os") 77 | def test_ignore_delete_method(self, os_env, mock_credential): 78 | req = func.HttpRequest(method="DELETE", url="/api/resource", body=None) 79 | resp = main(req) 80 | self.assertEqual( 81 | resp.status_code, 82 | 405, 83 | ) 84 | 85 | @patch("NotificationHandler.DefaultAzureCredential") 86 | @patch("NotificationHandler.os") 87 | def test_ignore_patch_method(self, os_env, mock_credential): 88 | req = func.HttpRequest(method="PATCH", url="/api/resource", body=None) 89 | resp = main(req) 90 | self.assertEqual( 91 | resp.status_code, 92 | 405, 93 | ) 94 | 95 | @patch("NotificationHandler.DefaultAzureCredential") 96 | @patch("NotificationHandler.os") 97 | def test_invalid_nonjson_payload(self, os_env, mock_credential): 98 | req = func.HttpRequest( 99 | method="post", url="/api/resource", body="foobar") 100 | resp = main(req) 101 | self.assertIn( 102 | b"Could not parse request", 103 | resp.get_body(), 104 | ) 105 | self.assertEqual( 106 | resp.status_code, 107 | 400, 108 | ) 109 | 110 | @patch("NotificationHandler.DefaultAzureCredential") 111 | @patch("NotificationHandler.os") 112 | def test_failed_delete_event(self, os_env, mock_credential): 113 | body = self.failed_event.copy() 114 | body["eventType"] = "DELETE" 115 | json_body = json.dumps(body) 116 | req = func.HttpRequest( 117 | method="post", 118 | url="/api/resource", 119 | body=json_body.encode(), 120 | ) 121 | resp = main(req) 122 | self.assertIn( 123 | b"Provisioning state is 'Failed'. Ignoring event...", 124 | resp.get_body(), 125 | ) 126 | 127 | self.assertEqual( 128 | resp.status_code, 129 | 200, 130 | ) 131 | 132 | @patch("NotificationHandler.DefaultAzureCredential") 133 | @patch("NotificationHandler.os") 134 | def test_accepted_state(self, os_env, mock_credential): 135 | body = self.succeeded_event.copy() 136 | body["provisioningState"] = "Accepted" 137 | json_body = json.dumps(body) 138 | req = func.HttpRequest( 139 | method="post", 140 | url="/api/resource", 141 | body=json_body.encode(), 142 | ) 143 | resp = main(req) 144 | self.assertIn( 145 | b"Provisioning state is 'Accepted'. Ignoring", 146 | resp.get_body(), 147 | ) 148 | self.assertEqual( 149 | resp.status_code, 150 | 200, 151 | ) 152 | 153 | @patch("NotificationHandler.DefaultAzureCredential") 154 | @patch("NotificationHandler.os") 155 | def test_deleting_state(self, os_env, mock_credential): 156 | body = self.succeeded_event.copy() 157 | body["provisioningState"] = "Deleting" 158 | json_body = json.dumps(body) 159 | req = func.HttpRequest( 160 | method="post", 161 | url="/api/resource", 162 | body=json_body.encode(), 163 | ) 164 | resp = main(req) 165 | self.assertIn( 166 | b"Provisioning state is 'Deleting'. Ignoring", 167 | resp.get_body(), 168 | ) 169 | self.assertEqual( 170 | resp.status_code, 171 | 200, 172 | ) 173 | 174 | @patch("NotificationHandler.DefaultAzureCredential") 175 | @patch("NotificationHandler.os") 176 | @patch("NotificationHandler.TableClient") 177 | def test_succeeded_state(self, table_client_mock, os_env, mock_credential): 178 | with patch("NotificationHandler.ApplicationClient.applications") as mock_application_client: 179 | class AppDetails: 180 | managed_resource_group_id = "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test" 181 | mock_application_client.get_by_id.return_value = AppDetails 182 | json_body = json.dumps(self.succeeded_event) 183 | req = func.HttpRequest( 184 | method="post", 185 | url="/api/resource", 186 | body=json_body.encode(), 187 | ) 188 | resp = main(req) 189 | self.assertEqual( 190 | b"OK", 191 | resp.get_body(), 192 | ) 193 | self.assertEqual( 194 | resp.status_code, 195 | 200, 196 | ) 197 | 198 | @patch("NotificationHandler.DefaultAzureCredential") 199 | @patch("NotificationHandler.os") 200 | @patch("NotificationHandler.TableClient") 201 | def test_deleted_state(self, table_client_mock, os_env, mock_credential): 202 | with patch("NotificationHandler.ApplicationClient.applications") as mock_application_client: 203 | mock_application_client.get_by_id.return_value = { 204 | "managed_resource_group_id": "subscriptions/bb5840c6-bd1f-4431-b82a-bcff37b7fd07/resourceGroups/managed-test" 205 | } 206 | json_body = json.dumps(self.deleted_event) 207 | req = func.HttpRequest( 208 | method="post", 209 | url="/api/resource", 210 | body=json_body.encode(), 211 | ) 212 | resp = main(req) 213 | self.assertEqual( 214 | b"OK", 215 | resp.get_body(), 216 | ) 217 | self.assertEqual( 218 | resp.status_code, 219 | 200, 220 | ) 221 | -------------------------------------------------------------------------------- /azure/function.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param logAnalyticsWorkspaceName string 3 | param logAnalyticsWorkspaceId string 4 | param streamDeclaration string 5 | param streamName string 6 | param storageAccountTableName string 7 | targetScope = 'resourceGroup' 8 | param appName string 9 | 10 | // Service principal credentials 11 | @secure() 12 | param spClientId string 13 | @secure() 14 | param spClientSecret string 15 | @secure() 16 | param spTenantId string 17 | 18 | var uniqueName = '${appName}${substring(replace(guid(resourceGroup().id), '-', ''), 0, 8)}' 19 | // Storage account and Key Vault names must be between 3 and 24 characters in length and use numbers and lower-case letters only. 20 | var uniqueNameWithoutDashes = toLower(replace(uniqueName, '-', '')) 21 | 22 | var storageAccountName = length(uniqueNameWithoutDashes) > 24 ? substring(uniqueNameWithoutDashes, 0, 24) : uniqueNameWithoutDashes 23 | var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}' 24 | var hostingPlanName = uniqueName 25 | var appInsightsName = uniqueName 26 | var functionAppName = uniqueName 27 | var keyvaultName = uniqueNameWithoutDashes 28 | 29 | // allows to publish policy states to LA 30 | var monitoringMetricsPublisherRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb') 31 | // allows reading resources 32 | var readerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') 33 | // allows to read subsciption id and rg name from Blob storage 34 | var storageBlobReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1') 35 | 36 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 37 | name: storageAccountName 38 | location: location 39 | kind: 'StorageV2' 40 | sku: { 41 | name: 'Standard_LRS' 42 | } 43 | } 44 | 45 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 46 | name: appInsightsName 47 | location: location 48 | kind: 'web' 49 | properties: { 50 | Application_Type: 'web' 51 | publicNetworkAccessForIngestion: 'Enabled' 52 | publicNetworkAccessForQuery: 'Enabled' 53 | } 54 | tags: { 55 | // circular dependency means we can't reference functionApp directly /subscriptions//resourceGroups//providers/Microsoft.Web/sites/" 56 | 'hidden-link:/subscriptions/${subscription().id}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Web/sites/${functionAppName}': 'Resource' 57 | } 58 | } 59 | 60 | resource keyvault 'Microsoft.KeyVault/vaults@2021-10-01' = { 61 | name: keyvaultName 62 | location: location 63 | properties: { 64 | enabledForDeployment: false 65 | enabledForDiskEncryption: false 66 | enabledForTemplateDeployment: true 67 | tenantId: subscription().tenantId 68 | accessPolicies: [] 69 | sku: { 70 | name: 'standard' 71 | family: 'A' 72 | } 73 | } 74 | } 75 | 76 | resource secretClientId 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = { 77 | parent: keyvault 78 | name: 'spClientId' 79 | properties: { 80 | value: spClientId 81 | } 82 | } 83 | 84 | resource secretClientSecret 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = { 85 | parent: keyvault 86 | name: 'spClientSecret' 87 | properties: { 88 | value: spClientSecret 89 | } 90 | } 91 | 92 | resource secretTenantId 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = { 93 | parent: keyvault 94 | name: 'spTenantId' 95 | properties: { 96 | value: spTenantId 97 | } 98 | } 99 | 100 | resource accessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2021-10-01' = { 101 | parent: keyvault 102 | name: 'add' 103 | properties: { 104 | accessPolicies: [ 105 | { 106 | objectId: function.identity.principalId 107 | tenantId: function.identity.tenantId 108 | permissions: { 109 | secrets: [ 110 | 'get' 111 | ] 112 | } 113 | } 114 | ] 115 | } 116 | } 117 | 118 | resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = { 119 | name: hostingPlanName 120 | location: location 121 | kind: 'linux' 122 | properties: { 123 | // true if it's a Linux plan, false otherwise. 124 | reserved: true 125 | } 126 | sku: { 127 | name: 'Y1' 128 | tier: 'Dynamic' 129 | } 130 | } 131 | 132 | resource policyStatesDataCollectionEndpoint 'Microsoft.Insights/dataCollectionEndpoints@2021-09-01-preview' = { 133 | name: 'policyStatesDataCollectionEndpoint' 134 | location: location 135 | properties: {} 136 | } 137 | 138 | resource policyStatesDataCollectionRule 'Microsoft.Insights/dataCollectionRules@2021-09-01-preview' = { 139 | name: 'policyStatesDataCollectionRule' 140 | location: location 141 | properties: { 142 | dataCollectionEndpointId: policyStatesDataCollectionEndpoint.id 143 | streamDeclarations: { 144 | '${streamDeclaration}': { 145 | columns: [ 146 | { 147 | name: 'Policy_assignment_id' 148 | type: 'string' 149 | } 150 | { 151 | name: 'Policy_assignment_name' 152 | type: 'string' 153 | } 154 | { 155 | name: 'Is_compliant' 156 | type: 'boolean' 157 | } 158 | { 159 | name: 'TimeGenerated' 160 | type: 'datetime' 161 | } 162 | ] 163 | } 164 | } 165 | dataSources: {} 166 | destinations: { 167 | logAnalytics: [ 168 | { 169 | workspaceResourceId: logAnalyticsWorkspaceId 170 | name: logAnalyticsWorkspaceName 171 | } 172 | ] 173 | } 174 | dataFlows: [ 175 | { 176 | streams: [ 177 | streamDeclaration 178 | ] 179 | destinations: [ 180 | logAnalyticsWorkspaceName 181 | ] 182 | transformKql: 'source' 183 | outputStream: streamDeclaration 184 | } 185 | ] 186 | } 187 | } 188 | 189 | resource function 'Microsoft.Web/sites@2021-03-01' = { 190 | name: functionAppName 191 | location: location 192 | kind: 'functionapp,linux' 193 | identity: { 194 | type: 'SystemAssigned' 195 | } 196 | properties: { 197 | serverFarmId: hostingPlan.id 198 | httpsOnly: true 199 | siteConfig: { 200 | linuxFxVersion: 'Python|3.9' 201 | appSettings: [ 202 | { 203 | name: 'AzureWebJobsDashboard' 204 | value: storageAccountConnectionString 205 | } 206 | { 207 | name: 'AzureWebJobsStorage' 208 | value: storageAccountConnectionString 209 | } 210 | { 211 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' 212 | value: storageAccountConnectionString 213 | } 214 | { 215 | name: 'FUNCTIONS_EXTENSION_VERSION' 216 | value: '~4' 217 | } 218 | { 219 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY' 220 | value: appInsights.properties.InstrumentationKey 221 | } 222 | { 223 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 224 | value: appInsights.properties.ConnectionString 225 | } 226 | { 227 | name: 'FUNCTIONS_WORKER_RUNTIME' 228 | value: 'python' 229 | } 230 | { 231 | name: 'AZURE_CLIENT_ID' 232 | value: '@Microsoft.KeyVault(SecretUri=${secretClientId.properties.secretUriWithVersion})' 233 | } 234 | { 235 | name: 'AZURE_CLIENT_SECRET' 236 | value: '@Microsoft.KeyVault(SecretUri=${secretClientSecret.properties.secretUriWithVersion})' 237 | } 238 | { 239 | name: 'AZURE_TENANT_ID' 240 | value: '@Microsoft.KeyVault(SecretUri=${secretTenantId.properties.secretUriWithVersion})' 241 | } 242 | { 243 | name: 'DATA_COLLECTION_ENDPOINT' 244 | value: policyStatesDataCollectionEndpoint.properties.logsIngestion.endpoint 245 | } 246 | { 247 | name: 'DATA_COLLECTION_IMMUTABLE_ID' 248 | value: policyStatesDataCollectionRule.properties.immutableId 249 | } 250 | { 251 | name: 'STREAM_NAME' 252 | value: streamName 253 | } 254 | { 255 | name: 'TABLE_NAME' 256 | value: storageAccountTableName 257 | } 258 | ] 259 | } 260 | } 261 | } 262 | 263 | resource policyIdentityReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 264 | name: guid('policyIdentityReader', resourceGroup().id) 265 | properties: { 266 | principalId: function.identity.principalId 267 | principalType: 'ServicePrincipal' 268 | roleDefinitionId: readerRole 269 | } 270 | } 271 | 272 | resource monitoringMetricsPublisher 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 273 | name: guid('monitoringMetricsPublisher', resourceGroup().id) 274 | properties: { 275 | principalId: function.identity.principalId 276 | principalType: 'ServicePrincipal' 277 | roleDefinitionId: monitoringMetricsPublisherRole 278 | } 279 | } 280 | 281 | resource storageBlobReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 282 | scope: storageAccount 283 | name: guid('storageBlobReader', resourceGroup().id) 284 | properties: { 285 | principalId: function.identity.principalId 286 | principalType: 'ServicePrincipal' 287 | roleDefinitionId: storageBlobReaderRole 288 | } 289 | } 290 | 291 | output functionAppName string = functionAppName 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This repository contains two Azure Function applications designed to help Managed Application solution owners in Partner Centre monitor and manage their deployed apps. 4 | 5 | The first app, Notification Endpoint, captures events triggered during customer deployment and saves managed app information into an Azure storage table, allowing solution owners to build analytics and alerts on top of this data. 6 | For further information about each function, please refer to the [README file](https://github.com/Azure/marketplace-management/blob/feedback/applications/README.md) located in the `applications` directory. 7 | 8 | 9 | 10 | The second app, Policy States Collector, automates monitoring of Azure policies for compliance by querying the latest state of Azure policies in managed applications, filtering and sending the data to the Policy Monitor table in the Log Analytics Workspace for real-time monitoring. The app also includes a Scheduled Query Rule Alert that monitors non-compliant policies and triggers an Action Group for notification when an issue is detected. 11 | 12 | 13 | 14 | Please note that these applications are an extra tool for publishers and is not required for using Managed Applications. Additionally, there are starter applications and can be customized according to your needs. The solution is part of [Azure Marketplace Managed Application samples](https://github.com/microsoft/commercial-marketplace-resources#azure-managed-application-offers) 15 | 16 | ## Repository content 17 | 18 | Within this repository, you can find the following directories: 19 | 20 | - `.github/workflows`: automated workflows to deploy infrastructure and code. 21 | - `applications`: code and documentation describing each application in more details. 22 | - `azure`: infrastructure templates that deploy the Azure components needed to support applications. 23 | - `images`: images for README. 24 | 25 | ## Get it up and running 26 | 27 | This project is a starting point to monitor your Managed Application offer created in [Partner center](https://partner.microsoft.com/), assuming that you have already created your offer. Use the guidance below. 28 | 29 | > :warning: **Please make sure you deploy the Notification Endpoint before you publish your managed app offer. Otherwise, you will need to republish the offer again with configuration of the Notification Endpoint URL.** 30 | 31 | ### Prerequisites 32 | 33 | The deployment of the azure function requires a series of actions to set up the environment, including the creation of a Marketplace [Managed Application service principal](https://learn.microsoft.com/en-gb/partner-center/marketplace/plan-azure-app-managed-app#choose-who-can-manage-the-application), an Azure service principal, configuring the necessary GitHub secrets and variables, and deploying the code. 34 | 35 | ### Create the Azure Service Principal 36 | 37 | Follow these steps to create a Service Principal with the `Owner` role scoped to the target Platform subscription. This Service Principal will be used by a GitHub-owned runner to authenticate to Azure and deploy the Azure function infrastructure. 38 | 39 | - Log in to the required Azure Active Directory Tenant using the Azure CLI on your local device or via [Azure Cloud Shell](https://shell.azure.com):
40 | `az login --tenant [Tenant ID]` 41 | - Select the target Platform subscription, inserting the Subscription ID where indicated:
`az account set --subscription [Subscription ID]` 42 | - Create the Service Principal, providing an identifying name where indicated:
`az ad sp create-for-rbac --name [SP Name] --query "{ client_id: appId, client_secret: password, tenant_id: tenant }"` 43 | - Capture the values of **client_id**, **client_secret** and **tenant_id** for later use. It is recommended that you do not persist this information to disk or share the client secret for security reasons. 44 | - Grant the Service Principal "Owner" role on the target Platform subscription:
`az role assignment create --assignee [SP Client ID] --role "Owner" --scope /subscriptions/[Subscription ID]` 45 | 46 | ### Configure the Managed Application service principal 47 | 48 | This Service Principal will be used by Azure function applications (NotificationHandler and PolicyStates) to authenticate against customers' deployments and fetch needed Managed app information. 49 | 50 | - Create another Service principal by using instructions from previous step [Create Azure Service Principal](#create-the-azure-service-principal) section without the last step (granting the Service Principal owner role). 51 | 52 | - Link this Service principal in Partner center by going to [Partner center](https://partner.microsoft.com/) and then Navigate to **your offer** > **your managed plan** > **Plan overview** > **Technical configuration** 53 | 54 | - In `Authorizations` section click on `Add authorizations`. Use `Object ID` from previous step and choose the `Owner role` 55 | 56 | ![Managed SP config](./images/managed_sp_config.jpg) 57 | 58 | ### Create secrets and variables 59 | 60 | For each of the secrets defined in the table below, follow these steps to add each secret and variables the corresponding value. 61 | 62 | - Navigate to **your github repo** > **Settings** > **Secrets and variables** > **New repository secret** 63 | 64 | - Add the secret name and value, taking care to use the exact secret name provided as this is explicitly referenced in the GitHub workflow. 65 | 66 | | Secret Name | Example Value | Description | 67 | | ------------------------ | ------------------------------------ | ----------- | 68 | | SUBSCRIPTION_ID | 3edb65d1-d7a8-409b-a320-3c01ac6825f9 | Subscription ID where infra will be deployed. | 69 | | CLIENT_ID | 9505fb9a-96e6-46d1-ac9b-2f74ee57f6d6 | Client ID from the [Create the Azure Service Principaln](#create-the-azure-service-principal) | 70 | | CLIENT_SECRET | [secure string] | Client Secret from the [Create the Azure Service Principal](#create-service-principal) | 71 | | TENANT_ID | f7d23806-d8ac-4576-814f-0ee931ffeab3 | Azure AD Tenant ID from the [Create the Azure Service Principal](#create-the-azure-service-principal) | 72 | | SP_CLIENT_ID | 9505fb9a-96e6-46d1-ac9b-2f74ee57f6d6 | Manage app Client ID from the [Configure the Managed Application service principal](#configure-the-managed-application-service-principal) | 73 | | SP_CLIENT_SECRET | [secure string] | Managed app Client Secret from the [Configure the Managed Application service principal](#configure-the-managed-application-service-principal) | 74 | 75 | - Navigate to **Settings** > **Secrets and variables** > **variables** > **New repository variables** 76 | 77 | - Add the varible name and value, taking care to use the exact secret name provided as this is explicitly referenced in the GitHub workflow. 78 | 79 | | Varible Name | Example Value | Description | 80 | | ------------------------ | ------------------------------------ | ----------- | 81 | | APP_NAME | marketplace | Azure function name | 82 | | LOCATION | northeurope | Location of infra resources | 83 | | RESOURCE_GROUP_NAME | marketplace-manage-applications | RG name | 84 | | STORAGE_ACCOUNT_TABLE_NAME | applications | name of Table for storing Manage app details in Azure storage | 85 | | SUBSCRIPTION_NAME | [string] | Subscription name where infra will be deployed | 86 | | EMAIL_ADDRESS | [string] | Email address for the alert notification when policies become non-compliant | 87 | | RECEIVERS_NAME | [string] | The receiver's name to who the alert is sent | 88 | 89 | ### Run the workflows 90 | 91 | Follow these steps to run workflows which will deploy the infrastructure and code. 92 | ![Managed SP config](./images/deployed_resources.jpg) 93 | 94 | - Navigate to **Actions** -> **Infrastructure deployment** 95 | - Click **Run workflow** 96 | - Ensure the desired branch is selected, e.g. **main** 97 | - Click the **Run workflow** button 98 | - Copy function name from logs in `Show function name` step after the `Infrastructure deployment` workflow is finished 99 | 100 | 101 | 102 | - Navigate to `Code deployment` workflow to run the second one. Paste the function name in the input 103 | 104 | Once the function is deployed, you can configure the Managed Application to use the Notification Endpoint URL. You can do this by following these steps: 105 | 106 | - Open the Managed Application in [Partner center](https://partner.microsoft.com/) 107 | - Navigate to **your offer** > **your managed plan** > **Plan overview** > **Technical configuration** 108 | - Enter the Notification Endpoint URL, which is the URL of the Azure Function that you created. 109 | Save the changes. 110 | 111 | 112 | 113 | - Publish / republish your plan 114 | 115 | ### Confirm Azure function applications are running successfully 116 | 117 | Once all Workflows have completed, navigate to Resource group `marketplace-manage-applications`. Select Azure function and click on `Functions`. You should see `NotificationHandler` and `PolicyStates`. Each functions has logs that can indicate the status. You can find them in `Monitor` section. 118 | 119 | When you deployed the Manage application, you should be able to find its details in the Azure storage table.
120 | You can also see policy states in Log Analytics by using `PolicyComplianceStates_CL` query. 121 | Only note they will be visible after some time after function gets triggered. 122 | If policies become non-complaint, you should get the email notification. 123 | 124 | ## Contributing 125 | 126 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 127 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 128 | the rights to use your contribution. For details, visit . 129 | 130 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 131 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 132 | provided by the bot. You will only need to do this once across all repos using our CLA. 133 | 134 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 135 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 136 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 137 | 138 | ## Trademarks 139 | 140 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 141 | trademarks or logos is subject to and must follow 142 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 143 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 144 | Any use of third-party trademarks or logos are subject to those third-party's policies. 145 | -------------------------------------------------------------------------------- /applications/README.md: -------------------------------------------------------------------------------- 1 | # Policy states collector 2 | 3 | The functionality that allows publishers to access and manage deployed customers managed application is the foundation upon which the entire solution is built. 4 | 5 | The time-triggered Azure Function application, policy states collector, uses Azure APIs to query the latest state of Azure policies in managed applications. To build the correct request, the application retrieves the Resource Group name and subscription ID from Azure Blob Storage. The application leverages the Service Principal, which is configured in the Managed App plan, to access managed resource policies. 6 | 7 | After retrieving the data, the application filters and sends it to the Policy Monitor table in the Log Analytics Workspace for real-time monitoring and analysis. It filters only the policies deployed by the Managed Application to ensure that no customers policies are pulled. 8 | 9 | Finally, the Scheduled Query Rule Alert is configured to monitor non-compliant policies and triggers an Action Group for notification when an issue is detected. 10 | 11 | This solution offers automated monitoring of Azure policies for compliance, allowing for proactive identification and resolution of policy violations. 12 | 13 | 14 | 15 | ## Solution components 16 | 17 | - Azure Log Analytics 18 | - Data collection rule and Data collection endpoint 19 | - Azure function 20 | - SP principles 21 | - Azure storage table 22 | - Azure Key vault 23 | - Scheduled Query Rule Alert 24 | - Action Group 25 | 26 | ## Authentication 27 | 28 | The function source code is written in Python and relies on the [`ClientSecretCredential()`](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential?view=azure-dotnet) and [`ManagedIdentityCredential()`](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet) 29 | classes for authentication to Azure. 30 | 31 | Azure function has system assigned identity and two roles that allow read policies and write data to Log Analytics. 32 | 33 | ## Local setup 34 | 35 | The function has the following requirements: 36 | 37 | - AZ cli 38 | - [Azure Functions runtime](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=in-process%2Cv4&pivots=programming-language-python) version 4.x 39 | - Python 3.9 and pip 40 | - Virtualenv 41 | - Also check out [Quickstart: Create a function in Azure with Python using Visual Studio Code](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python#configure-your-environment) 42 | to find out additional requirements and the recommended VS Code extensions. 43 | 44 | To get started, navigate to the `applications` directory and create your virtual environment. 45 | 46 | ```sh 47 | virtualenv .venv 48 | ``` 49 | 50 | Then you can source it and install the function dependencies in the `requirements.txt`. 51 | 52 | ```sh 53 | source .venv/bin/activate 54 | pip install -r requirements.txt 55 | ``` 56 | 57 | Update `function.json` to `"schedule": "* * * * * *"` for triggering application every second for testing purpose. 58 | 59 | Set up all local envs. Take an existing deployed Azure Function name and then run `func azure functionapp fetch-app-settings ` 60 | This will create `local.settings.json` file with all envs. 61 | 62 | Last step is assign correct role for DataCollectionRule. Go to `Monitor > Data Collection Rules` and find your DCR. In `Acess Contorole` add role `Monitoring Metrics Publisher` to your account. 63 | 64 | Run the function with `func start` command provided by the Azure Functions runtime. 65 | 66 | # Notification Endpoint 67 | 68 | This directory contains the necessary artifacts to deploy the Notification Endpoint that captures events triggered when the managed version of application is installed by customers from the Marketplace. To understand more about the process please check the [Managed Application deployment notifications](../docs/deploy-infrastructure.md) document. 69 | 70 | The idea behind this project is to be used as a starter application that can be further customized to fit the needs of the publisher. 71 | The Notification Endpoint is built as an [Azure Function](https://azure.microsoft.com/en-us/services/functions/) invoked via an HTTP trigger whose URL is configured as the notification endpoint URL of the Managed Application. 72 | 73 | > :warning: **Please make sure you deploy the Notification Endpoint before you publish your managed app offer. Otherwise, you will need to republish the offer again with configuration of the Notification Endpoint URL.** 74 | 75 | 76 | 77 | 78 | ## Requirements 79 | 80 | 1. According to the [Azure managed applications with notifications](https://docs.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/publish-notifications#getting-started) docs, the endpoint should expect POST requests and should return `200 OK` if the request was processed successfully. 81 | 82 | 2. According to the [Provide a notification endpoint URL](https://docs.microsoft.com/en-us/azure/marketplace/azure-app-managed#provide-a-notification-endpoint-url) docs, Azure appends `/resource` to the notification endpoint URL before sending the request. Therefore, our function's invoke URL should end with `/resource`. 83 | 84 | 3. The function needs to parse the incoming request, which will contain a JSON-encoded payload with the schema defined in the [Azure Marketplace application notification schema](https://docs.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/publish-notifications#azure-marketplace-application-notification-schema) docs. 85 | 86 | 4. It will also need to handle different types of [Event triggers](https://docs.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/publish-notifications#event-triggers) which include a combination of event types (e.g. PUT, PATCH, DELETE) and provisioning states (e.g. Accepted, Succeeded, Failed). 87 | 88 | 5. If the provisioning state is "Succeeded", the function app will obtain the application ID from the payload and send an API call to obtain additional information about the deployment. This information includes data such as the name given by the customer, the input parameters or the output values of the deployment. 89 | It will also save the managed app "subscription id" and "resource group name" into Azure storage table if event type is "PUT". This information will be used for "PolicyStates" function app. 90 | 91 | 5. If the provisioning state is "Deleted" and event type is "DELETE", the function app will delete the entity about manage app. 92 | 93 | ## Function configuration 94 | 95 | To address requirements 1 and 2, we can use the `function.json` file to configure the HTTP methods our function will accept and the URL route to the function. 96 | 97 | ```json 98 | { 99 | ... 100 | "methods": [ 101 | "post" 102 | ], 103 | "route": "resource" 104 | ... 105 | } 106 | ``` 107 | 108 | Check the [`NotificationHandler/function.json`](./NotificationHandler/function.json) file. 109 | 110 | ## Authentication 111 | 112 | The function source code is written in Python and relies on the [`DefaultAzureCredential()`](https://docs.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) class for authentication to Azure. Locally, it will use the AZ CLI credentials; and remotely, it will use the `AZURE_` environment variables that will be configured with the credentials of a Service Principal with authorization on the managed application. 113 | 114 | ## Local setup 115 | 116 | The function has the following requirements: 117 | 118 | - AZ cli 119 | - [Azure Functions runtime](https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=in-process%2Cv4&pivots=programming-language-python) version 4.x 120 | - Python 3.9 and pip 121 | - Virtualenv 122 | - Also check out [Quickstart: Create a function in Azure with Python using Visual Studio Code](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python#configure-your-environment) to find out additional requirements and the recommended VS Code extensions. 123 | 124 | To get started, navigate to the `notification-endpoint` directory and create your virtual environment. 125 | 126 | ``` 127 | virtualenv .venv 128 | ``` 129 | 130 | Then you can source it and install the function dependencies in the `requirements.txt`. 131 | 132 | ``` 133 | source .venv/bin/activate 134 | pip install -r requirements.txt 135 | ``` 136 | 137 | Now, make sure you are logged in with your AZ cli tool, and run the function with the `func start` command provided by the Azure Functions runtime. 138 | 139 | ``` 140 | $ func start 141 | Found Python version 3.9.10 (python3). 142 | 143 | Azure Functions Core Tools 144 | Core Tools Version: 4.0.3971 Commit hash: d0775d487c93ebd49e9c1166d5c3c01f3c76eaaf (64-bit) 145 | Function Runtime Version: 4.0.1.16815 146 | 147 | 148 | Functions: 149 | 150 | NotificationHandler: [POST] http://localhost:7071/api/resource 151 | 152 | For detailed output, run func with --verbose flag. 153 | info: Microsoft.AspNetCore.Hosting.Diagnostics[1] 154 | Request starting HTTP/2 POST http://127.0.0.1:33909/AzureFunctionsRpcMessages.FunctionRpc/EventStream application/grpc - 155 | info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0] 156 | Executing endpoint 'gRPC - /AzureFunctionsRpcMessages.FunctionRpc/EventStream' 157 | [2022-03-18T08:04:50.328Z] Worker process started and initialized. 158 | [2022-03-18T08:04:54.925Z] Host lock lease acquired by instance ID '0000000000000000000000008B849374'. 159 | ``` 160 | 161 | Once running, open a new window or tab and you can use the `test_event_ok.json` and `test_event_failure.json` files in the `NotificationHandler` directory to send a sample requests to the function. From this directory, you can use curl to send an HTTP request. You can modify the sample file with an application ID that you have access to in order to test the entire flow. 162 | 163 | ``` 164 | curl -i -H "Content-Type: application/json" -X POST http://localhost:7071/api/resource -d @NotificationHandler/test_event_ok.json 165 | ``` 166 | 167 | You can also run the function from VS Code (tasks and launch configuration is provided in the `.vscode` directory). This will allow you to put breakpoints and easily debug and develop the function code. 168 | 169 | If you want to test the App Registration (Service Principal) credentials, create a `local.settings.json` file with the environment variables that will be injected into the function when run locally. This will make the function use the provided environment variables for authentication instead of using the AZ cli credentials. 170 | 171 | ```json 172 | { 173 | "IsEncrypted": false, 174 | "Values": { 175 | "FUNCTIONS_WORKER_RUNTIME": "python", 176 | "AZURE_TENANT_ID": "", 177 | "AZURE_CLIENT_ID": "", 178 | "AZURE_CLIENT_SECRET": "" 179 | } 180 | } 181 | ``` 182 | 183 | ## Deployment to Azure 184 | 185 | The [`azure`](./azure) directory contains the Bicep templates that deploy the infrastructure components of the Notification Endpoint. Additionally, a GitHub workflow has been created at [`../.github/workflows/notification-endpoint.yml`](../.github/workflows/notification-endpoint.yml) to automate the infrastructure deployment and the function app deployment. 186 | 187 | Assuming you are logged in with the AZ cli tool, and you are targeting the appropriate subscription, you can manually trigger a deployment with following command. 188 | 189 | ``` 190 | az deployment group create -g \ 191 | --template-file azure/main.bicep \ 192 | --parameters appName=notifications 193 | ``` 194 | 195 | Check the [Run the function in Azure](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python#run-the-function-in-azure) docs to learn how to use VS Code to submit the function to Azure. 196 | 197 | ## Run tests 198 | 199 | Unit tests are defined in the `NotificationHandler/test_func.py` file. You can easily run them with pytest. 200 | 201 | ```bash 202 | $ pip install pytest 203 | ... 204 | $ pytest NotificationHandler 205 | ================================================= test session starts ================================================== 206 | platform linux -- Python 3.9.10, pytest-7.1.1, pluggy-1.0.0 207 | rootdir: /home/user/projects/notification-endpoint 208 | collected 10 items 209 | 210 | NotificationHandler/test_func.py .......... [100%] 211 | 212 | ================================================== 10 passed in 0.15s ================================================== 213 | ``` 214 | 215 | ## Useful links 216 | 217 | - [Azure Functions Python developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Cazurecli-linux%2Capplication-level) 218 | - [Quickstart: Create a function in Azure with Python using Visual Studio Code](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-python) 219 | --------------------------------------------------------------------------------