How I Stopped Manually Reading Email Headers and Built a Phishing Analyzer That Does It For Me
A practical walkthrough of building a Mini-SOC automation pipeline using Microsoft Power Automate, KnowBe4 PhishER, Freshservice, and…
How I Stopped Manually Reading Email Headers and Built a Phishing Analyzer That Does It For Me
A practical walkthrough of building a Mini-SOC automation pipeline using Microsoft Power Automate, KnowBe4 PhishER, Freshservice, and OpenAI. Written by someone who had zero Power Automate experience before this project.
There’s a specific kind of tedium that security analysts know well. It’s not the scary incidents, those keep you sharp. It’s the volume work. The stack of phishing reports sitting in your inbox every morning, each one requiring the same ritual: open the ticket, find the raw email headers buried in the body, read through lines of authentication results, figure out where the email actually came from, make a call on whether it’s a real threat or just aggressive marketing, write it up, post it to Teams, and repeat.
At Constellar Holdings, we use KnowBe4’s Phish Alert Button, which makes it genuinely easy for employees to report suspicious emails. Great for security culture, but it also means the volume of reports I was reviewing manually kept growing. Most of them weren’t threats. But I still had to look at each one to know that.
The decision to automate wasn’t dramatic. I just got tired of doing the same ten-step process for the fifteenth time in a week and thought: this should not require a human.
Here’s everything I built, how it works, and what went wrong along the way.
Understanding the Stack Before Building Anything
Our phishing reporting pipeline at Constellar works like this: an employee receives a suspicious email and clicks the KnowBe4 Phish Alert Button directly in Outlook. That action forwards the email to KnowBe4 PhishER, which does initial automated triage. PhishER then creates a ticket in Freshservice, our IT service management platform. Freshservice sends a notification email to the security team, and that email lands in a dedicated Outlook folder called “Phish Alert Report.”

Here’s what makes this setup useful for automation: the Freshservice ticket email doesn’t just say “user reported a phishing email.” It embeds the full raw headers of the originally reported email right in the body including the Authentication-Results header with SPF, DKIM, and DMARC verdicts, the Received chain showing every server the email passed through, the client IP of the originating server, and the Return-Path. That raw header data is what the entire flow parses.
To understand why parsing it matters, you need to understand what SPF, DKIM, and DMARC actually tell you.
Why SPF, DKIM, and DMARC Are the Foundation of Email Threat Detection
These three protocols are the closest thing email has to a chain of custody. They don’t catch every phishing email, but when they fail, that failure is meaningful signal. When all three pass on a suspicious-looking email, that’s also meaningful and more complicated than it sounds.
SPF (Sender Policy Framework) answers one specific question: is the mail server that sent this email authorized to send on behalf of the domain in the envelope sender address? Every domain can publish an SPF record in DNS, essentially a list of IP addresses or hostnames that are allowed to send email for that domain. When your mail server receives an email claiming to come from @constellar.co, it checks the SPF record for constellar.co and verifies whether the sending server's IP is on that list. If it's not, SPF fails. An spf=fail result in the Authentication-Results header means the email didn't come from where it claimed to.
What SPF doesn’t catch: attackers who send through legitimate infrastructure they control. If someone registers constellar-co.com and sets up their own mail server, SPF will pass — because they're the legitimate owner of that domain. SPF only validates the envelope sender, not the visible From address the end user sees.
DKIM (DomainKeys Identified Mail) adds cryptographic verification on top of that. Before sending, the mail server signs the email with a private key. The public key is published in DNS under the sender’s domain. When the receiving server gets the email, it retrieves that public key and verifies the signature proving both that the email was genuinely sent by someone with access to that domain’s private key, and that the email content hasn’t been modified in transit. If the signature doesn’t match, DKIM fails. A dkim=fail result means either the email is being impersonated, or it was tampered with between sender and recipient.
DMARC (Domain-based Message Authentication, Reporting, and Conformance) is where the real protection kicks in. SPF and DKIM both verify technical attributes of the email. But neither verifies that those attributes match the domain the end user actually sees in their From field and that gap is exactly what phishing exploits. An attacker can send through a legitimate server they control (SPF passes), sign it with a DKIM key from a domain they own (DKIM passes), while the visible From address shows @realcompany.com. DMARC checks whether the domain in the visible From header aligns with what SPF and DKIM actually verified. If it doesn't, DMARC fails. That mismatch is the textbook definition of domain spoofing, and a dmarc=fail result is the most significant red flag of the three.
A well-configured legitimate email should pass all three. When we see spf=fail dkim=fail dmarc=fail, that's a strong THREAT signal. But here's the nuance worth understanding: marketing emails sent through platforms like HubSpot or Mailchimp can pass all three authentication checks, because those platforms are authorized senders for the brand's domain and do sign with DKIM. So spf=pass dkim=pass dmarc=pass doesn't mean an email is benign it means the authentication chain is intact, and the content could still be malicious.
This is exactly why we layer AI analysis on top of the rule-based checks. Authentication results tell you about the technical envelope. AI reasoning tells you about the content and intent.

Raw email headers in Freshservice ticket body showing Authentication-Results with SPF/DKIM/DMARC
Building the Flow
The flow lives entirely in Power Automate cloud. After a lot of iteration, the final architecture uses only Compose actions and one HTTP action. No variables, no Condition branches, I’ll explain why those caused problems later.
The trigger is “When a new email arrives in folder (V3)” pointed at the “Phish Alert Report” Outlook folder. This fires every time Freshservice drops a ticket notification there. Use V3 of this trigger specifically it handles folder selection properly in a way older versions don’t.
From there, the flow runs through these actions in sequence.
Get_Body grabs the full email body containing the embedded raw headers:
triggerOutputs()?['body/body']
Body_Lowercase normalizes everything for consistent string matching. Authentication-Results headers can have mixed case depending on the sending server:
toLower(outputs('Get_Body'))
SPF_Result, DKIM_Result, DMARC_Result are three separate Compose actions doing a single string check each:
SPF_Result:
if(contains(outputs('Body_Lowercase'), 'spf=fail'), '❌ SPF FAIL', '✅ SPF OK')
DKIM_Result:
if(contains(outputs('Body_Lowercase'), 'dkim=fail'), '❌ DKIM FAIL', '✅ DKIM OK')
DMARC_Result:
if(contains(outputs('Body_Lowercase'), 'dmarc=fail'), '❌ DMARC FAIL', '✅ DMARC OK')
Field extraction pulls specific values from the header text using substring and indexOf:
Extract_From:
trim(substring(outputs('Body_Lowercase'),
add(indexOf(outputs('Body_Lowercase'), 'from: '), 6), 60))
Extract_Subject:
trim(substring(outputs('Body_Lowercase'),
add(indexOf(outputs('Body_Lowercase'), 'subject: '), 9), 80))
Extract_IP (Originating IP from client-ip field):
trim(substring(outputs('Body_Lowercase'),
add(indexOf(outputs('Body_Lowercase'), 'client-ip='), 10), 15))
Extract_ReturnPath:
trim(substring(outputs('Body_Lowercase'),
add(indexOf(outputs('Body_Lowercase'), 'return-path: '), 13), 60))
The Return-Path extraction is worth calling out specifically. When a marketing email is sent through HubSpot, the Return-Path will be something like 1axcautl@bf56x.hubspotemail.net even though the From address shows a brand name. That domain mismatch between From and Return-Path is a common indicator of email sent through a third-party ESP worth flagging in the report even when all three authentication checks pass.
Verdict calculates the overall assessment:
if(or(contains(outputs('Body_Lowercase'), 'spf=fail'),
contains(outputs('Body_Lowercase'), 'dkim=fail'),
contains(outputs('Body_Lowercase'), 'dmarc=fail')),
'⛔ THREAT — Auth Failure Detected',
if(and(contains(outputs('Body_Lowercase'), 'spf=pass'),
contains(outputs('Body_Lowercase'), 'dkim=pass'),
contains(outputs('Body_Lowercase'), 'dmarc=pass')),
'✅ CLEAN — All Auth Passed',
'⚠️ SPAM — Suspicious Pattern'))

Power Automate flow canvas showing all Compose actions in sequence
The AI Analysis Layer
After the rule-based checks, the email data goes to OpenAI’s API via an HTTP action for contextual analysis. This is where the flow goes from “tells you what the authentication results were” to “tells you what to actually do about it.”
The HTTP action posts to https://api.openai.com/v1/chat/completions with these headers:
Content-Type: application/json
Authorization: Bearer YOUR_OPENAI_API_KEY
And this body:
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a cybersecurity analyst specializing in phishing email detection. Be concise and actionable. Do not add disclaimers."
},
{
"role": "user",
"content": "Analyze this email security report and provide a structured threat assessment in exactly 3 parts:\n\n1. SUMMARY: 1-2 sentences describing what this email appears to be and who sent it.\n\n2. RISK FACTORS: A short bullet list of specific suspicious elements detected — authentication failures, domain mismatches, suspicious sending infrastructure, urgency language, or anything else notable.\n\n3. RECOMMENDATION: One of these four actions with a one-sentence reason — Ignore / Monitor / Investigate / Block.\n\nEmail headers and body content:\n@{substring(outputs('Body_Lowercase'), 0, 2000)}\n\nAuthentication Results:\nSPF: @{outputs('SPF_Result')}\nDKIM: @{outputs('DKIM_Result')}\nDMARC: @{outputs('DMARC_Result')}\nOverall Verdict: @{outputs('Verdict')}"
}
],
"max_tokens": 350,
"temperature": 0.3
}
A few decisions in that prompt are worth explaining. Setting temperature to 0.3 keeps responses focused and consistent, security analysis should be deterministic, not creative. The "exactly 3 parts" instruction with numbered labels enforces formatting consistency across wildly different email types. "Do not add disclaimers" is necessary because without it, GPT consistently appends caveats like "this analysis should not substitute professional security review" to every response , useless noise in an operational context.
The AI response is extracted with:
body('OpenAI_Analysis')?['choices']?[0]?['message']?['content']
On cost: GPT-4o mini charges $0.15 per million input tokens. Each analysis uses roughly 2,000 tokens combined. That’s $0.0003 per email. At 600 reports per month, the total cost is $0.18. We topped up the OpenAI account with $5 and it will last years at this volume.
What the Output Looks Like
The flow sends two notifications for every phishing report. Microsoft Teams gets a compact card with the verdict, ticket subject, reporter, originating IP, Return-Path, authentication results, and the AI analysis all in one place readable at a glance without opening anything else. The email gets a full HTML report with a color-coded verdict banner, a structured table of all extracted fields, and the full GPT-4o mini analysis in a highlighted callout box. Both arrive before I’ve even opened the original ticket.

Microsoft Teams alert card showing SOC Alert with verdict, auth results, and AI analysis


Email report with color-coded verdict banner and GPT-4o mini analysis section
What Actually Went Wrong
The finished flow looks clean. The path to get there wasn’t.
The self-reference variable problem was the first major wall. My original design used a string variable to accumulate a running list of detected issues each failed check would append to it. Power Automate throws this error when you try to reference a variable in its own Set Variable action through Expression mode:
WorkflowRunActionInputsInvalidProperty: Self reference is not
supported when updating the value of variable 'Findings'.
Using @{variables('Findings')} in plain text interpolation mode technically works because PA treats it as template interpolation rather than self-reference. But the cleaner fix was eliminating variables entirely. Every piece of data is now a Compose output, which any downstream action can reference without restriction.
Condition actions created more problems than they solved. I initially built the SPF/DKIM/DMARC logic using Condition branches, which felt like the natural Power Automate way to structure conditional logic. What I discovered: Condition actions have a read-only Code View tab where you can see the underlying JSON but cannot edit it. Expression validation is inconsistently strict. And the GUI makes it easy to accidentally use Dynamic content tokens when you need Expression syntax those two things look nearly identical but behave very differently. Rebuilding everything as Compose actions with if() expressions fixed all of it.
The Expression tab versus Dynamic content confusion deserves its own mention because it cost real time. When building a Condition, Power Automate shows both a Dynamic content picker and an Expression tab. If you pick outputs('Body_Lowercase') from Dynamic content, it creates a token that looks correct but throws an invalid expression error at save time. You have to click the Expression tab, type outputs('Body_Lowercase') manually without an @ prefix, and click OK. PA adds the prefix itself. This is not obvious from the interface.
The OpenAI API scope issue was the last significant blocker. After creating a Restricted API key with only Chat completions enabled, the first API call returned Missing scopes: model.request. Setting the key to All permissions resolved it immediately. For production, you'll want to revisit this — but for getting the flow working, All permissions is fine.
Results
Time per ticket before: five to ten minutes of manual header reading and copy-pasting. Time per ticket after: under thirty seconds, mostly spent reading the AI analysis. The Teams alert is already waiting when I open the app. If it says CLEAN, I confirm in five seconds and move on. If it says THREAT, I already have the originating IP, Return-Path, and AI reasoning in front of me before I’ve opened the original ticket.
Consistency is also worth noting. Manual analysis varies by analyst and by how much coffee was involved. The flow applies the same rules every time, regardless of ticket volume.

Side-by-side Teams notification and email report in production
What’s Next
The current AI analysis is limited by what goes into the prompt. We’re passing the first 2,000 characters of the body, which is mostly raw email headers, so the model sees authentication data and sender information but not the actual email content the user received. The next iteration will extract the email body separately and include it in the prompt, so the AI can reason about urgency language, social engineering tactics, suspicious link text, and the actual message copy.
Beyond that: URL extraction with VirusTotal reputation lookup, auto-routing emails to verdict-specific subfolders, attachment hash scanning, and pulling from flow run history. The extraction logic also needs proper null checking right now, if indexOf returns -1 because a header field isn't present, the substring expression crashes the action.
The Takeaway
Security automation doesn’t have to be a six-month project with a dedicated engineering team. Power Automate is included in most Microsoft 365 licenses. KnowBe4 and Freshservice were already in our stack. The only new cost was $5 in OpenAI credits.
What it required was understanding the data knowing what information the Freshservice ticket actually contained, understanding what SPF/DKIM/DMARC results actually mean, and being willing to iterate when the first approach didn’t work. The whole thing took one afternoon to build from scratch, including all the debugging time. If you’re a security analyst still reviewing phishing reports manually, the barrier to building something like this is lower than it looks.
Kiell Tampubolon — Cybersecurity at Constellar Holdings, Singapore
#Cybersecurity #PowerAutomate #SOCAutomation #KnowBe4 #OpenAI #EmailSecurity #PhishingDetection #SecurityEngineering
메타데이터
- post_id
- 265cc0223ecf
- slug
- how-i-stopped-manually-reading-email-headers-and-built-a-phishing-analyzer-that-does-it-for-me-265cc0223ecf
- url
- https://medium.com/the-constellar-digital-technology-blog/how-i-stopped-manually-reading-email-headers-and-built-a-phishing-analyzer-that-does-it-for-me-265cc0223ecf
- canonical_url
- https://medium.com/the-constellar-digital-technology-blog/how-i-stopped-manually-reading-email-headers-and-built-a-phishing-analyzer-that-does-it-for-me-265cc0223ecf
- author_url
- https://medium.com/@kielltampubolon
- status
- ok
- fetched_at
- 2026-06-16 19:09:56