Extending Microsoft Copilot for Security Capabilities with Azure Function Apps
Azure Function Apps Function Apps) offer a convenient way to execute functions in a server-less environment. They allow users to write functions in C#, Java, JavaScript, PowerShell, Python and Typescript which can then be called using several trigger options. One of the most common triggers is the HTTP trigger allowing functions to be called like a REST API.
Microsoft Copilot for Security (Copilot) is a large language model (LLM) based generative Artificial Intelligence (AI) system for cybersecurity use cases. Copilot is not a monolithic system but is an ecosystem running on a platform that allows data requests to be fulfilled from multiple sources using a unique plugin mechanism. The plugin mechanism gives Copilot the capability to pull data for any external data source that supports a REST like API, thus allowing Copilot to call Function Apps that use the HTTP trigger.
By using Function Apps as Copilot for Security plugins, you can have Copilot indirectly execute custom code. In the code, custom use cases which are not provided by current set of Microsoft and non-Microsoft plugins or Logic Apps, can be implemented. Some examples where Function Apps can be used are:
Calling external APIs which use non-standard authentication: If an external API does not use oAuth2 or any of the supported authentication types in Copilot, or require a message hash to generated with each API request, the only way to make the API request is by executing custom code from a Function App.
Data Massaging: Several API calls return lot of data and data massaging is required to remove irrelevant fields and condense the data being sent to Copilot. In this case, Function Apps can act as the proxy that makes the API call, receives the response data, removes irrelevant fields and then passes the cleaned up data to Copilot.
Getting Data from in-house tools: Several organizations have custom security and operations tools which do not have a REST API, making a Copilot plugin not possible. In these cases, Function Apps can act as the proxy that takes the required input from Copilot, run the custom tools and send the output back to Copilot.
This article assumes proficiency with the Python programming language and familiarity with Function Apps and the Visual Studio Code (VS Code) development environment.
Integrating Azure Function Apps with VS Code
The Azure Functions core tools allow development of Functions App locally while the Azure Function extension for VS Code lets you easily deploy Function Apps from VS Code itself. A good description of the setup required to build and deploy Python based Functions apps in VS Code is given here. We highly recommend to setup your VS Code for Azure Function App development, before proceeding further.
Once setup of core tools and Azure functions has been completed successfully, authenticate to your Azure account after which an Azure icon is visible in VS Code as highlighted in the circle. Clicking on the icon populates the list of Azure resources in the navigation pane.
Sample Use Case
Prioritizing remediation for the latest CVEs is an overwhelming task and it is common for security teams to prioritize remediation based on the exploitability of a CVE. It is important to know if a CVE is being actively exploited in the wild and if that particular CVE impacts your environment. Copilot calling Function Apps as a plug-in works well for this use case.
The Cybersecurity & Infrastructure Security Agency (CISA) maintains a catalog called the Known Exploited Vulnerabilities (KEV) which contains a list of vulnerabilities that have been exploited in the wild. Since the threat landscape is constantly evolving the KEV catalog is constantly being updated and several organizations use the KEV catalog as one of the sources of Common Vulnerabilities and Exposures (CVEs) that should be scanned in their environment.
The KEV catalog is available in CSV and JSON formats which can be downloaded from here. A sample cutout of the CSV file is shown below, notice the several fields available for each CVE:
Using Function Apps we will add capability in Copilot to query the CISA KEV catalog and extract the following information:
Total number of CVEs present.
Number of CVEs present for a Product.
Number of CVEs present for a Publisher.
List all CVEs for a Product.
The above is only a small subset of information that can be extracted from the KEV catalog and you can build upon the code to extract any additional information. Note that Copilot also has in-built Public Web and GPT skills that can extract some information from the KEV catalog, but extracting more specific information requires help of code which a Function App provides.
Python Code
When building a function app, it is good practice to test the code separately and we recommend encapsulating all the functions you need in a Python Class, which makes porting to a Function App much easier. The following class encapsulates all the functions we need to fulfill the 4 data points we need for our use case, inline comments provide more details on each function.
Filename: cisakey.py
import requests
import ast
import json
class cisakev:
”’
Input: N/A
Output: N/A
Details: Constructor, intended to be private function
”’
def __init__(self):
# Hard code URL to the CISA KEV
self._url = “https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json”
self._jsonDict = “” # Dictionary to store data from the CISA KEV catalog
self.__download__() # Download The file in the constructor
”’
Input: N/A
Output: N/A
Details: Downloads the latest CISA KEV and assign to an internal JSON dictionary
Called from constructor, intended as private function
”’
def __download__(self):
try:
response = requests.get(self._url)
response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
jsonStr = response.content.decode(“utf-8”)
# safe evaluation
jsonStr = jsonStr.replace(‘\/’,”)
self._jsonDict = ast.literal_eval(jsonStr) # Convert to dictionary from JSON string
except requests.RequestException as e:
raise SystemExit(f”Failed to make the request. Error: {e}”) # Will result in HTTP 500
”’
Input: CVE ID
Output: True or False depending if the CVE is present
Details: Does an exact match on the CVE after converting input to upper case
”’
def isCVEPresent(self, cve):
try:
cve = cve.upper()
for vuln in self._jsonDict[“vulnerabilities”]:
if(vuln[“cveID”] == cve): # Do an exact match
return self.getJSONOutput(True)
return self.getJSONOutput(False)
except Exception as e:
raise SystemExit(f”Failed to make the request. Error: {e}”) # Will result in HTTP 500
”’
Input: Vendor Name
Output: Count of the CVEs tagged with the Vendor
Details: Uses a ‘contains in’ match, where the given vendor name string is contained
inside the ‘vendorProject’ field
”’
def countCVEForVendor(self, vendor):
try:
vendor = vendor.upper()
count = 0
for vuln in self._jsonDict[“vulnerabilities”]:
if(vendor in vuln[“vendorProject”].upper()): # contains in match
count += 1
return self.getJSONOutput(count)
except Exception as e:
raise SystemExit(f”Failed to make the request. Error: {e}”) # Will result in HTTP 500
”’
Input: Product Name
Output: Count of the CVEs tagged with the Product
Details: Uses a ‘contains in’ match, where the given product string is contained
inside the ‘product’ field
”’
def countCVEForProduct(self, product):
try:
product = product.upper()
count = 0
for vuln in self._jsonDict[“vulnerabilities”]: # contains in match
if(product in vuln[“product”].upper()):
count += 1
return self.getJSONOutput(count)
except Exception as e:
raise SystemExit(f”Failed to make the request. Error: {e}”) # Will result in HTTP 500
”’
Input: Product Name
Output: list of the CVEs tagged with the Product, returned as List(Array)
Details: Uses a ‘contains in’ match, where the given product string is contained
inside the ‘product’ field
”’
def listCVEForProduct(self, product):
try:
product = product.upper()
cves = []
for vuln in self._jsonDict[“vulnerabilities”]:
if(product in vuln[“product”].upper()):
cves.append(vuln[“cveID”])
return self.getJSONOutput(cves)
except Exception as e:
raise SystemExit(f”Failed to make the request. Error: {e}”) # Will result in HTTP 500
”’
Input: Numeric or Array
Output: Converts the Input to a Json assigned to the variable ‘value’.
Example: If input is 1, Output will be {“value”:1}
Details: N/A
”’
def getJSONOutput(self, value):
sJson = ‘{“value”:’
sJson += json.dumps(value) + “}”
# Any exception will bubble up
return sJson
The constructor of this Class (__init__()) calls __download__() which downloads the JSON formatted KEV catalog and stores it in an internal dictionary variable (_jsonDict) which allows search and data extraction convenient. We use the JSON format since it is easier to handle JSON in Python code, but using the CSV is also ok.
_jsonDict is referenced by the individual functions that take the required input parameters and return the data after searching _jsonDict. Note that the final output is passed via getJSONOutput(), so that a formatted JSON response is returned. While this is not required, it adds consistency as the output format is like what one should expect from a REST API.
The following code is given in a separate file which is used to call the functions in the Class and simulates the Function App. This is good way to insure that correct data is being returned and for more complex functions, using a Pythonic Unit test framework like unittest is better.
Filename: consumer.py
from cisakev import cisakev
def main():
ck = cisakev()
print(“ck.isCVEPresent(‘CVE-2021-27102’)”,ck.isCVEPresent(“CVE-2021-27102”)) # True
print(“ck.countCVEForVendor(‘Linux’)”,ck.countCVEForVendor(“Linux”)) # 14
print(“ck.countCVEForProduct(‘Outlook’)”,ck.countCVEForProduct(“Outlook”)) # 1
print(“ck.listCVEForProduct(‘Android Kernel’)”,ck.listCVEForProduct(“Android Kernel”)) # “CVE-2019-2215”, “CVE-2020-0041”
if __name__ == ‘__main__’:
main()
The output of running consumer.py in VS Code is given below:
With all functions in the cisakev Class working we are ready to move and build the Function App.
Testing Azure Function Apps locally
We need to create a Function App project in VSCode. This can be done by bringing in the Command Palette (F1) and selecting ‘Azure Functions: Create New Project….’. or ‘Azure Functions: Create Function App in Azure …’. The latter will also create the necessary Azure App resource in the Azure subscription while with the former option the Azure App has to be created via the Azure console, which is the option we have selected for this article.
After choosing ‘Azure Functions: Create New Project….’, select the language as Python, model as ‘Model V2’ and among the many trigger options select the HTTP trigger (allowing our code to run on a Copilot HTTP GET request):
Name the function ‘http_trigger_cisakev’:
Select ‘Anonymous’ for level control. Anonymous access allows the request to be received from any address, this is a good option for testing. Before we host the function in Azure, we will change it to ‘Function’ via the code, requiring all requests to contain the API Key:
Once these steps are complete an Azure Function basic project structure is created, as shown below:
function_app.py is where the main code and entry functions resides, and a basic templated code is auto-generated for us:
Note that you may have to install the ‘azure.functions’ module (using: pip install azure.functions) before running the code.
Next, copy the cisakev.py file previously built in the standalone project to this project. The project structure should look like this:
In our API call, we will add two query parameters ‘op’ and ‘data’. The value of ‘op’ will determine which of our 4 functions is called, and the ‘data’ is the parameter to that function. Hence a sample URL to run the Azure function will be:
https://<URL>?op='<code>’&data='<data>’
We will use Python’s match-case syntax to branch over the op value and call the respective function. The updated code for function_app.py is given, note that we instantiate the cisakev class in the same way we did in the consumer.py code:
Filename: function_app.py
import azure.functions as func
import logging
from cisakev import cisakev
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
# URL /?op='<code>’&data='<data>’
@app.route(route=”get_cisa_kev”)
def http_trigger_cisakev(req: func.HttpRequest) -> func.HttpResponse:
logging.info(‘Python HTTP trigger function processed a request.’)
op = req.params.get(‘op’)
data = req.params.get(‘data’)
# Instantiate the cisakev object
ck = cisakev()
response = “”
match op: # Call the function based on the op and pass in data
case “cvePresent”:
# code for pattern 1
response = ck.isCVEPresent(data)
case “countCVEForVendor”:
# code for pattern 2
response = ck.countCVEForVendor(data)
case “countCVEForProduct”:
# code for pattern N
response = ck.countCVEForProduct(data)
case “listCVEForProduct”:
# default code block
response = ck.listCVEForProduct(data)
case _:
response = “Input data is missing”
return func.HttpResponse(response,status_code=200)
With Azure core tools you can run the function app locally (press F5) and for HTTP triggers a local port is made accessible. Note that if code needs storage the ‘Azurite’ emulator needs to be running as explained here. Once the Azure function is loaded and ready, the local URL will made available in the VS Code Terminal pane:
Using a REST API client (Boomerang, Postman etc.) we can make calls to the local Azure App and check if all required options are working correctly. The following call checks if a CVE is present in the latest KEV catalog with the JSON response shown in the right.
The following call gets the list of CVEs for a specific product:
With the Function app working well locally, it’s time to deploy to Azure.
Deploying the Azure Function App to Azure
We will need a Function App resource that will host and execute the code. The resource can be created from the Azure console or from VS Code using the ‘Azure Functions: Create Function App in Azure …’ command as detailed here. For our example, we created the Function App ‘FA-CISA-KEY’ in Azure and can see it in our resources (resource name: FA-CISA-APP):
Once the resource is available, use command template (F1) in VS Code and select ‘Azure Functions: Deploy to Function App…’. However, before proceeding further let’s discuss an important file that contains list of modules our code needs.
requirements.txt
In our cisakev class we are using the requests module which is not part of standard Python distribution. While we can use pip to install any PyPI package locally we cannot execute pip in Function Apps, so all non-standard modules that are required must be defined in the requirements.txt file, which exists in the main folder of our projects. Make sure not to remove any existing modules present in the requirements.txt file, and after requests is added the contents of this file are:
Filename: requirements.txt
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues
azure-functions
requests
With the requirements.txt updated, run ‘Azure Functions: Deploy to Function App…’ command which guides through the steps. Since the Function App is already hosted, you will get to select the function app resource in Azure where the newly developed Function App project must be deployed.
The deployment of the function app will erase any previous deployments, VS Code shows a warning as to that respect:
After verifying you are updating the right Function App, select ‘Deploy’ to deploy the Function App to Azure.
App/API Keys
In the code we tested locally, the authentication level was set to Anonymous which allows anyone to call the Function app, which is a really bad idea in a production environment. Hence, we will add some access control using App keys which are Function App’s equivalent to API keys.
Function App access can be set to Anonymous, Admin or Function level, while access keys can be Function, Host, Master and System. More details on the access and key types is given here. For our App we will set Function level access and use the default key.
The access level is set up in the code. In the function_app.py files change the existing Anonymous access from:
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
to:
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
Deploy the app again with this change so that the Function App running in Azure will only allow access if the key is present.
The key is accessed in the Azure console via the App Keys link as shown below, and we will use the default key:
Testing the Function App
It’s time to test our Function app via a REST client as we did before. The URL of the hosted Function App is visible in the Overview pane in Azure console:
With this URL let’s head to Boomerang and make a call to see if a CVE is present. We only change the URL and make the call, but we get a HTTP 401 (Access Denied). Can you guess why?
Since we have added access control in the code we need to specify the access key in the headers. The header string for Function App access key is ‘x-functions-key’ and the value is copy/pasted from the Azure Console as shown below:
With the correct value for ‘x-functions-key’ specified, this call is successful. Let’s make another call to fetch the list of CVEs and confirm it returns the correct data.
Now it’s time to have Copilot make calls to our Function app via an API plugin.
Copilot API Plugin
Copilot’s API plugins needs two JSON or YAML files, and a beginner’s walkthrough for building API plugins is available here. The main plugin YAML file is given below:
Filename: API_Plugin_AzureFunction_CISA_KEV.yaml
Descriptor:
Name: Fetch details from the CISA Known Exploited Vulnerabilities (KEV) list
DisplayName: Fetch details from the CISA Known Exploited Vulnerabilities (KEV) list
Description: The skills in this plugin will get different information on CVEs present from the latest CISA KEV list
DescriptionForModel: The skills in this plugin will get information like CVE count by product or publisher as well as list of CVEs. This information is sourced from the latest CISA Fetch details from the CISA Known Exploited Vulnerabilities (KEV) list.
SupportedAuthTypes:
– ApiKey
Authorization:
Type: APIKey
Key: x-functions-key
Location: Header
AuthScheme: ”
SkillGroups:
– Format: API
Settings:
OpenApiSpecUrl: http://<URL>/API_Plugin_AzureFunction_CISA_KEV_OAPI.yaml
Note the ‘Key’ value given in the YAML side, this is inserted into the header by Copilot and should match exactly as the Function App expects it. The OpenAPI specification file for our plugin is given below:
Filename: API_Plugin_AzureFunction_CISA_KEV_OAPI.yaml
openapi: 3.0.0
info:
title: Fetch details from the CISA Known Exploited Vulnerabilities (KEV) list
description: The skills in this plugin will get different information on CVEs present from the latest CISA KEV list
version: “v1”
# This should point to the Azure Function URL
servers:
– url: https://fa-cisa-kev.azurewebsites.net/api/
paths:
/get_cisa_kev?op=cvePresent:
get:
operationId: cvePresent
description: Determines if a given CVE is present in the latest CISA KEV list
parameters:
– in: query
name: data
schema:
type: string
required: true
description: The CVE whose presence has to be determined.
responses:
“200”:
description: OK
content:
application/json:
/get_cisa_kev?op=countCVEForProduct:
get:
operationId: countCVEForProduct
description: Counts the number of CVEs based on the name of a product, in the latest CISA KEV list
parameters:
– in: query
name: data
schema:
type: string
required: true
description: The product name for whom the CVE count has to be determined
responses:
“200”:
description: OK
content:
application/json:
/get_cisa_kev?op=listCVEForProduct:
get:
operationId: listCVEForProduct
description: Lists all the CVEs based on the name of a product, in the latest CISA KEV list.
parameters:
– in: query
name: data
schema:
type: string
required: true
description: The product name for whom the CVE list has to be determined
responses:
“200”:
description: OK
content:
application/json:
/get_cisa_kev?op=countCVEForVendor:
get:
operationId: countCVEForVendor
description: Counts the number of CVEs based on the name of a vendor, in the latest CISA KEV list
parameters:
– in: query
name: data
schema:
type: string
required: true
description: The vendor name for whom the CVE count has to be determined
responses:
“200”:
description: OK
content:
application/json:
Each of the supported operations is in its own URL (matching to an individual skill) that is given as a fixed query parameter, but the data is taken as an Input. Copilot will insert the data from the user’s prompt, and Copilot’s orchestrator will select the skill (and the plugin) based on the Description and DescriptionForModel defined in both the YAML files.
After uploading the OpenAPI specification file to an Internet accessible URL, we are ready to upload the plugin to Copilot.
In the Copilot console, select the Plugin sources in command bar:
Select Add Plugin:
Select the main YAML file for the plugin and press ‘Add’:
You will receive a confirmation when the Plugin has been added:
After the plugin is added, you will be asked to enter an API key. Copy/Paste the API/App key from the Azure console as discussed earlier and press ‘Save’.
With the key entered ensure the custom plugins and this specific plugin are both enabled:
We are now ready to test the plugin. Let us first invoke one of the skills directly to confirm Copilot can communicate with our Function App. For directly invoking the skill, click on the ‘Prompts’:
And select “See all System capabilities”:
Scroll down till the “Fetch Details From…” plugin name is displayed, and within it all four skills will be visible.
Select ‘cvePresent’, and enter the same CVE we had been using in our previous tests, click the Submit icon:
Copilot will select the CISA KEV plugin and the skill, make a call to the Azure Function App and format the response JSON to natural language:
This confirms Copilot can talk to the Function app and we are ready to issue a prompt that leverages the CISA KEV plugin. Let’s start with checking the presence of another CVE:
Prompt: “Can you tell me if CVE-2020-12812 is present in the CISA KEV catalog?”
Prompt: “Show me the number of CVEs present in the CISA KEV catalog where the vendor is Linux”:
Prompt: “Show me the number of CVEs present in the CISA KEV catalog where the product is Outlook”:
Prompt: “Show me all the CVEs present in the CISA KEV catalog where the product is Android Kernel”:
After getting the CVE list we can query Defender or other sources to check if those CVEs exist in the environment. This highlights how Copilot can pull data from multiple tools and have custom plugins like the one we built, enrich the reasoning from those tools.
Prompt: “From Defender, how many of the above CVEs are present in my environment?”
Copilot confirms via data from Defender Vulnerability Management that none of the specific CVEs we queried from CISA KEV catalog exist in our environment.
In our Function App, for each request the CISA KEV catalog is downloaded. While this ensures we are looking at the latest CISA KEV catalog, if requests from Copilot are coming in frequently the same CISA KEV catalog file is downloaded multiple times. Even though serverless computing like Function App are meant to be stateless, there are ways the downloaded file can be persisted in storage or in memory (via Durable Functions) improving the efficiency of the Function App. However, this is outside the purview of this article and for more information please look in the previous links.
In this article, we have shown how to build a Function App and have Copilot use it as an API plugin. This gives Copilot the indirect capability to execute custom code and use it’s output to reason on, opening a wide range of possibilities and use cases.
Would be good to start by explaining why this cannot be achieved any other way i.e. in what circumstances is using Function Apps the best or the only way to achieve a certain use cases.
Microsoft Tech Community – Latest Blogs –Read More