How to build a Copilot for Security API Plugin – Part 1
Published Apr 09 2024 09:50 AM 2,287 Views
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 from multiple sources using a unique plugin mechanism. The plugin mechanism allows Copilot to not only pull data from Microsoft products but also from third-parties.

 

Plugins in Copilot can be of three types:

 

  • GPT
  • KQL
  • API

More details and the differences between these plugins are given here. At a high level, GPT plugins allow you to call specific skills inherent in the LLM, while KQL plugins execute Kusto Query Language (KQL) queries to pull data from Microsoft Defender 365 products like Sentinel and Defender.  API plugins allow Copilot to make REST API calls, to both Microsoft and non-Microsoft services and hence are the most powerful and relatively complex of the plugins. In this article, we how to build a simple API plugin that utilizes a GET request with no parameters. In a follow-up article we will show how to build API plugins for more complex GET and POST calls which involve the passing of parameters. Since JSON (JavaScript Object Notation) is the most popular format used in REST APIs that is the output format we will be using for our plugins. This article assumes you have basic familiarity with the Python programming language and the YAML file format. The code for the Webservice and the YAML files is available under Workshops in the Copilot GitHub page here.

 

Note that this article discusses building new plugins from scratch. If you want to know more about the existing 3-P plugins that are currently available on Copilot please click here.

 

A Flask Webservice to service REST APIs 

While we can use any standard application that exposes a REST API, it will be easier and clearer from the server-side if we build a simple web service of our own and have our plugin call it.  You can also use some of the test http servers like httpbin.org or jsontest.com. If you want to make REST call to your own web application or test http servers, feel free to skip this part and go to Section 2.  We will build this web service in Python using the Flask web framework. The code of this webservice is given below:

 

 

 

 

from flask import Flask, request, jsonify, send_file

#-------- Define classes that will be serialized
class ReflectorSimple:
    def __init__(self,data,ip,useragent):
        self.object = "Reflector Simple"
        self.userdata = data
        self.sourceip = ip
        self.useragent=useragent

    def getDict(self):
        return self.__dict__

class ReflectorJson:
    def __init__(self,data,json,ip,useragent):
        self.object = "Reflector Json"
        self.userdata = data
        self.value1 = json["value1"]
        self.value2 = json["value2"]
        self.sourceip = ip
        self.useragent=useragent

    def getDict(self):
        return self.__dict__
#------------------------------------
app = Flask(__name__)

# Show the REST APIs supported and format as a basic HTML.
# When the main page is visited via browswer this page will be shown
@app.route('/')

def main():
    text = "<h3>Welcome to Copilot for Security test REST APIs. Please use the following REST API routes to get specific JSON results.</h1>"
    text += "<p><b> - GET /simple/&lt;data&gt; - Will return a JSON contains &lt;data&gt and few other fields </b>"
    return text
  
# ---------------- GET methods
@app.route('/simple/<data>', methods=['GET'])
def get_simple(data):
    obj = ReflectorSimple(data,getIpAddr(request),request.user_agent.string)
    response =  jsonify(obj.getDict())
    return response

#---- Use this URL to return YAML files if you do not want to the host the files in FTP/Azure/Blob

#---- NOTE: All files should be placed in the ./Files folder
@app.route('/file/<fileName>')
def get_file(fileName):
    try:
        filePath = 'Files\\'+fileName
        return send_file(filePath,download_name=fileName,as_attachment=True)
    except Exception as e:
        return str(e)

# Private function to return IP Address
def getIpAddr(request):
    if request.headers.getlist("X-Forwarded-For"):
        ip_addr = request.headers.getlist("X-Forwarded-For")[0]
    else:
        ip_addr = request.remote_addr
    return ip_addr

if __name__ == '__main__':
    # Specify Network interface to bind to and the port to run
    app.run(host='0.0.0.0', port=5000)

 

 

 

 

 

This webservice exposes two GET routes one for ‘/Simple/<data>’ and another optional route ‘/file/<fileName>’. The purpose of having the optional ‘/file/<fileName>’ route will be given later. Note that if someone navigates to the main site (‘/’ route) they will get a HTML message as shown in the main(). This route is not meant to be used by Copilot, so we will not focus on this. The ‘/Simple/<data>’ route calls the function get_simple(data) which creates an object of type ReflectionSimple and using the value that is passed to the GET call. ReflectionSimple assigns some value to internal properties and returns a dictionary of properties in the ReflectionSimple.getDict() function. The dictionary is converted to a JSON by the jsonify() function and returned as a HTTP response. Hence the get_simple(), function is returning the JSON serialization of the ReflectionSimple object with the serialization including the value passed to it, hence this class reflects the value passed along with some other data.

 

To better understand the output let us run this site manually in a local machine. We have bind the webservice to all network interfaces and will run it on port 5000. When you see the following output in your Python console you know the webserver is up and ready to service requests.

 

cbelwal_0-1712354587228.png

 

Open your web browser and navigate to the http://127.0.0.1:5000.  This will call the main() function which will render the basic HTML stored in the text variable:

 

cbelwal_1-1712354587232.png

 

When we navigate to http://127.0.0.1:5000/simple/TestParam a GET request will be made to the path ‘/simple/TestParam’ which will call the function get_simple(data) returning a JSON with the value of variable data contained within it. Since we passed the value ‘TestParams’ the JSON contains the value along with 3 other parameters (object: contains a hard-coded string, sourceip: IP of the GET request originator, useragent: value of user-agent header if available). Modify the ReflectionSimple class to return other values you want. 

 

cbelwal_2-1712354587236.png

 

It also helps to see the request that is being received on the server side. The Flask website prints each request it receives along with the full URL, and we can see both the above requests we made in the browser. Note the 404 in the 2nd request, the URL has to be case sensitive in Flask and when we gave the path ‘Simple’ rather than ‘simple’ it results in a not found message.

 

cbelwal_3-1712354587236.png

 

Once it's proven the Flask web application is running locally, you will now need to run the above website in a host that is reachable from the Internet since Copilot for Security is a Cloud-based application. The best option is to use an Azure App Service and instructions to host and run a Python web application in Azure App Service are given here. Azure App Service also builds a TLS layer on your website (allowing you to use https) even though our webservice is non-TLS. The other option is to run the webserver from an Azure-hosted VM, but you will not have the benefit of a TLS layer. While Copilot for Security can call both http and https websites, for any production application TLS is a must.   

 

Defining the YAML files for out plug-ins

We have a test REST API web application running that is returning a JSON output via a GET call, so it’s time to build our API plugins. For API plugins we need two YAML files, one for the base definition and one for an OpenAPI specification. The base definition contains the Internet-accessible URL of the OpenAPI YAML and gets downloaded by Copilot at the time the base definition file gets uploaded.

 

The base YAML for a plugin that will access our Flask Webservice given in the previous section is given below. Note that the details on how to make the actual GET call are given in the OpenAPI YAML file whose URL is given in the base file. More details on the properties of the base YAML file are available here.

 

 

 

 

 

#Filename: API_Plugin_Reflection_GET_Simple.yaml
Descriptor:
  Name: McCulloch-Pitts Reflection API plug-in using GET
  DisplayName: McCulloch-Pitts Reflection API plug-in using GET
  Description: Skills for getting a GET REST API call reflection
  DescriptionForModel: A set of plugins that reflect the data that was sent to the plugin

SkillGroups:
  - Format: API
    Settings:
      # Replace this with your own URL where the OpenAPI spec file is located.
      OpenApiSpecUrl:  http://X.X.X.X/file/API_Plugin_Reflection_OAPI_GET_Simple.yaml

 

 

 

 

 

In YAML any line prefixed with ‘#’ is treated as a comment, they do not impact the operation or impact prompt behavior in any way. The Name, DisplayName and Description properties contain the words that define what the plugin does and are used in the user interface of Copilot. These properties are not used when doing similarity search based on the user’s prompt, however they should be descriptive on what they are doing. The DescriptionForModel is used by the Copilot planner/orchestrator and plays a critical role in making sure your plugin is selected based on prompts, hence it should be as detailed as possible.  Note that if DescriptionForModel  is not given then the Description property is used, but it is a good idea to always give an explicit value for DescriptionForModel. Once the plugin is selected, the correct skills are chosen based on other properties defined in the OpenAPI specification which we discuss shortly.

 

 Our plugin is going to make a GET call with reflected data so we name it accordingly and prefix it with a non-standard word ‘McCulloch-Pitts’. This name honors Warren McCulloch and Walter Pitts, inventors of the Perceptron the very first formal artificial neural network. The OpenApiSpecUrl property is very important and it tells Copilot for Security the download location of the OpenAPI specification document which is where most of the ‘meat’ of the API plugin is defined,  note that this URL should be accessible via the Internet. You can have multiple SkillGroups defined in a single YAML file and each one of them can refer to different OpenAPI specification YAML documents.

 

OpenAPI is an industry-standard specification to describe and produce web services and details on the latest definition are available here. The OpenAPI definition for our plugin in YAML format is given below:

 

 

 

 

 

# Filename: API_Plugin_Reflection_GET_OAPI_Simple.yaml
# The basic template of this file can be generated using Bing Copilot using the following prompt:
#   Write an OpenAPI specification document for an API that receives a variable called input in the path via a GET
#   request to /simple/{input} that outputs a JSON with the following fields: object, userdata,sourceip, useragent

openapi: 3.0.0

info:
    title: REST API Reflection using GET
    description: Skills for getting reflection input for a GET REST API call
    version: "v1"

servers:
#    Replace this with out own URL
    - url: http://X.X.X.X

paths:
    /simple/{input}:
        get:
            operationId: ReflectionData
            summary: A Reflection Data Plugin
            parameters:
                - in: path
                  name: input
                  schema:
                      type: string
                  required: true
                  description: Parameter Input
            responses:
                "200":
                    description: OK
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ReflectionDataPluginResponse"

# This is referred to by $ref
components:
    schemas:
        ReflectionDataPluginResponse:
            type: object
            properties:
                object:
                    type: string
                    description: main data
                sourceip:
                    type: string
                    description: The Source IP
                useragent:
                    type: string
                    description: The User Agent
                value:
                    type: string
                    description: Fixed value

 

 

 

 

 

The server property defines the Internet-accessible webservice we need to reach while the child properties of path define the individual REST APIs (using GET, POST etc.) that are available in each path. The content sub-property helps us to define the structure of the JSON that will be received and the schema can be defined in its own property (components). The operationId and Summary for each path are important and should be defined carefully. They are used in similarity search and allow Copilot to select the specific skill (after the plugin has been selected based on the DescriptionForModel property discussed earlier) based on what the user prompts.

Due to their expressive power, OpenAPI specifications can be complex and understanding them from scratch can be a daunting task. Generative AI steps in to help in this area allowing you to write the base OpenAPI specification for your query using Copilot for Bing. The prompt to enter in Copilot for Bing is shown in the comments in the above YAML and it’s output is shown below.

 

cbelwal_4-1712354587242.png

 

While Copilot for Bing may not give the exact OpenAPI specification for every REST API, it does more than half the work significantly speeding up the process of writing OpenAPI specification documents.

 

With the OpenAPI specification ready, please upload the YAML file in some Internet-accessible location like an Azure Blob storage, and make sure the correct URL is defined for it on the base YAML. If you are using the Flask webservice discussed in Section 1 you have another choice to place the file. Recall we had a function get_file(fileName). The OpenAPI specification file can be placed inside the Flask webservice (in the ./Files) folder, and a GET call to the http://<URL>/files/API_Plugin_Reflection_OAPI_GET_Simple.yaml will allow Copilot to download it.

Now it’s time to load our plug-in in Copilot for Security and run it.

 

Loading the plug-ins in Copilot for Security and naming it

Copilot for Security makes it very easy to import custom plugins. In the prompt window click on the sources icon, as highlighted by the red circle below:

 

cbelwal_5-1712354587243.png

 

This will open  the plugin window, scroll down to the custom section and click ‘Upload Plugin’:

 

cbelwal_0-1712756795436.png

 

 

In the option to select the file type, select ‘Copilot for Security plugin’:

 

cbelwal_7-1712354587251.png

 

In the file browser, select the file ‘API_Plugin_Reflection_GET_Simple.yaml’ and upload it. Once file is uploaded it’s name will show so click on ‘Add’.

 

cbelwal_8-1712354587255.png

 

If the plugin upload is successful, you will get a confirmation message as shown below, else you will see an error message with code. If you encounter an error, make sure your YAML format is correct and the OpenAPI specification YAML file is accessible. More troubleshooting steps are here.

 

cbelwal_9-1712354587255.png

 

The plugin will show in the list of plugins as shown below:

 

cbelwal_10-1712354587257.png

 

At this point the work is done and now we are ready to use our plug-in. Our first prompt will be:

Get reflection data for TestParam

 

This is an explicit prompt to call reflection data and easily matches to the operationId and Summary of the GET call we defined in the OpenAPI YAML file. Copilot selects the plugin, calls the REST API associated and frames the JSON response in a nice paragraph. Note that the ‘TestParam’ is passed in the GET call and is returned by our Flask Webservice.

 

cbelwal_11-1712354587261.png

 

Let’s try a different prompt:

Tell me more about the reflection data for TestParam_123

 

Again, the correct plugin is selected and Copilot makes the GET call and shows the data in a generated sentence.

 

cbelwal_12-1712354587265.png

 

If you ask the following prompt in a new session it shows the JSON data in a structured format:

Can you show me ReflectionData for TestParam_789

 

cbelwal_13-1712354587268.png

 

Since Copilot is making REST calls to our Flask Webservice we can see each of the calls. The very first call is request for the OpenAPI spec file (in our example we used option of the Flask website to service the YAML), and Copilot downloads the file at the time we upload the base YAML file. Rest of the calls are with different params which Copilot makes based on the user’s prompt. Note that Copilot automatically extracts the relevant information from the prompt to pass as parameter to the REST GET call. We should point out that the last few calls are with the same parameter (TestParam_789) which we made but have not shared the screen capture.

 

cbelwal_14-1712354587275.png

 

In this article, we have seen in detail how to build a simple API plugin using REST APIs. We are only passing a single parameter, in a follow-up article we will look at how to pass multiple parameters in GET and also make POST calls with a JSON payload.

 

Till then, prompt-on!  

Co-Authors
Version history
Last update:
‎Apr 12 2024 11:16 AM
Updated by: