initial public release

This commit is contained in:
Brian Caswell
2020-09-18 12:21:04 -04:00
parent 9c3aa0bdfb
commit d3a0b292e6
387 changed files with 43810 additions and 28 deletions

1
src/deployment/README.md Normal file
View File

@ -0,0 +1 @@
deployment-role.json - Example Role for deploying OneFuzz

View 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
View 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()

View 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"
]
}

View 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.

View File

@ -0,0 +1,6 @@
#!/bin/bash
# Put your instance specific setup code here.
set -ex
echo This is a sample setup tool

View File

@ -0,0 +1,3 @@
# This is a custom setup tool
Write-Output "Custom setup"

View 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()

View 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

File diff suppressed because one or more lines are too long