eventsmicrosoft-teamswebhookssnyk

How can Snyk push notifications to MS Teams?


Snyk has native integrations to Slack, ServiceNow, Jira and others But no integration to MS Teams How can a team get Snyk notifications pushed to MS Teams?


Solution

  • You can make use of Snyk's outbound webhooks

    Register any endpoint of yours that can consume JSON payload; sample payload is at https://snyk.docs.apiary.io/#introduction/consuming-webhooks

    There is a sample Azure Function specific for MS Teams: https://github.com/harrykimpel/snyk-webhook-subscription/blob/main/azure-function-microsoft-teams.cs and additional info in this repo's README and blog post

    Another script that does the same but for Azure Boards is this here, which is Node.js based: https://github.com/mathiasconradt/snyk-azure-boards-webhook

    Below sample Azure function shows to to consume the incoming JSON payload from Snyk, then forward it further to another system, such as MS Teams:

    #r "Newtonsoft.Json"
    
    using System.Net;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Primitives;
    using Newtonsoft.Json;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Text;
    using Newtonsoft.Json.Linq;
    
    public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");
    
        string name = req.Query["name"];
    
        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        //log.LogInformation("data: " + requestBody);
    
        string count = data.newIssues.Count.ToString();
        log.LogInformation("data.newIssues.Count: " + count);
        string responseMessage = "No new issues found. Nothing to process!";
    
        if (data.newIssues.Count > 0)
        {
            log.LogInformation("New issues found!");
    
            name = name ?? data?.name;
            string projectName = data.project.name;
            string browseUrl = data.project.browseUrl;
            int x = 0;
    
            for (int i = 0; i < data.newIssues.Count; i++)
            {
                // send data to Azure Boards
                StringBuilder sb = new StringBuilder();
    
                //var item = (JObject)data.newIssues[i];
                //do something with item
                string id = data.newIssues[i].id.ToString();
                //log.LogInformation("data.newIssues[i].id:" + id);
                string descr = data.newIssues[i].issueData.description.ToString();
                //log.LogInformation("data.newIssues[i].issueData.description:" + descr);
    
                sb.Append("{");
                sb.Append("  \"@context\": \"https://schema.org/extensions\",");
                sb.Append("  \"@type\": \"MessageCard\",");
                sb.Append("  \"themeColor\": \"0072C6\",");
                sb.Append("  \"title\": \"" + projectName + "\",");
                sb.Append("  \"text\": \"" + id + "<br><br>" + descr + "\",");
                sb.Append("  \"potentialAction\": [ ");
                sb.Append("    {");
                sb.Append("      \"@type\": \"OpenUri\",");
                sb.Append("      \"name\": \"Project Details\",");
                sb.Append("      \"targets\": [");
                sb.Append("        { \"os\": \"default\", \"uri\": \"" + browseUrl + "\" }");
                sb.Append("      ]");
                sb.Append("    }");
                sb.Append("  ]");
                sb.Append("}");
    
                string payload = sb.ToString();
                //log.LogInformation("content: " + payload);
    
                var content = new StringContent(payload);
    
                content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    
                var MS_TEAMS_WEBHOOK = Environment.GetEnvironmentVariable("MS_TEAMS_WEBHOOK");
                var url = MS_TEAMS_WEBHOOK;
                using var client = new HttpClient();
                var response = await client.PostAsync(url, content);
    
                string result = response.Content.ReadAsStringAsync().Result;
                //log.LogInformation("response.StatusCode: " + response.StatusCode);
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    x++;
                }
                //log.LogInformation("result: " + result);
            }
    
            // write output as summary
            string output = "Successfully processed " + x + " issues.";
            log.LogInformation(output);
            responseMessage = output;
        }
    
        return new OkObjectResult(responseMessage);
    }
    

    Another example based on Node.js but for Azure Boards. If you prefer Node.js, simply replace the outbound logic with MS Teams API.

    // index.js
    const express = require('express');
    const bodyParser = require('body-parser');
    const axios = require('axios');
    const crypto = require('crypto');
    const PORT = process.env.PORT || 5000;
    const ISSUE_TEMPLATE = [
      {
        "op": "add",
        "path": "/fields/System.Title",
        "from": null,
        "value": null
      },
      {
        "op": "add",
        "path": "/fields/System.Description",
        "from": null,
        "value": null
      },
    {
        "op": "add",
        "path": "/fields/System.WorkItemType",
        "from": null,
        "value": "Issue"
      }
    ];
    
    const app = express()
      .use(bodyParser.urlencoded({ extended: true }))
      .use(bodyParser.json())
      .use(bodyParser.raw())
      .get('/snyk', (req, res) => {  
        console.log('process.env.AZURE_DEVOPS_USER ' + process.env.AZURE_DEVOPS_USER);
        res.sendStatus(200);
      })
      .post('/snyk', (req, res) => {
          console.log('Got body:', req.body);
    
          var verified = this.verifySignature(req);
          console.log('verified: ', verified);
    
          if (verified && req.body.newIssues) {
            var newIssues = req.body.newIssues;
            newIssues.forEach(issue => {        
              var it = JSON.parse(JSON.stringify(ISSUE_TEMPLATE));
              it[0].value = issue.issueData.title + " [" + issue.issueData.id + "]";
              it[1].value = issue.issueData.description;
              this.createIssuePostman(it);
            });
          }
          res.sendStatus(200);
      })
      .listen(PORT, () => console.log(`Listening on ${ PORT }`));
    
    module.exports.verifySignature = function (request) {
      const hmac = crypto.createHmac( 'sha256' , process.env.SNYK_WEBHOOKS_SECRET);
      const buffer = JSON .stringify(request.body);
      hmac.update(buffer, 'utf8' );
      const signature = `sha256=${hmac.digest('hex')}` ;
      return signature === request.headers[ 'x-hub-signature' ];
    }
    
    module.exports.createIssuePostman = function(issue) {
      console.log('createIssuePostman: ' + issue[0].value);
      var auth = 'Basic ' + Buffer.from(process.env.AZURE_DEVOPS_USER + ':' + process.env.AZURE_DEVOPS_ACCESS_TOKEN).toString('base64');
      var config = {
        method: 'post',
        url: 'https://dev.azure.com/' + process.env.AZURE_DEVOPS_ORGANIZATION + '/' + process.env.AZURE_DEVOPS_PROJECT + '/_apis/wit/workitems/$Issue?validateOnly=false&api-version=6.0',
        headers: {       
          'Authorization': auth,
          'Content-Type': 'application/json-patch+json'
        },
        data : issue
      };
      axios(config)
      .then(function (response) {
        console.log(JSON.stringify(response.data));
      })
      .catch(function (error) {
        console.log(error);
      });
    }