mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-20 05:23:44 +00:00
initial public release
This commit is contained in:
1
src/deployment/README.md
Normal file
1
src/deployment/README.md
Normal file
@ -0,0 +1 @@
|
||||
deployment-role.json - Example Role for deploying OneFuzz
|
645
src/deployment/azuredeploy.json
Normal file
645
src/deployment/azuredeploy.json
Normal file
@ -0,0 +1,645 @@
|
||||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"signedExpiry": {
|
||||
"type": "string"
|
||||
},
|
||||
"diagnosticsLogsLevel": {
|
||||
"type": "string",
|
||||
"defaultValue": "Verbose",
|
||||
"allowedValues": [
|
||||
"Verbose",
|
||||
"Information",
|
||||
"Warning",
|
||||
"Error"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "The degree of severity for diagnostics logs."
|
||||
}
|
||||
},
|
||||
"workbookData": {
|
||||
"type": "object",
|
||||
"metadata": {
|
||||
"description": "Azure Monitor workbook definitions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"telemetry": "d7a73cf4-5a1a-4030-85e1-e5b25867e45a",
|
||||
"signalr-name": "[concat('onefuzz-', uniquestring(resourceGroup().id))]",
|
||||
"monitorAccountName": "[concat('logs-wb-', uniquestring(resourceGroup().id))]",
|
||||
"storageAccountName": "[concat('fuzz', uniquestring(resourceGroup().id))]",
|
||||
"storageAccountNameFunc": "[concat('func', uniquestring(resourceGroup().id))]",
|
||||
"Network Contributor": "4d97b98b-1d4f-4787-a291-c67834d212e7",
|
||||
"Storage Account Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab",
|
||||
"Virtual Machine Contributor": "9980e02c-c2be-4d73-94e8-173b1dc7cf3c",
|
||||
"Log Analytics Contributor": "92aaf0da-9dab-42b6-94a3-d43ce8d16293",
|
||||
"storage_account_sas": {
|
||||
"signedServices": "bfqt",
|
||||
"signedPermission": "rwdlacup",
|
||||
"signedExpiry": "[parameters('signedExpiry')]",
|
||||
"signedResourceTypes": "sco"
|
||||
},
|
||||
"diagnosticsLogsRetentionInDays": 90
|
||||
},
|
||||
"functions": [
|
||||
{
|
||||
"namespace": "onefuzz",
|
||||
"members": {
|
||||
"severitiesAtMostInfo": {
|
||||
"parameters": [
|
||||
],
|
||||
"output": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{
|
||||
"severity": "emerg"
|
||||
},
|
||||
{
|
||||
"severity": "alert"
|
||||
},
|
||||
{
|
||||
"severity": "crit"
|
||||
},
|
||||
{
|
||||
"severity": "err"
|
||||
},
|
||||
{
|
||||
"severity": "warning"
|
||||
},
|
||||
{
|
||||
"severity": "notice"
|
||||
},
|
||||
{
|
||||
"severity": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2018-11-01",
|
||||
"name": "[parameters('name')]",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"kind": "functionapp,linux",
|
||||
"location": "[resourceGroup().location]",
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.SignalRService/SignalR', variables('signalr-name'))]",
|
||||
"[resourceId('microsoft.insights/components/', parameters('name'))]",
|
||||
"[resourceId('Microsoft.Web/serverFarms', parameters('name'))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountNameFunc'))]"
|
||||
],
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2018-02-01",
|
||||
"type": "config",
|
||||
"name": "logs",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[concat('Microsoft.Web/sites/', parameters('name'))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
],
|
||||
"properties": {
|
||||
"applicationLogs": {
|
||||
"azureBlobStorage": {
|
||||
"level": "[parameters('diagnosticsLogsLevel')]",
|
||||
"sasUrl": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountNameFunc'))).primaryEndpoints.blob, 'app-logs', '?', listAccountSas(variables('storageAccountNameFunc'), '2018-02-01', variables('storage_account_sas')).accountSasToken)]",
|
||||
"retentionInDays": "[variables('diagnosticsLogsRetentionInDays')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"name": "[parameters('name')]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "FUNCTIONS_EXTENSION_VERSION",
|
||||
"value": "~3"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_WORKER_RUNTIME",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "FUNCTIONS_WORKER_PROCESS_COUNT",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
|
||||
"value": "[reference(resourceId('microsoft.insights/components/', parameters('name')), '2015-05-01').InstrumentationKey]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_TELEMETRY",
|
||||
"value": "[variables('telemetry')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureWebJobsStorage",
|
||||
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountNameFunc'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureWebJobsDisableHomepage",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "AzureSignalRConnectionString",
|
||||
"value": "[listkeys(resourceId('Microsoft.SignalRService/SignalR', variables('signalr-name')), '2018-10-01').primaryConnectionString]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_INSTANCE_NAME",
|
||||
"value": "[parameters('name')]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_INSTANCE",
|
||||
"value": "[concat('https://', parameters('name'), '.azurewebsites.net')]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_RESOURCE_GROUP",
|
||||
"value": "[resourceGroup().id]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_DATA_STORAGE",
|
||||
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_FUNC_STORAGE",
|
||||
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_MONITOR",
|
||||
"value": "[variables('monitorAccountName')]"
|
||||
},
|
||||
{
|
||||
"name": "ONEFUZZ_OWNER",
|
||||
"value": "[parameters('owner')]"
|
||||
}
|
||||
],
|
||||
"linuxFxVersion": "Python|3.7",
|
||||
"alwaysOn": true,
|
||||
"defaultDocuments": [
|
||||
],
|
||||
"httpLoggingEnabled": true,
|
||||
"logsDirectorySizeLimit": 100,
|
||||
"detailedErrorLoggingEnabled": true,
|
||||
"http20Enabled": true,
|
||||
"minTlsVersion": "1.2",
|
||||
"ftpsState": "Disabled",
|
||||
"siteAuthSettings": {
|
||||
"enabled": true,
|
||||
"unauthenticatedClientAction": "RedirectToLoginPage",
|
||||
"tokenStoreEnabled": true,
|
||||
"clientId": "[parameters('clientId')]",
|
||||
"clientSecret": "[parameters('clientSecret')]",
|
||||
"issuer": "[concat('https://sts.windows.net/', subscription().tenantId, '/')]",
|
||||
"defaultProvider": "AzureActiveDirectory",
|
||||
"allowedAudiences": [
|
||||
"[concat('https://', parameters('name'), '.azurewebsites.net')]"
|
||||
],
|
||||
"isAadAutoProvisioned": false
|
||||
}
|
||||
},
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverFarms', parameters('name'))]",
|
||||
"hostingEnvironment": "",
|
||||
"clientAffinityEnabled": false,
|
||||
"httpsOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2018-02-01",
|
||||
"name": "[parameters('name')]",
|
||||
"type": "Microsoft.Web/serverFarms",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "linux",
|
||||
"dependsOn": [
|
||||
],
|
||||
"properties": {
|
||||
"name": "[parameters('name')]",
|
||||
"reserved": true
|
||||
},
|
||||
"sku": {
|
||||
"name": "P2v2",
|
||||
"tier": "PremiumV2",
|
||||
"size": "P2v2",
|
||||
"family": "Pv2",
|
||||
"capacity": 1
|
||||
},
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-05-01",
|
||||
"name": "[parameters('name')]",
|
||||
"type": "microsoft.insights/components",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "",
|
||||
"properties": {
|
||||
"ApplicationId": "[parameters('name')]",
|
||||
"Application_Type": "other"
|
||||
},
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"name": "df20765c-ed5b-46f9-a47b-20f4aaf7936d",
|
||||
"type": "microsoft.insights/workbooks",
|
||||
"location": "[resourceGroup().location]",
|
||||
"apiVersion": "2018-06-17-preview",
|
||||
"dependsOn": [
|
||||
"[resourceId('microsoft.insights/components', parameters('name'))]"
|
||||
],
|
||||
"kind": "shared",
|
||||
"properties": {
|
||||
"displayName": "LibFuzzer Job Dashboard",
|
||||
"serializedData": "[parameters('workbookData').libFuzzerJob]",
|
||||
"version": "1.0",
|
||||
"sourceId": "[resourceId('microsoft.insights/components', parameters('name'))]",
|
||||
"category": "tsg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.OperationalInsights/workspaces",
|
||||
"name": "[variables('monitorAccountName')]",
|
||||
"apiVersion": "2017-03-15-preview",
|
||||
"location": "[resourceGroup().location]",
|
||||
"properties": {
|
||||
"sku": {
|
||||
"name": "PerGB2018"
|
||||
},
|
||||
"retentionInDays": 120,
|
||||
"features": {
|
||||
"searchVersion": 1,
|
||||
"legacy": 0,
|
||||
"enableLogAccessUsingOnlyResourcePermissions": true
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"location": "[resourceGroup().location]",
|
||||
"name": "[concat('VMInsights', '(', variables('monitorAccountName'), ')')]",
|
||||
"type": "Microsoft.OperationsManagement/solutions",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
},
|
||||
|
||||
"plan": {
|
||||
"name": "[concat('VMInsights', '(', variables('monitorAccountName'), ')')]",
|
||||
"publisher": "Microsoft",
|
||||
"product": "[Concat('OMSGallery/', 'VMInsights')]",
|
||||
"promotionCode": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "syslogDataSourceKern",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "LinuxSyslog",
|
||||
"properties": {
|
||||
"syslogName": "kern",
|
||||
"syslogSeverities": "[onefuzz.severitiesAtMostInfo()]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "syslogDataSourceUser",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "LinuxSyslog",
|
||||
"properties": {
|
||||
"syslogName": "user",
|
||||
"syslogSeverities": "[onefuzz.severitiesAtMostInfo()]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "syslogDataSourceDaemon",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "LinuxSyslog",
|
||||
"properties": {
|
||||
"syslogName": "daemon",
|
||||
"syslogSeverities": "[onefuzz.severitiesAtMostInfo()]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "syslogDataSourceCron",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "LinuxSyslog",
|
||||
"properties": {
|
||||
"syslogName": "cron",
|
||||
"syslogSeverities": "[onefuzz.severitiesAtMostInfo()]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "syslogDataSourceCollection",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "LinuxSyslogCollection",
|
||||
"properties": {
|
||||
"state": "Enabled"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "windowsEventSystem",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "WindowsEvent",
|
||||
"properties": {
|
||||
"eventLogName": "System",
|
||||
"eventTypes": [
|
||||
{
|
||||
"eventType": "Error"
|
||||
},
|
||||
{
|
||||
"eventType": "Warning"
|
||||
},
|
||||
{
|
||||
"eventType": "Information"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2015-11-01-preview",
|
||||
"type": "datasources",
|
||||
"name": "windowsEventApplication",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', variables('monitorAccountName'))]"
|
||||
],
|
||||
"kind": "WindowsEvent",
|
||||
"properties": {
|
||||
"eventLogName": "Application",
|
||||
"eventTypes": [
|
||||
{
|
||||
"eventType": "Error"
|
||||
},
|
||||
{
|
||||
"eventType": "Warning"
|
||||
},
|
||||
{
|
||||
"eventType": "Information"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"apiVersion": "2019-06-01",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"name": "[variables('storageAccountName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "StorageV2",
|
||||
"sku": {
|
||||
"name": "Standard_LRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"accessTier": "Hot"
|
||||
},
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2019-06-01",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"name": "[variables('storageAccountNameFunc')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "StorageV2",
|
||||
"sku": {
|
||||
"name": "Standard_LRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"accessTier": "Hot"
|
||||
},
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "[concat(variables('storageAccountNameFunc'), '/default')]",
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices",
|
||||
"apiVersion": "2019-06-01",
|
||||
"properties": {
|
||||
"deleteRetentionPolicy": {
|
||||
"enabled": true,
|
||||
"days": 30
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "[concat(variables('storageAccountName'), '/default')]",
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices",
|
||||
"apiVersion": "2019-06-01",
|
||||
"properties": {
|
||||
"deleteRetentionPolicy": {
|
||||
"enabled": true,
|
||||
"days": 30
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
|
||||
"apiVersion": "2018-03-01-preview",
|
||||
"name": "[concat(variables('storageAccountNameFunc'), '/default/', 'repro-scripts')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
|
||||
"apiVersion": "2018-03-01-preview",
|
||||
"name": "[concat(variables('storageAccountNameFunc'), '/default/', 'task-configs')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
|
||||
"apiVersion": "2018-03-01-preview",
|
||||
"name": "[concat(variables('storageAccountNameFunc'), '/default/', 'app-logs')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Authorization/roleAssignments",
|
||||
"apiVersion": "2017-09-01",
|
||||
"name": "[guid(concat(resourceGroup().id, '-vmss'))]",
|
||||
"properties": {
|
||||
"roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', variables('Virtual Machine Contributor'))]",
|
||||
"principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('name')), '2018-02-01', 'Full').identity.principalId]"
|
||||
},
|
||||
"DependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('name'))]"
|
||||
],
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Authorization/roleAssignments",
|
||||
"apiVersion": "2017-09-01",
|
||||
"name": "[guid(concat(resourceGroup().id, '-storage'))]",
|
||||
"properties": {
|
||||
"roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', variables('Storage Account Contributor'))]",
|
||||
"principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('name')), '2018-02-01', 'Full').identity.principalId]"
|
||||
},
|
||||
"DependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('name'))]"
|
||||
],
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Authorization/roleAssignments",
|
||||
"apiVersion": "2017-09-01",
|
||||
"name": "[guid(concat(resourceGroup().id, '-network'))]",
|
||||
"properties": {
|
||||
"roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', variables('Network Contributor'))]",
|
||||
"principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('name')), '2018-02-01', 'Full').identity.principalId]"
|
||||
},
|
||||
"DependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('name'))]"
|
||||
],
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Authorization/roleAssignments",
|
||||
"apiVersion": "2017-09-01",
|
||||
"name": "[guid(concat(resourceGroup().id, '-logs'))]",
|
||||
"properties": {
|
||||
"roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', variables('Log Analytics Contributor'))]",
|
||||
"principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('name')), '2018-02-01', 'Full').identity.principalId]"
|
||||
},
|
||||
"DependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', parameters('name'))]"
|
||||
],
|
||||
"tags": {
|
||||
"OWNER": "[parameters('owner')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.SignalRService/SignalR",
|
||||
"apiVersion": "2018-10-01",
|
||||
"name": "[variables('signalr-name')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Standard_S1",
|
||||
"tier": "Standard",
|
||||
"size": "S1",
|
||||
"capacity": 1
|
||||
},
|
||||
"properties": {
|
||||
"hostNamePrefix": "[variables('signalr-name')]",
|
||||
"features": [
|
||||
{
|
||||
"flag": "ServiceMode",
|
||||
"value": "Serverless",
|
||||
"properties": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"flag": "EnableConnectivityLogs",
|
||||
"value": "True",
|
||||
"properties": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"flag": "EnableMessagingLogs",
|
||||
"value": "False",
|
||||
"properties": {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"fuzz-storage": {
|
||||
"type": "string",
|
||||
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
|
||||
},
|
||||
"fuzz-name": {
|
||||
"type": "string",
|
||||
"value": "[variables('storageAccountName')]"
|
||||
},
|
||||
"fuzz-key": {
|
||||
"type": "string",
|
||||
"value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value]"
|
||||
},
|
||||
"func-name": {
|
||||
"type": "string",
|
||||
"value": "[variables('storageAccountNameFunc')]"
|
||||
},
|
||||
"func-storage": {
|
||||
"type": "string",
|
||||
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc'))]"
|
||||
},
|
||||
"func-key": {
|
||||
"type": "string",
|
||||
"value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountNameFunc')), '2019-06-01').keys[0].value]"
|
||||
}
|
||||
}
|
||||
}
|
655
src/deployment/deploy.py
Normal file
655
src/deployment/deploy.py
Normal file
@ -0,0 +1,655 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from azure.common.client_factory import get_client_from_cli_profile
|
||||
from azure.common.credentials import get_cli_profile
|
||||
from azure.core.exceptions import ResourceExistsError
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import (
|
||||
ApplicationCreateParameters,
|
||||
AppRole,
|
||||
GraphErrorException,
|
||||
OptionalClaims,
|
||||
RequiredResourceAccess,
|
||||
ResourceAccess,
|
||||
ServicePrincipalCreateParameters,
|
||||
)
|
||||
from azure.mgmt.eventgrid import EventGridManagementClient
|
||||
from azure.mgmt.eventgrid.models import (
|
||||
EventSubscription,
|
||||
EventSubscriptionFilter,
|
||||
RetryPolicy,
|
||||
StorageQueueEventSubscriptionDestination,
|
||||
)
|
||||
from azure.mgmt.resource import ResourceManagementClient, SubscriptionClient
|
||||
from azure.mgmt.resource.resources.models import (
|
||||
Deployment,
|
||||
DeploymentMode,
|
||||
DeploymentProperties,
|
||||
)
|
||||
from azure.mgmt.storage import StorageManagementClient
|
||||
from azure.storage.blob import (
|
||||
BlobServiceClient,
|
||||
ContainerSasPermissions,
|
||||
generate_container_sas,
|
||||
)
|
||||
from azure.storage.queue import QueueServiceClient
|
||||
from msrest.serialization import TZ_UTC
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from register_pool_application import (
|
||||
add_application_password,
|
||||
authorize_application,
|
||||
update_registration,
|
||||
get_application,
|
||||
register_application,
|
||||
)
|
||||
|
||||
USER_IMPERSONATION = "311a71cc-e848-46a1-bdf8-97ff7156d8e6"
|
||||
ONEFUZZ_CLI_APP = "72f1562a-8c0c-41ea-beb9-fa2b71c80134"
|
||||
ONEFUZZ_CLI_AUTHORITY = (
|
||||
"https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"
|
||||
)
|
||||
TELEMETRY_NOTICE = (
|
||||
"Telemetry collection on stats and OneFuzz failures are sent to Microsoft. "
|
||||
"To disable, delete the ONEFUZZ_TELEMETRY application setting in the "
|
||||
"Azure Functions instance"
|
||||
)
|
||||
FUNC_TOOLS_ERROR = (
|
||||
"azure-functions-core-tools is not installed, "
|
||||
"install v3 using instructions: "
|
||||
"https://github.com/Azure/azure-functions-core-tools#installing"
|
||||
)
|
||||
|
||||
logger = logging.getLogger("deploy")
|
||||
|
||||
|
||||
def gen_guid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(
|
||||
self,
|
||||
resource_group,
|
||||
location,
|
||||
application_name,
|
||||
owner,
|
||||
client_id,
|
||||
client_secret,
|
||||
app_zip,
|
||||
tools,
|
||||
instance_specific,
|
||||
third_party,
|
||||
arm_template,
|
||||
workbook_data,
|
||||
create_registration,
|
||||
):
|
||||
self.resource_group = resource_group
|
||||
self.arm_template = arm_template
|
||||
self.location = location
|
||||
self.application_name = application_name
|
||||
self.owner = owner
|
||||
self.app_zip = app_zip
|
||||
self.tools = tools
|
||||
self.instance_specific = instance_specific
|
||||
self.third_party = third_party
|
||||
self.create_registration = create_registration
|
||||
self.results = {
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
self.cli_config = {
|
||||
"client_id": ONEFUZZ_CLI_APP,
|
||||
"authority": ONEFUZZ_CLI_AUTHORITY,
|
||||
}
|
||||
|
||||
if os.name == "nt":
|
||||
self.azcopy = os.path.join(self.tools, "win64", "azcopy.exe")
|
||||
else:
|
||||
self.azcopy = os.path.join(self.tools, "linux", "azcopy")
|
||||
subprocess.check_output(["chmod", "+x", self.azcopy])
|
||||
|
||||
with open(workbook_data) as f:
|
||||
self.workbook_data = json.load(f)
|
||||
|
||||
def get_subscription_id(self):
|
||||
profile = get_cli_profile()
|
||||
return profile.get_subscription_id()
|
||||
|
||||
def get_location_display_name(self):
|
||||
location_client = get_client_from_cli_profile(SubscriptionClient)
|
||||
locations = location_client.subscriptions.list_locations(
|
||||
self.get_subscription_id()
|
||||
)
|
||||
for location in locations:
|
||||
if location.name == self.location:
|
||||
return location.display_name
|
||||
|
||||
raise Exception("unknown location: %s", self.location)
|
||||
|
||||
def check_region(self):
|
||||
# At the moment, this only checks are the specified providers available
|
||||
# in the selected region
|
||||
|
||||
location = self.get_location_display_name()
|
||||
|
||||
with open(self.arm_template, "r") as handle:
|
||||
arm = json.load(handle)
|
||||
|
||||
client = get_client_from_cli_profile(ResourceManagementClient)
|
||||
providers = {x.namespace: x for x in client.providers.list()}
|
||||
|
||||
unsupported = []
|
||||
|
||||
for resource in arm["resources"]:
|
||||
namespace, name = resource["type"].split("/", 1)
|
||||
|
||||
# resource types are in the form of a/b/c....
|
||||
# only the top two are listed as resource types within providers
|
||||
name = "/".join(name.split("/")[:2])
|
||||
|
||||
if namespace not in providers:
|
||||
unsupported.append("Unsupported provider: %s" % namespace)
|
||||
continue
|
||||
|
||||
provider = providers[namespace]
|
||||
resource_types = {x.resource_type: x for x in provider.resource_types}
|
||||
if name not in resource_types:
|
||||
unsupported.append(
|
||||
"Unsupported resource type: %s/%s" % (namespace, name)
|
||||
)
|
||||
continue
|
||||
|
||||
resource_type = resource_types[name]
|
||||
if (
|
||||
location not in resource_type.locations
|
||||
and len(resource_type.locations) > 0
|
||||
):
|
||||
unsupported.append(
|
||||
"%s/%s is unsupported in %s" % (namespace, name, self.location)
|
||||
)
|
||||
|
||||
if unsupported:
|
||||
print("The following resources required by onefuzz are not supported:")
|
||||
print("\n".join(["* " + x for x in unsupported]))
|
||||
sys.exit(1)
|
||||
|
||||
def setup_rbac(self):
|
||||
"""
|
||||
Setup the client application for the OneFuzz instance.
|
||||
|
||||
By default, Service Principals do not have access to create
|
||||
client applications in AAD.
|
||||
"""
|
||||
if self.results["client_id"] and self.results["client_secret"]:
|
||||
logger.info("using existing client application")
|
||||
return
|
||||
|
||||
client = get_client_from_cli_profile(GraphRbacManagementClient)
|
||||
logger.info("checking if RBAC already exists")
|
||||
|
||||
try:
|
||||
existing = list(
|
||||
client.applications.list(
|
||||
filter="displayName eq '%s'" % self.application_name
|
||||
)
|
||||
)
|
||||
except GraphErrorException:
|
||||
logger.error("unable to query RBAC. Provide client_id and client_secret")
|
||||
sys.exit(1)
|
||||
|
||||
if not existing:
|
||||
logger.info("creating Application registration")
|
||||
url = "https://%s.azurewebsites.net" % self.application_name
|
||||
|
||||
params = ApplicationCreateParameters(
|
||||
display_name=self.application_name,
|
||||
identifier_uris=[url],
|
||||
reply_urls=[url + "/.auth/login/aad/callback"],
|
||||
optional_claims=OptionalClaims(id_token=[], access_token=[]),
|
||||
required_resource_access=[
|
||||
RequiredResourceAccess(
|
||||
resource_access=[
|
||||
ResourceAccess(id=USER_IMPERSONATION, type="Scope")
|
||||
],
|
||||
resource_app_id="00000002-0000-0000-c000-000000000000",
|
||||
)
|
||||
],
|
||||
app_roles=[
|
||||
AppRole(
|
||||
allowed_member_types=["Application"],
|
||||
display_name="CliClient",
|
||||
id=str(uuid.uuid4()),
|
||||
is_enabled=True,
|
||||
description="Allows access from the CLI.",
|
||||
value="CliClient",
|
||||
),
|
||||
AppRole(
|
||||
allowed_member_types=["Application"],
|
||||
display_name="LabMachine",
|
||||
id=str(uuid.uuid4()),
|
||||
is_enabled=True,
|
||||
description="Allow access from a lab machine.",
|
||||
value="LabMachine",
|
||||
),
|
||||
],
|
||||
)
|
||||
app = client.applications.create(params)
|
||||
|
||||
logger.info("creating service principal")
|
||||
service_principal_params = ServicePrincipalCreateParameters(
|
||||
account_enabled=True,
|
||||
app_role_assignment_required=False,
|
||||
service_principal_type="Application",
|
||||
app_id=app.app_id,
|
||||
)
|
||||
client.service_principals.create(service_principal_params)
|
||||
else:
|
||||
app = existing[0]
|
||||
creds = list(client.applications.list_password_credentials(app.object_id))
|
||||
client.applications.update_password_credentials(app.object_id, creds)
|
||||
|
||||
(password_id, password) = add_application_password(app.object_id)
|
||||
onefuzz_cli_app_uuid = uuid.UUID(ONEFUZZ_CLI_APP)
|
||||
cli_app = get_application(onefuzz_cli_app_uuid)
|
||||
|
||||
if cli_app is None:
|
||||
logger.info(
|
||||
"Could not find the default CLI application under the current subscription, creating a new one"
|
||||
)
|
||||
app_info = register_application("onefuzz-cli", self.application_name)
|
||||
self.cli_config = {
|
||||
"client_id": app_info.client_id,
|
||||
"authority": app_info.authority,
|
||||
}
|
||||
|
||||
else:
|
||||
authorize_application(onefuzz_cli_app_uuid, app.app_id)
|
||||
|
||||
self.results["client_id"] = app.app_id
|
||||
self.results["client_secret"] = password
|
||||
|
||||
# Log `client_secret` for consumption by CI.
|
||||
logger.debug("client_id: %s client_secret: %s", app.app_id, password)
|
||||
|
||||
def deploy_template(self):
|
||||
logger.info("deploying arm template: %s", self.arm_template)
|
||||
|
||||
with open(self.arm_template, "r") as template_handle:
|
||||
template = json.load(template_handle)
|
||||
|
||||
client = get_client_from_cli_profile(ResourceManagementClient)
|
||||
client.resource_groups.create_or_update(
|
||||
self.resource_group, {"location": self.location}
|
||||
)
|
||||
|
||||
expiry = (datetime.now(TZ_UTC) + timedelta(days=365)).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
params = {
|
||||
"name": {"value": self.application_name},
|
||||
"owner": {"value": self.owner},
|
||||
"clientId": {"value": self.results["client_id"]},
|
||||
"clientSecret": {"value": self.results["client_secret"]},
|
||||
"signedExpiry": {"value": expiry},
|
||||
"workbookData": {"value": self.workbook_data},
|
||||
}
|
||||
deployment = Deployment(
|
||||
properties=DeploymentProperties(
|
||||
mode=DeploymentMode.incremental, template=template, parameters=params
|
||||
)
|
||||
)
|
||||
result = client.deployments.create_or_update(
|
||||
self.resource_group, gen_guid(), deployment
|
||||
).result()
|
||||
if result.properties.provisioning_state != "Succeeded":
|
||||
logger.error(
|
||||
"error deploying: %s",
|
||||
json.dumps(result.as_dict(), indent=4, sort_keys=True),
|
||||
)
|
||||
sys.exit(1)
|
||||
self.results["deploy"] = result.properties.outputs
|
||||
|
||||
def create_queues(self):
|
||||
logger.info("creating eventgrid destination queue")
|
||||
|
||||
name = self.results["deploy"]["func-name"]["value"]
|
||||
key = self.results["deploy"]["func-key"]["value"]
|
||||
account_url = "https://%s.queue.core.windows.net" % name
|
||||
client = QueueServiceClient(
|
||||
account_url=account_url,
|
||||
credential={"account_name": name, "account_key": key},
|
||||
)
|
||||
for queue in ["file-changes", "heartbeat", "proxy", "update-queue"]:
|
||||
try:
|
||||
client.create_queue(queue)
|
||||
except ResourceExistsError:
|
||||
pass
|
||||
|
||||
def create_eventgrid(self):
|
||||
logger.info("creating eventgrid subscription")
|
||||
src_resource_id = self.results["deploy"]["fuzz-storage"]["value"]
|
||||
dst_resource_id = self.results["deploy"]["func-storage"]["value"]
|
||||
client = get_client_from_cli_profile(StorageManagementClient)
|
||||
event_subscription_info = EventSubscription(
|
||||
destination=StorageQueueEventSubscriptionDestination(
|
||||
resource_id=dst_resource_id, queue_name="file-changes"
|
||||
),
|
||||
filter=EventSubscriptionFilter(
|
||||
included_event_types=[
|
||||
"Microsoft.Storage.BlobCreated",
|
||||
"Microsoft.Storage.BlobDeleted",
|
||||
]
|
||||
),
|
||||
retry_policy=RetryPolicy(
|
||||
max_delivery_attempts=30,
|
||||
event_time_to_live_in_minutes=1440,
|
||||
),
|
||||
)
|
||||
|
||||
client = get_client_from_cli_profile(EventGridManagementClient)
|
||||
result = client.event_subscriptions.create_or_update(
|
||||
src_resource_id, "onefuzz1", event_subscription_info
|
||||
).result()
|
||||
if result.provisioning_state != "Succeeded":
|
||||
raise Exception(
|
||||
"eventgrid subscription failed: %s"
|
||||
% json.dumps(result.as_dict(), indent=4, sort_keys=True),
|
||||
)
|
||||
|
||||
def upload_tools(self):
|
||||
logger.info("uploading tools from %s", self.tools)
|
||||
account_name = self.results["deploy"]["func-name"]["value"]
|
||||
key = self.results["deploy"]["func-key"]["value"]
|
||||
account_url = "https://%s.blob.core.windows.net" % account_name
|
||||
client = BlobServiceClient(account_url, credential=key)
|
||||
if "tools" not in [x["name"] for x in client.list_containers()]:
|
||||
client.create_container("tools")
|
||||
|
||||
expiry = datetime.utcnow() + timedelta(minutes=30)
|
||||
|
||||
sas = generate_container_sas(
|
||||
account_name,
|
||||
"tools",
|
||||
account_key=key,
|
||||
permission=ContainerSasPermissions(
|
||||
read=True, write=True, delete=True, list=True
|
||||
),
|
||||
expiry=expiry,
|
||||
)
|
||||
url = "%s/%s?%s" % (account_url, "tools", sas)
|
||||
|
||||
subprocess.check_output(
|
||||
[self.azcopy, "sync", self.tools, url, "--delete-destination", "true"]
|
||||
)
|
||||
|
||||
def upload_instance_setup(self):
|
||||
logger.info("uploading instance-specific-setup from %s", self.instance_specific)
|
||||
account_name = self.results["deploy"]["func-name"]["value"]
|
||||
key = self.results["deploy"]["func-key"]["value"]
|
||||
account_url = "https://%s.blob.core.windows.net" % account_name
|
||||
client = BlobServiceClient(account_url, credential=key)
|
||||
if "instance-specific-setup" not in [
|
||||
x["name"] for x in client.list_containers()
|
||||
]:
|
||||
client.create_container("instance-specific-setup")
|
||||
|
||||
expiry = datetime.utcnow() + timedelta(minutes=30)
|
||||
|
||||
sas = generate_container_sas(
|
||||
account_name,
|
||||
"instance-specific-setup",
|
||||
account_key=key,
|
||||
permission=ContainerSasPermissions(
|
||||
read=True, write=True, delete=True, list=True
|
||||
),
|
||||
expiry=expiry,
|
||||
)
|
||||
url = "%s/%s?%s" % (account_url, "instance-specific-setup", sas)
|
||||
|
||||
subprocess.check_output(
|
||||
[
|
||||
self.azcopy,
|
||||
"sync",
|
||||
self.instance_specific,
|
||||
url,
|
||||
"--delete-destination",
|
||||
"true",
|
||||
]
|
||||
)
|
||||
|
||||
def upload_third_party(self):
|
||||
logger.info("uploading third-party tools from %s", self.third_party)
|
||||
account_name = self.results["deploy"]["fuzz-name"]["value"]
|
||||
key = self.results["deploy"]["fuzz-key"]["value"]
|
||||
account_url = "https://%s.blob.core.windows.net" % account_name
|
||||
|
||||
client = BlobServiceClient(account_url, credential=key)
|
||||
containers = [x["name"] for x in client.list_containers()]
|
||||
|
||||
for name in os.listdir(self.third_party):
|
||||
path = os.path.join(self.third_party, name)
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
if name not in containers:
|
||||
client.create_container(name)
|
||||
|
||||
expiry = datetime.utcnow() + timedelta(minutes=30)
|
||||
sas = generate_container_sas(
|
||||
account_name,
|
||||
name,
|
||||
account_key=key,
|
||||
permission=ContainerSasPermissions(
|
||||
read=True, write=True, delete=True, list=True
|
||||
),
|
||||
expiry=expiry,
|
||||
)
|
||||
url = "%s/%s?%s" % (account_url, name, sas)
|
||||
|
||||
subprocess.check_output(
|
||||
[self.azcopy, "sync", path, url, "--delete-destination", "true"]
|
||||
)
|
||||
|
||||
def deploy_app(self):
|
||||
logger.info("deploying function app %s", self.app_zip)
|
||||
current_dir = os.getcwd()
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
with zipfile.ZipFile(self.app_zip, "r") as zip_ref:
|
||||
zip_ref.extractall(tmpdirname)
|
||||
os.chdir(tmpdirname)
|
||||
subprocess.check_output(
|
||||
[
|
||||
shutil.which("func"),
|
||||
"azure",
|
||||
"functionapp",
|
||||
"publish",
|
||||
self.application_name,
|
||||
"--python",
|
||||
"--no-build",
|
||||
],
|
||||
env=dict(os.environ, CLI_DEBUG="1"),
|
||||
)
|
||||
|
||||
os.chdir(current_dir)
|
||||
|
||||
def update_registration(self):
|
||||
if not self.create_registration:
|
||||
return
|
||||
update_registration(self.application_name)
|
||||
|
||||
def done(self):
|
||||
logger.info(TELEMETRY_NOTICE)
|
||||
client_secret_arg = (
|
||||
("--client_secret %s" % self.cli_config["client_secret"])
|
||||
if "client_secret" in self.cli_config
|
||||
else ""
|
||||
)
|
||||
logger.info(
|
||||
"Update your CLI config via: onefuzz config --endpoint https://%s.azurewebsites.net --authority %s --client_id %s %s",
|
||||
self.application_name,
|
||||
self.cli_config["authority"],
|
||||
self.cli_config["client_id"],
|
||||
client_secret_arg,
|
||||
)
|
||||
|
||||
|
||||
def arg_dir(arg):
|
||||
if not os.path.isdir(arg):
|
||||
raise argparse.ArgumentTypeError("not a directory: %s" % arg)
|
||||
return arg
|
||||
|
||||
|
||||
def arg_file(arg):
|
||||
if not os.path.isfile(arg):
|
||||
raise argparse.ArgumentTypeError("not a file: %s" % arg)
|
||||
return arg
|
||||
|
||||
|
||||
def main():
|
||||
states = [
|
||||
("check_region", Client.check_region),
|
||||
("rbac", Client.setup_rbac),
|
||||
("arm", Client.deploy_template),
|
||||
("queues", Client.create_queues),
|
||||
("eventgrid", Client.create_eventgrid),
|
||||
("tools", Client.upload_tools),
|
||||
("instance-specific-setup", Client.upload_instance_setup),
|
||||
("third-party", Client.upload_third_party),
|
||||
("api", Client.deploy_app),
|
||||
("update_registration", Client.update_registration),
|
||||
]
|
||||
|
||||
formatter = argparse.ArgumentDefaultsHelpFormatter
|
||||
parser = argparse.ArgumentParser(formatter_class=formatter)
|
||||
parser.add_argument("location")
|
||||
parser.add_argument("resource_group")
|
||||
parser.add_argument("application_name")
|
||||
parser.add_argument("owner")
|
||||
parser.add_argument(
|
||||
"--arm-template",
|
||||
type=arg_file,
|
||||
default="azuredeploy.json",
|
||||
help="(default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workbook-data",
|
||||
type=arg_file,
|
||||
default="workbook-data.json",
|
||||
help="(default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--app-zip",
|
||||
type=arg_file,
|
||||
default="api-service.zip",
|
||||
help="(default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tools", type=arg_dir, default="tools", help="(default: %(default)s)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instance_specific",
|
||||
type=arg_dir,
|
||||
default="instance-specific-setup",
|
||||
help="(default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--third-party",
|
||||
type=arg_dir,
|
||||
default="third-party",
|
||||
help="(default: %(default)s)",
|
||||
)
|
||||
parser.add_argument("--client_id")
|
||||
parser.add_argument("--client_secret")
|
||||
parser.add_argument(
|
||||
"--start_at",
|
||||
default=states[0][0],
|
||||
choices=[x[0] for x in states],
|
||||
help=(
|
||||
"Debug deployments by starting at a specific state. "
|
||||
"NOT FOR PRODUCTION USE. (default: %(default)s)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
parser.add_argument(
|
||||
"--create_pool_registration",
|
||||
default=False,
|
||||
type=bool,
|
||||
help="Create an application registration and/or generate a "
|
||||
"password for the pool agent (default: False)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if shutil.which("func") is None:
|
||||
logger.error(FUNC_TOOLS_ERROR)
|
||||
sys.exit(1)
|
||||
|
||||
client = Client(
|
||||
args.resource_group,
|
||||
args.location,
|
||||
args.application_name,
|
||||
args.owner,
|
||||
args.client_id,
|
||||
args.client_secret,
|
||||
args.app_zip,
|
||||
args.tools,
|
||||
args.instance_specific,
|
||||
args.third_party,
|
||||
args.arm_template,
|
||||
args.workbook_data,
|
||||
args.create_pool_registration,
|
||||
)
|
||||
if args.verbose:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.WARN
|
||||
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
logging.getLogger("deploy").setLevel(logging.INFO)
|
||||
|
||||
# TODO: using az_cli resets logging defaults. For now, force these
|
||||
# to be WARN level
|
||||
if not args.verbose:
|
||||
for entry in [
|
||||
"adal-python",
|
||||
"msrest.universal_http",
|
||||
"urllib3.connectionpool",
|
||||
"az_command_data_logger",
|
||||
"msrest.service_client",
|
||||
"azure.core.pipeline.policies.http_logging_policy",
|
||||
]:
|
||||
logging.getLogger(entry).setLevel(logging.WARN)
|
||||
|
||||
if args.start_at != states[0][0]:
|
||||
logger.warning(
|
||||
"*** Starting at a non-standard deployment state. "
|
||||
"This may result in a broken deployment. ***"
|
||||
)
|
||||
|
||||
started = False
|
||||
for state in states:
|
||||
if args.start_at == state[0]:
|
||||
started = True
|
||||
if started:
|
||||
state[1](client)
|
||||
|
||||
client.done()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
38
src/deployment/deployment-role.json
Normal file
38
src/deployment/deployment-role.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"Name": "OneFuzz Deployment",
|
||||
"Description": "Permissions required for OneFuzz deployment",
|
||||
"Actions": [
|
||||
"Microsoft.Authorization/locks/*",
|
||||
"Microsoft.Authorization/roleAssignments/write",
|
||||
"Microsoft.EventGrid/eventSubscriptions/read",
|
||||
"Microsoft.EventGrid/eventSubscriptions/write",
|
||||
"Microsoft.Insights/components/read",
|
||||
"Microsoft.Insights/components/write",
|
||||
"Microsoft.Resources/deployments/operationStatuses/read",
|
||||
"Microsoft.Resources/deployments/read",
|
||||
"Microsoft.Resources/deployments/write",
|
||||
"Microsoft.Resources/deployments/validate/action",
|
||||
"Microsoft.Resources/subscriptions/resourceGroups/read",
|
||||
"Microsoft.Resources/subscriptions/resourceGroups/write",
|
||||
"Microsoft.Resources/subscriptions/resourcegroups/delete",
|
||||
"Microsoft.Storage/storageAccounts/blobServices/containers/write",
|
||||
"Microsoft.Storage/storageAccounts/blobServices/write",
|
||||
"Microsoft.Storage/storageAccounts/listKeys/action",
|
||||
"Microsoft.Storage/storageAccounts/read",
|
||||
"Microsoft.Storage/storageAccounts/write",
|
||||
"Microsoft.Web/serverfarms/write",
|
||||
"Microsoft.Web/serverfarms/read",
|
||||
"Microsoft.Web/sites/config/list/action",
|
||||
"Microsoft.Web/sites/config/read",
|
||||
"Microsoft.Web/sites/config/write",
|
||||
"Microsoft.Web/sites/publishxml/action",
|
||||
"Microsoft.Web/sites/restart/action",
|
||||
"Microsoft.Web/sites/read",
|
||||
"Microsoft.Web/sites/write"
|
||||
],
|
||||
"DataActions": [],
|
||||
"NotDataActions": [],
|
||||
"AssignableScopes": [
|
||||
"/subscriptions/038d675a-9bbe-4964-9cd1-6d50071a61b5"
|
||||
]
|
||||
}
|
3
src/deployment/instance-specific-setup/README.md
Normal file
3
src/deployment/instance-specific-setup/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
If you have setup scripts, configurations, or tools that should always run on every VM, please put them here.
|
||||
|
||||
Either `linux/setup.sh` or `windows/setup.ps1` will be launched during VM initialization.
|
6
src/deployment/instance-specific-setup/linux/setup.sh
Normal file
6
src/deployment/instance-specific-setup/linux/setup.sh
Normal file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Put your instance specific setup code here.
|
||||
|
||||
set -ex
|
||||
|
||||
echo This is a sample setup tool
|
3
src/deployment/instance-specific-setup/windows/setup.ps1
Normal file
3
src/deployment/instance-specific-setup/windows/setup.ps1
Normal file
@ -0,0 +1,3 @@
|
||||
# This is a custom setup tool
|
||||
|
||||
Write-Output "Custom setup"
|
288
src/deployment/register_pool_application.py
Normal file
288
src/deployment/register_pool_application.py
Normal file
@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Tuple, NamedTuple, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from azure.cli.core import get_default_cli # type: ignore
|
||||
from azure.common.client_factory import get_client_from_cli_profile
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import (
|
||||
Application,
|
||||
ApplicationCreateParameters,
|
||||
RequiredResourceAccess,
|
||||
ResourceAccess,
|
||||
)
|
||||
from functional import seq
|
||||
from msrest.serialization import TZ_UTC
|
||||
|
||||
|
||||
logger = logging.getLogger("deploy")
|
||||
|
||||
|
||||
def az_cli(args):
|
||||
cli = get_default_cli()
|
||||
cli.invoke(args, out_file=open(os.devnull, "w"))
|
||||
if cli.result.result:
|
||||
return cli.result.result
|
||||
elif cli.result.error:
|
||||
raise cli.result.error
|
||||
|
||||
|
||||
class ApplicationInfo(NamedTuple):
|
||||
client_id: UUID
|
||||
client_secret: str
|
||||
authority: str
|
||||
|
||||
|
||||
def register_application(
|
||||
registration_name: str, onefuzz_instance_name: str
|
||||
) -> ApplicationInfo:
|
||||
logger.debug("retrieving the application registration")
|
||||
client = get_client_from_cli_profile(GraphRbacManagementClient)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % registration_name)
|
||||
)
|
||||
|
||||
if len(apps) == 0:
|
||||
logger.debug("No existing registration found. creating a new one")
|
||||
app = create_application_registration(onefuzz_instance_name, registration_name)
|
||||
else:
|
||||
app = apps[0]
|
||||
|
||||
onefuzz_apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||
)
|
||||
|
||||
if len(onefuzz_apps) == 0:
|
||||
raise Exception("onefuzz app not found")
|
||||
|
||||
onefuzz_app = onefuzz_apps[0]
|
||||
pre_authorized_applications = (
|
||||
onefuzz_app.pre_authorized_applications
|
||||
if onefuzz_app.pre_authorized_applications is not None
|
||||
else []
|
||||
)
|
||||
|
||||
if app.app_id not in [app.app_id for app in pre_authorized_applications]:
|
||||
authorize_application(UUID(app.app_id), UUID(onefuzz_app.app_id))
|
||||
|
||||
password = create_application_credential(registration_name)
|
||||
|
||||
return ApplicationInfo(
|
||||
client_id=app.app_id,
|
||||
client_secret=password,
|
||||
authority=("https://login.microsoftonline.com/%s" % client.config.tenant_id),
|
||||
)
|
||||
|
||||
|
||||
def create_application_credential(application_name: str) -> str:
|
||||
""" Add a new password to the application registration """
|
||||
|
||||
client = get_client_from_cli_profile(GraphRbacManagementClient)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % application_name)
|
||||
)
|
||||
|
||||
app: Application = apps[0]
|
||||
|
||||
(key, password) = add_application_password(app.object_id)
|
||||
return str(password)
|
||||
|
||||
|
||||
def create_application_registration(
|
||||
onefuzz_instance_name: str, name: str
|
||||
) -> Application:
|
||||
""" Create an application registration """
|
||||
|
||||
client = get_client_from_cli_profile(GraphRbacManagementClient)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||
)
|
||||
|
||||
app = apps[0]
|
||||
resource_access = [
|
||||
ResourceAccess(id=role.id, type="Role") for role in app.app_roles
|
||||
]
|
||||
|
||||
params = ApplicationCreateParameters(
|
||||
is_device_only_auth_supported=True,
|
||||
display_name=name,
|
||||
identifier_uris=[],
|
||||
password_credentials=[],
|
||||
required_resource_access=(
|
||||
[
|
||||
RequiredResourceAccess(
|
||||
resource_access=resource_access, resource_app_id=app.app_id,
|
||||
)
|
||||
]
|
||||
if len(resource_access) > 0
|
||||
else []
|
||||
),
|
||||
)
|
||||
|
||||
registered_app: Application = client.applications.create(params)
|
||||
body = {
|
||||
"publicClient": {
|
||||
"redirectUris": ["https://%s.azurewebsites.net" % onefuzz_instance_name]
|
||||
},
|
||||
"isFallbackPublicClient": True,
|
||||
}
|
||||
az_cli(
|
||||
[
|
||||
"rest",
|
||||
"-m",
|
||||
"PATCH",
|
||||
"-u",
|
||||
"https://graph.microsoft.com/v1.0/applications/%s"
|
||||
% registered_app.object_id,
|
||||
"--headers",
|
||||
"Content-Type=application/json",
|
||||
"-b",
|
||||
json.dumps(body),
|
||||
]
|
||||
)
|
||||
authorize_application(UUID(registered_app.app_id), UUID(app.app_id))
|
||||
return registered_app
|
||||
|
||||
|
||||
def add_application_password(app_object_id: UUID) -> Tuple[str, str]:
|
||||
|
||||
key = uuid4()
|
||||
password_request = {
|
||||
"passwordCredential": {
|
||||
"displayName": "%s" % key,
|
||||
"startDateTime": "%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"),
|
||||
"endDateTime": "%s"
|
||||
% (datetime.now(TZ_UTC) + timedelta(days=365)).strftime(
|
||||
"%Y-%m-%dT%H:%M.%fZ"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
password: Dict = az_cli(
|
||||
[
|
||||
"rest",
|
||||
"-m",
|
||||
"POST",
|
||||
"-u",
|
||||
"https://graph.microsoft.com/v1.0/applications/%s/addPassword"
|
||||
% app_object_id,
|
||||
"-b",
|
||||
json.dumps(password_request),
|
||||
]
|
||||
)
|
||||
|
||||
return (str(key), password["secretText"])
|
||||
|
||||
|
||||
def get_application(app_id: UUID) -> Optional[Dict]:
|
||||
apps: Dict = az_cli(
|
||||
[
|
||||
"rest",
|
||||
"-m",
|
||||
"GET",
|
||||
"-u",
|
||||
"https://graph.microsoft.com/v1.0/applications",
|
||||
"--uri-parameters",
|
||||
"$filter=appId eq '%s'" % app_id,
|
||||
]
|
||||
)
|
||||
|
||||
if len(apps["value"]) == 0:
|
||||
return None
|
||||
|
||||
return apps["value"][0]
|
||||
|
||||
|
||||
def authorize_application(
|
||||
registration_app_id: UUID,
|
||||
onefuzz_app_id: UUID,
|
||||
permissions: List[str] = ["user_impersonation"],
|
||||
):
|
||||
onefuzz_app = get_application(onefuzz_app_id)
|
||||
if onefuzz_app is None:
|
||||
logger.error("Application '%s' not found" % onefuzz_app_id)
|
||||
return
|
||||
|
||||
scopes = seq(onefuzz_app["api"]["oauth2PermissionScopes"]).filter(
|
||||
lambda scope: scope["value"] in permissions
|
||||
)
|
||||
|
||||
existing_preAuthorizedApplications = (
|
||||
seq(onefuzz_app["api"]["preAuthorizedApplications"])
|
||||
.map(
|
||||
lambda paa: seq(paa["delegatedPermissionIds"]).map(
|
||||
lambda permission_id: (paa["appId"], permission_id)
|
||||
)
|
||||
)
|
||||
.flatten()
|
||||
)
|
||||
|
||||
preAuthorizedApplications = (
|
||||
scopes.map(lambda s: (str(registration_app_id), s["id"]))
|
||||
.union(existing_preAuthorizedApplications)
|
||||
.distinct()
|
||||
.group_by_key()
|
||||
.map(lambda data: {"appId": data[0], "delegatedPermissionIds": data[1]})
|
||||
)
|
||||
|
||||
body = {"api": {"preAuthorizedApplications": preAuthorizedApplications.to_list()}}
|
||||
|
||||
az_cli(
|
||||
[
|
||||
"rest",
|
||||
"-m",
|
||||
"PATCH",
|
||||
"-u",
|
||||
"https://graph.microsoft.com/v1.0/applications/%s" % onefuzz_app["id"],
|
||||
"--headers",
|
||||
"Content-Type=application/json",
|
||||
"-b",
|
||||
json.dumps(body),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def update_registration(application_name: str):
|
||||
|
||||
logger.info("Updating application registration")
|
||||
application_info = register_application(
|
||||
registration_name=("%s_pool" % application_name),
|
||||
onefuzz_instance_name=application_name,
|
||||
)
|
||||
logger.info("Registration complete")
|
||||
logger.info("These generated credentials are valid for a year")
|
||||
logger.info("client_id: %s" % application_info.client_id)
|
||||
logger.info("client_secret: %s" % application_info.client_secret)
|
||||
|
||||
|
||||
def main():
|
||||
formatter = argparse.ArgumentDefaultsHelpFormatter
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=formatter,
|
||||
description="Create an application registration and/or generate a password for the pool agent",
|
||||
)
|
||||
parser.add_argument("application_name")
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.verbose:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.WARN
|
||||
|
||||
logging.basicConfig(format="%(levelname)s:%(message)s", level=level)
|
||||
logging.getLogger("deploy").setLevel(logging.INFO)
|
||||
|
||||
update_registration(args.application_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
12
src/deployment/requirements.txt
Normal file
12
src/deployment/requirements.txt
Normal file
@ -0,0 +1,12 @@
|
||||
azure-mgmt-storage~=11.0
|
||||
azure-storage-queue~=12.1
|
||||
azure-storage-blob~=12.3
|
||||
azure-core~=1.6
|
||||
azure-graphrbac~=0.61.1
|
||||
azure-mgmt-eventgrid~=2.2
|
||||
azure-mgmt-web~=0.45
|
||||
azure-mgmt-resource~=9.0
|
||||
azure-mgmt-servicebus~=0.6.0
|
||||
azure-cli-core~=2.7
|
||||
azure-cli~=2.10.1
|
||||
pyfunctional~=1.4.1
|
3
src/deployment/workbook-data.json
Normal file
3
src/deployment/workbook-data.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user