Inhalt

Run XDRInternals as GitHub Action

When Nathan and I released XDRInternals one of the biggest shortcomings for me was the lack of workload identity support. Since we are using the native API of the Defender portal only delegated permissions are supported, which makes it very hard to automate things in a pipeline.

But the fact that it makes it very hard should not prevent you from doing it. Security considerations and common sense are the reasons you should not do it, but let’s throw them overboard for the fun of it.

Act 1 : Authenticate as a user, as a machine

It’s 2026 and agentic AI is the buzzword everybody on LinkedIn is posting about. This will be not about this technology. I was thinking about a way to sign-in as a “regular” user inside of a pipeline for a long time and tools like ROADtools Token eXchange or AADInternals allow you to automate those authentication flows including support for TOTP.

But on my way home from the family Christmas party I came up with an “even better” idea. Why not use the latest and greatest in security to authenticate:

Passkeys (or Huskys, as Google Assistant wrote in my voice memo)

Both of the before mentioned tools do not (as of now) support this flow natively and I thought it would be a nice coding task.

So I spun up Fiddler and authenticated using a Passkey to a Entra ID client, in this case the security portal. This already gave me a good idea of how the actual flow should look like.

/run-xdrinternal-github-action/images/passkey-authentication-flow.png

Next I looked into options to export the key material. An easy task if you use a third passkey provider like KeePassXC, Bitwarden, you name it. With the native platform providers like Google and Apple this is much harder, as they don’t support any export capability.

I settled on KeePassXC and the browser plugin for my testing, but also validated an exported Bitwarden passkey in the process.

/run-xdrinternal-github-action/images/keepassxc-passkey-export.png

Take this serious! It’s not a good idea to export the private key to an unencrypted medium.

/run-xdrinternal-github-action/images/passkey-export-security-warning.png

And what does a passkey look like on the inside, you might ask? Basically it’s just another JSON file that contains all the information you need to sign a challenge provided by the relying party (aka Microsoft Entra ID).

/run-xdrinternal-github-action/images/passkey-json-structure.png

With this out of the way I explored the best options to sign a FIDO2 challenge programmatically without relying on custom PowerShell classes or DLLs. And when you restrict yourself to PowerShell 7 this is very easy, as Microsoft has the required functions and crypto built in.

I won’t go into the details, but I implemented everything into TokenTacticsV2. Take a look at the code yourself if you are curious.

After some trial and error with the actual flow I finally was able to implement the flow as a new function called Invoke-EntraIDPasskeyLogin and it supports native KeePassXC passkey exports or you can provide the information bit by bit yourself.

Let’s put it to the test locally and inside a GitHub action.

Act 2: Run little security issue, run!

On my local machine I tried my new toy by connecting to a tenant, as vanilla as they come, thanks to Nathan. And after the successful sign-in using the stored passkey credential I was able to use the resulting ESTSAUTH cookie to connect to the Defender portal using XDRInternals and query the Advanced Hunting settings. Neat!

/run-xdrinternal-github-action/images/xdrinternal-defender-portal.png
XDRInternals connected to Microsoft Defender portal showing Advanced Hunting configuration after successful passkey authentication

Now I used the values from the passkey file and added them to a GitHub repo as secrets and variables.

/run-xdrinternal-github-action/images/github-secrets-setup.png

/run-xdrinternal-github-action/images/github-variables-setup.png

/run-xdrinternal-github-action/images/github-additional-variables.png

Then I created a GitHub action that authenticates via those variables and extracts the same XDR Advanced Features as the local script

name: Authenticate and run XDRInternals for glamor and glory

on:
  workflow_dispatch:

permissions:
  contents: read
  id-token: write

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout TokenTactics repository
        uses: actions/checkout@v6
        with:
          repository: f-bader/TokenTacticsV2
          ref: passkey
          path: TokenTacticsV2

      - name: Checkout XDRInternals repository
        uses: actions/checkout@v6
        with:
          repository: MSCloudInternals/XDRInternals
          path: XDRInternals

      - name: Authenticate via passkey and run XDRInternals
        shell: pwsh
        run: |
          Import-Module $env:GITHUB_WORKSPACE/XDRInternals/XDRInternals/XDRInternals.psd1
          Import-Module $env:GITHUB_WORKSPACE/TokenTacticsV2/TokenTactics.psd1
          $Parameters = @{
            UserPrincipalName = "${{ vars.PASSKEY_USERNAME }}"
            RelyingParty      = "${{ vars.PASSKEY_RELYING_PARTY }}"
            UserHandle        = "${{ secrets.PASSKEY_USER_HANDLE }}"
            CredentialId      = "${{ secrets.PASSKEY_CREDENTIAL_ID }}"
            PrivateKey        = "${{ secrets.PASSKEY_PRIVATE_KEY_PEM }}"
          }
          # This will expose the ESTSAUTH cookie as a global variable $ESTSAUTH
          Invoke-EntraIDPasskeyLogin @Parameters
          Connect-XdrByEstsCookie -EstsAuthCookieValue $ESTSAUTH
          Get-XdrEndpointAdvancedFeatures          

And like all my GitHub actions, it worked on the first try ;)

/run-xdrinternal-github-action/images/github-action-success-output.png

/run-xdrinternal-github-action/images/xdr-advanced-features-results.png
PowerShell output showing successful XDR Advanced Features retrieval and advanced hunting schema configuration

Wonderful. This means I can now sign-in to Entra ID from inside a GitHub pipeline using a passkey and fulfil a strong authentication requirement. Still, if this is a good thing is not what we discuss today. Today we do fun stuff.

Act 3: Profit Build a community resource

Of course just running this inside of a GitHub action is not really that helpful. But the data provided inside of any XDR Defender tenant is. For example there is the full schema reference for all Advanced Hunting tables. To stay on top of the changes made to those tables can be an almost impossible task on your own. But now with the power of automation I can revive one of my projects from long ago:

xdrinternals.com - A website that tracks the changes to the schema like new ActionType values, new tables and updated descriptions.

The last manual update was done in late 2024, but as with most manual tasks, I never came around to do it on a regular schedule. Now I’m able to. After some minor changes to my script from back then I now have a fully working solution that updates itself on a daily basis.

Next steps will include updating the website in a way that it also incorporates the XDRinternals module documentation, update the theme and open-source the complete script.

If you should run this to automate real world tasks in your tenant? That’s for you to answer, but GitHub secrets are a safe way to store the private key. Of course you must make sure to never expose the secrets.