dependency-track icon indicating copy to clipboard operation
dependency-track copied to clipboard

MS Teams is retiring webhooks - Power Automate workflows is the new black

Open black-snow opened this issue 1 year ago • 21 comments

Current Behavior

M$ is retiring the classic webhooks and you'll have to use Power Automate workflows instead.

The linked page doesn't have too many details about the transition so I thought it'd be a swap & replace. But apparently that's not true. With the existing template my workflow errs with:

... "Send_each_adaptive_card": The execution of template action 'Send_each_adaptive_card' failed: the result of the evaluation of 'foreach' expression '@triggerOutputs()?['body']?['attachments']' is of type 'Null'. The result must be a valid array.

Proposed Behavior

I guess it'd be a good idea to have a new template for notifications for workflows that works out of the box and has an adequate name.

Checklist

black-snow avatar Jul 09 '24 07:07 black-snow

Quick headsup because I tried it this morning as well with dependency track.

Its not enough to wrap the existing message into the new payload structure that comes with the default template for sending messages to a teams channel (see)

They also only accept adaptive cards there, no more MessageCards or even the simpler text version of the old webhook. If you however put the example from the link above into a custom template in DTrack it works. So I guess we need a new adaptive card template.

otbe avatar Jul 09 '24 10:07 otbe

Thanks for looking into this @otbe. Did you just reuse org.dependencytrack.notification.publisher.MsTeamsPublisher for your custom template?

black-snow avatar Jul 10 '24 14:07 black-snow

Thanks for looking into this @otbe. Did you just reuse org.dependencytrack.notification.publisher.MsTeamsPublisher for your custom template?

Yes exactly.

otbe avatar Jul 10 '24 16:07 otbe

I'll be giving this a shot:

{
       "type":"message",
       "attachments":[
          {
             "contentType":"application/vnd.microsoft.card.adaptive",
             "contentUrl":"{{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy='json') }}",
             "content":{
                "$schema":"http://adaptivecards.io/schemas/adaptive-card.json",
                "type":"AdaptiveCard",
                "version":"1.2",
                "body":[
                    {
                    "type": "TextBlock",
{% if notification.group == "POLICY_VIOLATION" %}
                    "text": "{{ subject.policyViolation.policyCondition.subject | escape(strategy="json") }} {{ subject.component.toString | escape(strategy="json") }} - {{ notification.content | escape(strategy="json") }}"
{% elseif notification.group == "BOM_PROCESSING_FAILED" %}
                    "text": "BOM processing failed for {{ subject.project.toString | escape(strategy="json") }} - {{ notification.content | escape(strategy="json") }}"
{% else  %}
                    "text": "{{ notification.content | escape(strategy="json") }}"
{% endif %}
                    }
                ]
             }
          }
       ]
    }

Should be easy to add a new template then. Perhaps I'll do it today or tomorrow.

black-snow avatar Jul 11 '24 10:07 black-snow

Hi, i'm suffering from the same problem. Unfortunately i'm not too deep into this subjects and i have to admit, that i have not understood everything from your post. I managed to create a workflow 'post to channel...'. When i post your templete to the workflow url, a message is displayed in the teams channel, but it's empty. No text at all. May i kindly ask, to have a look? Is there something missing in my payload?

  4 def send_teams_message(flow_url, message):
  5     headers = {
  6         "Content-Type": "application/json"
  7     }
  8
  9     proxyip = "blinded"
 10     proxyport = "3128"
 11     proxies= {"https": f"http://{proxyip}:{proxyport}"}
 12
 13     #payload = {
 14     #    "message": message
 15     #}
 16     payload = {
 17        "title": "Ueberschrift",
 18        "type":"AdaptiveCard",
 19        "attachments":[
 20           {
 21              "contentType":"application/vnd.microsoft.card.adaptive",
 22              "contentUrl":None,
 23              "content":{
 24                 "$schema":"http://adaptivecards.io/schemas/adaptive-card.json",
 25                 "type":"AdaptiveCard",
 26                 "version":"1.2",
 27                 "body":[
 28                     {
 29                     "type": "TextBlock",
 30                     "size": "Medium",
 31                     "weight": "Bolder",
 32                     "text": "Bla Bla"
 33                     }
 34                 ]
 35              }
 36           }
 37        ]
 38     }
 39
 40
 41     response = requests.post(flow_url, headers=headers, json=payload, proxies=proxies)
 42
 43     if response.status_code == 202:
 44         print("Message sent successfully")
 45     else:
 46         print(f"Failed to send message. Status code: {response.status_code}")
 47         print(f"Response: {response.text}")

hajohoetger avatar Jul 11 '24 14:07 hajohoetger

grafik Works - so all there is to do (probs) is deciding what to put where to have the information you need and to make it look pretty.

@hajohoetger didn't go through your code but it looks different from the template "draft" I posted above. Give it a try.

P.S.: You can use triple backticks to get a code block or > for quotes - makes it way easier to read.

black-snow avatar Jul 12 '24 13:07 black-snow

I got this far, it works for new vulnerabilities.

{ "attachments": [ { "contentType": "object", "content": { "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.2", "body": [ { "type": "TextBlock", "size": "Medium", "weight": "Bolder", "text": "{{ notification.title | escape(strategy="json") }}" }, { "type": "TextBlock", "text": "Dependency-Track", "weight": "Bolder", "spacing": "Medium" }, { "type": "TextBlock", "text": "{{ timestamp }}", "spacing": "None" }, { "type": "Image", "url": "https://raw.githubusercontent.com/DependencyTrack/branding/master/dt-logo-symbol-blue-background.png", "size": "Small", "spacing": "Medium" }, { "type": "FactSet", "facts": [ { "title": "VulnID", "value":"{{ subject.vulnerability.vulnId | escape(strategy="json") }}" }, { "title": "Severity", "value": "{{ subject.vulnerability.severity | escape(strategy="json") }}" }, { "title": "Source", "value": "{{ subject.vulnerability.source | escape(strategy="json") }}" }, { "title": "Component", "value": "{{ subject.component.toString | escape(strategy="json") }}" } ] }, { "type": "TextBlock", "text": "{{ notification.content | escape(strategy="json") }}", "wrap": true } ] } } ] }

image

Use the teams workflow as below:

image

Wes-Love avatar Jul 26 '24 07:07 Wes-Love

Hi, sorry to bother you again, but i'm trying for hours now without success. I did manage to display my card in teams, but it is cut, though i have defined "targetWidth": "Wide". I tried that in all parts but that does not change the layout. I just want the card to fit the width of the screen. Often the expert sees the problem at first sight. Then it would be very nice to hint me on that...

    proxies= {
            "https": f"http://{proxyip}:{proxyport}"
            }
    payload = { 
            "type":"AdaptiveCard",
            "attachments":[
                {
                    "contentType":"application/vnd.microsoft.card.adaptive",
                    "contentUrl":None,
                    "content":{
                        "$schema":"http://adaptivecards.io/schemas/adaptive-card.json",
                        "type":"AdaptiveCard",
                        "version":"1.2",
                        "targetWidth": "Wide",
                        "body":[
                            {
                                "type": "TextBlock",
                                "size": "Medium",
                                "weight": "Bolder",
                                "text": "Provisionierung erfolgt!"
                                },
                            {
                                "type": "ColumnSet",
                                "columns": [
                                    {   
                                        "type": "Column",
                                        "items": [
                                            {   
                                                "type": "TextBlock",
                                                "text": "Ressource:"
                                                },  
                                            {   
                                                "type": "TextBlock",
                                                "text": "UserID:"
                                                },
                                            {
                                                "type": "TextBlock",
                                                "text": "Action:"
                                                }
                                            ],
                                        "width": "auto"
                                        },
                                    {
                                        "type": "Column",
                                        "items": [
                                            {
                                                "type": "TextBlock",
                                                "text": ressource
                                                },
                                            {
                                                "type": "TextBlock",
                                                "text": uid
                                                },
                                            {
                                                "type": "TextBlock",
                                                "text": action
                                                }
                                            ],
                                        "width": "stretch"
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            }
    response = requests.post(url[stage], headers=headers, json=payload, proxies=proxies)

The output in Teams looks like this: screenshot

hajohoetger avatar Aug 01 '24 08:08 hajohoetger

I used three ticks to make the code more readeable, but that didn't work. Maybe, if i upload that as file:

code.txt

hajohoetger avatar Aug 01 '24 08:08 hajohoetger

@hajohoetger I used three ticks to make the code more readeable, but that didn't work.

It has to be backticks: ```

nscuro avatar Aug 01 '24 09:08 nscuro

@hajohoetger I used three ticks to make the code more readeable, but that didn't work.

It has to be backticks: ```

...indeed, that works! (Have edited my former posts.) Thank you! :-)

hajohoetger avatar Aug 01 '24 10:08 hajohoetger

Getting below error while posting a message with using new workflow URL The execution of template action 'Send_each_adaptive_card' failed: the result of the evaluation of 'foreach' expression '@triggerOutputs()?['body']?['attachments']' is of type 'Null'. The result must be a valid array. I am using legacy card like that used to work with webhook URL. It's throwing an above error when use workflow URL

[Array]$audit = 
    @{
        name  = "LastBuildStatus and Provisioning State"
        value = ("$($results.LastRunStatusRunState)" + " - " + "$($results.ProvisioningState -join ',')")
    }  

$body = ConvertTo-Json -Depth 10 @{
        title     = "Status"
        text      = "Reminder "
        separator = "true"
        sections  = @(
            
            @{
                activityTitle    = "Test "
                activitySubtitle = "Please take an action, accordingly, refer [document](https: doc link)"
                facts            = $audit 
            }     
            
        )
      
    } 

Invoke-RestMethod -Method post -ContentType 'application/Json' -Body $body  -Uri $workflowUrl

Any one encountered same issue? Do we really need to convert legacy card into adaptative card format ?

AshishDadhich4h2 avatar Aug 26 '24 16:08 AshishDadhich4h2

Yes we do. Apparently they realized the time window was a bit short and they are going to support old webhooks for longer but you still can no longer create them, only power automate workflows. And they expect a different body with attachments of adaptive cards. I haven't come around to file a PR as the duct taped version above is GEFN for me 🙃

black-snow avatar Aug 28 '24 19:08 black-snow

Here is an adaptive-card-template that works well for me:

{
    "type":"message",
    "attachments":[
        {
            "contentType":"application/vnd.microsoft.card.adaptive",
            "contentUrl":"{{ baseUrl }}/projects/{{ subject.project.uuid | escape(strategy='json') }}",
            "content":{
                "type": "AdaptiveCard",
                "body": [
                    {
                        "type": "TextBlock",
                        "size": "Medium",
                        "weight": "Bolder",
                        "text": "{{ notification.title | escape(strategy="json") }}",
                        "wrap": true
                    },
                    {
                        "type": "ColumnSet",
                        "columns": [
                            {
                                "type": "Column",
                                "items": [
                                    {
                                        "type": "Image",
                                        "style": "Person",
                                        "url": "https://raw.githubusercontent.com/DependencyTrack/branding/master/dt-logo-symbol-blue-background.png",
                                        "altText": "DependencyTrack",
                                        "size": "Small"
                                    }
                                ],
                                "width": "auto"
                            },
                            {
                                "type": "Column",
                                "items": [
                                    {
                                        "type": "TextBlock",
                                        "weight": "Bolder",
                                        "text": "DependencyTrack",
                                        "wrap": true
                                    },
                                    {
                                        "type": "TextBlock",
                                        "spacing": "None",
                                        "text": "{{ timestamp }}",
                                        "isSubtle": true,
                                        "wrap": true
                                    }
                                ],
                                "width": "stretch"
                            }
                        ]
                    },
                    {
                        "type": "TextBlock",
                        "text": "{{ notification.content | escape(strategy="json") }}",
                        "wrap": true
                    },
                    {% if notification.group == "NEW_VULNERABILITY" %}
                    {
                        "type": "FactSet",
                        "facts": [
                            {
                                "title": "Project:",
                                "value": "{{ subject.component.project.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Component:",
                                "value": "{{ subject.component.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Version:",
                                "value": "{{ subject.component.version | escape(strategy="json") }}"
                            },
                            {
                                "title": "Severity:",
                                "value": "{{ subject.vulnerability.severity | escape(strategy="json") }}"
                            },
                            {
                                "title": "VulnId:",
                                "value": "{{ subject.vulnerability.vulnId | escape(strategy="json") }}"
                            }
                        ]
                    }
                    {% elseif notification.group == "NEW_VULNERABLE_DEPENDENCY" %}
                    {
                        "type": "FactSet",
                        "facts": [
                            {
                                "title": "Project:",
                                "value": "{{ subject.component.project.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Component:",
                                "value": "{{ subject.component.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Version:",
                                "value": "{{ subject.component.version | escape(strategy="json") }}"
                            }
                        ]
                    }
                    {% elseif notification.group == "POLICY_VIOLATION" %}
                    {
                        "type": "FactSet",
                        "facts": [
                            {
                                "title": "Project:",
                                "value": "{{ subject.component.project.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Component:",
                                "value": "{{ subject.component.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Version:",
                                "value": "{{ subject.component.version | escape(strategy="json") }}"
                            }
                        ]
                    }
                    {% elseif notification.group == "BOM_PROCESSING_FAILED" %}
                    {
                        "type": "FactSet",
                        "facts": [
                            {
                                "title": "Project:",
                                "value": "{{ subject.component.project.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Component:",
                                "value": "{{ subject.component.toString | escape(strategy="json") }}"
                            },
                            {
                                "title": "Version:",
                                "value": "{{ subject.component.version | escape(strategy="json") }}"
                            }
                        ]
                    }
                    {% endif %}
                ],
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                "version": "1.4"
            }
        }
    ]
}

fabian-zeindl-oebb avatar Sep 04 '24 12:09 fabian-zeindl-oebb

Anything on this that could be included per default in DT? Do we need two separate publishers? One for the legacy, and one for the new format?

nscuro avatar Oct 11 '24 11:10 nscuro

Someone still has to sit down and work this out for the general case.

As the legacy connectors' life span was extended we should have both.

black-snow avatar Oct 15 '24 12:10 black-snow

@black-snow My template works for the general case.

There is a designer available here to work on the template: https://adaptivecards.io/

fabian-zeindl-oebb avatar Oct 15 '24 12:10 fabian-zeindl-oebb

Someone still has to sit down and actually file a PR with @fabian-zeindl-oebb 's template :D

black-snow avatar Oct 15 '24 20:10 black-snow

Note that Microsoft has updated the guidance in the original post and now says that they are looking into allowing messages to continue to be posted using the MessageCard format:

Image

wazzamatazz avatar Nov 04 '24 06:11 wazzamatazz

For those like me who read the above message and were hoping to have an update, we'll have to wait til August:

Update 04/11/2025: New information related to Office 365 Connectors Retirement in Teams – Webhook URL Migration We are still developing a method for webhooks in the Workflow app to support the following scenarios and will share more details before the end of August 2025.

  • Post messageCard formatted message payloads (so you do not have to reformat the payload to adaptive card)
  • Post to private channels

lucymoss-aveva avatar Apr 21 '25 18:04 lucymoss-aveva

Big thanks to @fabian-zeindl-oebb for the template!

I've tweaked it slightly, replacing the component.toString with component.name so the component version doesn't appear twice :)

https://gist.github.com/binaryoverload/a87d243a41eab0749f64adfea964d1ff

binaryoverload avatar Jun 12 '25 09:06 binaryoverload

This is the template we use: https://gist.github.com/elmuerte/54fc63ccc07ed82b28ac9c573b2b26e9

Besides generalizing some parts in the notification it offers a bunch of deeplinks where possible. It also contains a filter at the top to rule out notifications for projects versions ending with -MR-12345 or -MR12345. We also run dependency track analysis on merge request build pipelines, for that we create temporary projects but we don't what violations to show up on Teams (they are shown in the MR though).

Image

FYI, for people working on their own template. You might want to add the following part to the attachment on the level of $schema. It will make the card full-with in Teams:

"msteams": {
    "width": "Full"
},
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"

elmuerte avatar Jul 11 '25 13:07 elmuerte

From what I can see in my testing is that both the old "Office Connector" webhooks and the new "Power Automate/Workflow" webhooks accept the Adaptive Cards format. Dependency Track could switch to the format and be ready for whatever/whenever Micorosoft phases out the old webhook urls. That's what we're doing in https://github.com/DefectDojo/django-DefectDojo/pull/13082

valentijnscholten avatar Sep 03 '25 21:09 valentijnscholten

Well that would be the best-case scenario indeed. Thanks for the heads-up @valentijnscholten

black-snow avatar Sep 05 '25 10:09 black-snow

Since this will break 11/20/2025 is there an update?

kmcrawford avatar Oct 08 '25 19:10 kmcrawford

I don't see a new end date yet on the ms blogpost?

valentijnscholten avatar Oct 08 '25 19:10 valentijnscholten

Image

kmcrawford avatar Oct 08 '25 20:10 kmcrawford

Pretty meaningless without a link...

valentijnscholten avatar Oct 08 '25 20:10 valentijnscholten