Files
parsedmarc/dashboards/splunk/dmarc_aggregate_dashboard.xml
T
Sean WhalenandClaude Fable 5 e892de794b Address Copilot round-4 findings on dashboards
Fix three pre-existing saved-object title typos in the OpenSearch
ndjson (leading space on "Aggregate DMARC passed DMARC", trailing
space on "Aggregate DMARC reporting organizations", double space in
"map  of message sources by country"), in both the top-level title
and the embedded visState title.

Normalize the Splunk DKIM details placeholders: the base search's
fillnull renders wholly-missing DKIM fields as the literal string
"null", so unsigned mail showed "null / null / null" while the SPF
panel shows "none". Rewrite the values to "none" after the signature
split, where the fields are single-valued and the mvzip pairing
cannot be disturbed. Verified against the dev Splunk that no
truncation or mis-pairing occurs either way, since fillnull
guarantees the fields are never actually null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:35:48 -04:00

365 lines
15 KiB
XML

<form theme="dark" version="1.1">
<label>Aggregate DMARC data</label>
<description>A summary of aggregate DMARC report data</description>
<search id="base_search">
<query>
index="email" sourcetype="dmarc:aggregate" spf_aligned=$spf_aligned$ dkim_aligned=$dkim_aligned$ passed_dmarc=$passed_dmarc$ org_name=$org_name$ source_reverse_dns=$source_reverse_dns$ header_from=$header_from$ envelope_from=$envelope_from$ disposition=$disposition$ source_ip_address=$source_ip_address$ source_base_domain=$source_base_domain$ source_country=$source_country$
| rename spf_results{}.domain as envelope_domain spf_results{}.result as spf_result spf_results{}.scope as spf_scope dkim_results{}.selector as dkim_selector dkim_results{}.domain as dkim_domain dkim_results{}.result as dkim_result
| fillnull value=null source_reverse_dns source_base_domain dkim_selector dkim_domain dkim_result source_type source_name source_as_name
| search dkim_selector=$dkim_selector$ dkim_domain=$dkim_domain$ source_type="$source_type$" source_name="$source_name$" source_as_name="$source_as_name$"
| table *
</query>
<earliest>$time_range.earliest$</earliest>
<latest>$time_range.latest$</latest>
</search>
<fieldset submitButton="false" autoRun="true">
<input type="dropdown" token="spf_aligned" searchWhenChanged="true">
<label>SPF alignment</label>
<choice value="*">any</choice>
<choice value="true">true</choice>
<choice value="false">false</choice>
<default>*</default>
</input>
<input type="dropdown" token="dkim_aligned" searchWhenChanged="true">
<label>DKIM alignment</label>
<choice value="*">any</choice>
<choice value="true">true</choice>
<choice value="false">false</choice>
<default>*</default>
</input>
<input type="dropdown" token="passed_dmarc" searchWhenChanged="true">
<label>Passed DMARC</label>
<choice value="*">any</choice>
<choice value="true">true</choice>
<choice value="false">false</choice>
<default>*</default>
</input>
<input type="text" token="org_name" searchWhenChanged="true">
<label>Reporting organization</label>
<default>*</default>
</input>
<input type="text" token="header_from" searchWhenChanged="true">
<label>Message header from</label>
<default>*</default>
</input>
<input type="text" token="envelope_from" searchWhenChanged="true">
<label>Envelope from</label>
<default>*</default>
</input>
<input type="dropdown" token="disposition" searchWhenChanged="true">
<label>Message disposition</label>
<choice value="*">any</choice>
<choice value="none">none</choice>
<choice value="quarantine">quarantine</choice>
<choice value="reject">reject</choice>
<default>*</default>
</input>
<input type="dropdown" token="source_name" searchWhenChanged="true">
<label>Source name</label>
<default>*</default>
<choice value="*">any</choice>
<initialValue>*</initialValue>
<fieldForLabel>source_name</fieldForLabel>
<fieldForValue>source_name</fieldForValue>
<search>
<query>index="email" sourcetype="dmarc:aggregate" source_type="$source_type$"
| stats count by source_name</query>
</search>
</input>
<input type="dropdown" token="source_type" searchWhenChanged="true">
<label>Source type</label>
<default>*</default>
<initialValue>*</initialValue>
<choice value="*">any</choice>
<fieldForLabel>source_type</fieldForLabel>
<fieldForValue>source_type</fieldForValue>
<search>
<query>index="email" sourcetype="dmarc:aggregate"
| stats count by source_type</query>
</search>
</input>
<input type="dropdown" token="source_as_name" searchWhenChanged="true">
<label>Source AS name</label>
<choice value="*">any</choice>
<default>*</default>
<initialValue>*</initialValue>
<fieldForLabel>source_as_name</fieldForLabel>
<fieldForValue>source_as_name</fieldForValue>
<search>
<query>index="email" sourcetype="dmarc:aggregate"
| stats count by source_as_name</query>
<earliest>0</earliest>
<latest></latest>
</search>
</input>
<input type="text" token="source_ip_address" searchWhenChanged="true">
<label>Source IP address</label>
<default>*</default>
</input>
<input type="text" token="source_reverse_dns" searchWhenChanged="true">
<label>Source reverse DNS</label>
<default>*</default>
</input>
<input type="text" token="source_base_domain" searchWhenChanged="true">
<label>Source base domain</label>
<default>*</default>
</input>
<input type="text" token="source_country" searchWhenChanged="true">
<label>Source country ISO code</label>
<default>*</default>
</input>
<input type="text" token="dkim_selector" searchWhenChanged="true">
<label>DKIM selector</label>
<default>*</default>
</input>
<input type="text" token="dkim_domain" searchWhenChanged="true">
<label>DKIM domain</label>
<default>*</default>
</input>
<input type="time" token="time_range" searchWhenChanged="true">
<label>Time range</label>
<default>
<earliest>-7d@h</earliest>
<latest>now</latest>
</default>
</input>
</fieldset>
<row>
<panel>
<title>SPF alignment</title>
<chart>
<search base="base_search">
<query>| stats sum(message_count) as message_count by spf_aligned</query>
</search>
<option name="charting.chart">pie</option>
<option name="charting.drilldown">none</option>
<option name="charting.fieldColors">{"true":0x65a637,"false":0xd93f3c}</option>
</chart>
</panel>
<panel>
<title>DKIM alignment</title>
<chart>
<search base="base_search">
<query> | stats sum(message_count) by dkim_aligned</query>
</search>
<option name="charting.chart">pie</option>
<option name="charting.drilldown">none</option>
<option name="charting.fieldColors">{"true":0x65a637,"false":0xd93f3c}</option>
<option name="height">250</option>
</chart>
</panel>
<panel>
<title>Passed DMARC</title>
<chart>
<search base="base_search">
<query>| stats sum(message_count) by passed_dmarc</query>
</search>
<option name="charting.chart">pie</option>
<option name="charting.drilldown">none</option>
<option name="charting.fieldColors">{"true":0x65a637,"false":0xd93f3c}</option>
</chart>
</panel>
</row>
<row>
<panel>
<title>Reporting organizations</title>
<table>
<search base="base_search">
<query>| stats sum(message_count) as message_count by org_name | sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="sum(message_count)">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<title>Message sources by reverse DNS</title>
<table>
<search base="base_search">
<query>| fillnull value="none" | stats sum(message_count) as message_count by source_base_domain | sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="sum(message_count)">
<option name="precision">0</option>
</format>
</table>
</panel>
<panel>
<title>Message volume and DMARC compliance by from domain</title>
<table>
<search base="base_search">
<query>| stats sum(message_count) as Messages, sum(eval(if(passed_dmarc="true", message_count, 0))) as passed by header_from | eval "% DMARC Compliant"=if(Messages>0, round(passed/Messages*100, 1), null()) | fields - passed | rename header_from as "From Domain" | sort -Messages</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="Messages">
<option name="precision">0</option>
</format>
<format type="number" field="% DMARC Compliant">
<option name="precision">1</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>Message sources by name and type</title>
<table>
<search base="base_search">
<query> | stats sum(message_count) as message_count by source_name, source_type | sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="message_count">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>Message sources by Autonomous System</title>
<table>
<search base="base_search">
<query>| stats sum(message_count) as message_count by source_asn, source_as_name, source_as_domain
| eval source_asn = if(isnotnull(source_asn) AND source_asn!="", "AS" . source_asn, source_asn)
| sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="message_count">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>DMARC passage over time</title>
<chart>
<search base="base_search">
<query>| timechart span=1d sum(message_count) as message_count by passed_dmarc</query>
</search>
<option name="charting.axisTitleX.text">Time</option>
<option name="charting.axisTitleX.visibility">visible</option>
<option name="charting.axisTitleY.text">Messages</option>
<option name="charting.axisTitleY.visibility">visible</option>
<option name="charting.axisTitleY2.visibility">visible</option>
<option name="charting.chart">line</option>
<option name="charting.drilldown">none</option>
<option name="charting.fieldColors">{"true":0x65a637,"false":0xd93f3c}</option>
<option name="charting.legend.placement">right</option>
<option name="height">280</option>
<option name="refresh.display">progressbar</option>
</chart>
</panel>
</row>
<row>
<panel>
<title>Message disposition over time</title>
<chart>
<search base="base_search">
<query>| timechart span=1d sum(message_count) as message_count by disposition</query>
</search>
<option name="charting.axisTitleX.text">Time</option>
<option name="charting.axisTitleY.text">Messages</option>
<option name="charting.chart">line</option>
<option name="charting.chart.nullValueMode">zero</option>
<option name="charting.chart.showDataLabels">none</option>
<option name="charting.drilldown">none</option>
<option name="charting.fieldColors">{"none":0x65a637,"quarantine":0xf2b134,"reject":0xd93f3c}</option>
<option name="refresh.display">progressbar</option>
</chart>
</panel>
</row>
<row>
<panel>
<title>Map of message sources by country</title>
<map>
<search base="base_search">
<query> | iplocation source_ip_address | stats sum(message_count) as message_count by Country | geom geo_countries featureIdField="Country"</query>
</search>
<option name="drilldown">none</option>
<option name="height">566</option>
<option name="mapping.type">choropleth</option>
</map>
</panel>
<panel>
<title>Message sources by country</title>
<table>
<search base="base_search">
<query>| stats sum(message_count) as message_count by source_country | sort -message_count</query>
</search>
<option name="count">20</option>
<option name="drilldown">none</option>
<option name="percentagesRow">false</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="sum(message_count)">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>Message sources by IP address</title>
<table>
<search base="base_search">
<query>| fillnull value="none" source_reverse_dns source_base_domain | fillnull value="unknown" source_country | stats sum(message_count) as message_count by source_ip_address,source_reverse_dns,source_base_domain,source_country | sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="message_count">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>SPF details</title>
<table>
<search base="base_search">
<query>| fillnull value="none" source_base_domain envelope_domain spf_scope spf_result
| eval spf_signature=mvzip(mvzip(spf_scope, envelope_domain, " / "), spf_result, " / ")
| mvexpand spf_signature
| stats sum(message_count) as message_count by header_from, envelope_from, spf_signature, source_base_domain, spf_aligned
| eval parts=split(spf_signature, " / "), spf_scope=mvindex(parts, 0), spf_domain=mvindex(parts, 1), spf_result=mvindex(parts, 2)
| table header_from, envelope_from, spf_scope, spf_domain, spf_result, spf_aligned, source_base_domain, message_count
| sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="message_count">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
<row>
<panel>
<title>DKIM details</title>
<table>
<search base="base_search">
<query>| fillnull value="none" source_base_domain
| eval dkim_signature=mvzip(mvzip(dkim_selector, dkim_domain, " / "), dkim_result, " / ")
| mvexpand dkim_signature
| stats sum(message_count) as message_count by header_from, dkim_signature, dkim_aligned, source_base_domain
| eval parts=split(dkim_signature, " / "), dkim_selector=mvindex(parts, 0), dkim_domain=mvindex(parts, 1), dkim_result=mvindex(parts, 2)
| eval dkim_selector=if(dkim_selector=="null", "none", dkim_selector), dkim_domain=if(dkim_domain=="null", "none", dkim_domain), dkim_result=if(dkim_result=="null", "none", dkim_result)
| table header_from, dkim_selector, dkim_domain, dkim_result, dkim_aligned, source_base_domain, message_count
| sort -message_count</query>
</search>
<option name="drilldown">none</option>
<option name="refresh.display">progressbar</option>
<format type="number" field="message_count">
<option name="precision">0</option>
</format>
</table>
</panel>
</row>
</form>