Back to Blog
Intelligence March 02, 2026 | 8 min read

DNS Intelligence and Enumeration: Passive DNS and Security Scoring in Elixir

Building DNS intelligence capabilities in Elixir: zone transfer detection, passive DNS analysis, DNSSEC validation, SPF/DKIM/DMARC security scoring, and DNS resolver patterns for OSINT platforms.

Tomas Korcak (korczis)

Prismatic Platform

DNS as an Intelligence Layer


DNS is the nervous system of the internet. Every domain, every service, every piece of infrastructure relies on DNS resolution. For intelligence platforms, DNS data reveals infrastructure topology, hosting relationships, email security posture, and historical changes that other data sources miss.


Unlike active scanning tools that touch target infrastructure, passive DNS analysis reconstructs DNS history from previously observed resolutions. This lets you map infrastructure changes over time without generating any traffic toward the target β€” a critical distinction for sensitive investigations.


Elixir DNS Resolver


Erlang's built-in :inet_res module provides DNS resolution, but it lacks support for advanced record types and DNSSEC. We build a resolver that handles all common record types and provides structured output:



defmodule Prismatic.OSINT.DNS.Resolver do

@moduledoc """

Structured DNS resolver supporting all common record types.

Wraps :inet_res with timeout handling and result normalization.

"""


require Logger


@default_timeout 5_000

@record_types [:a, :aaaa, :mx, :ns, :txt, :cname, :soa, :srv, :ptr, :caa]


@type dns_result :: %{

domain: String.t(),

record_type: atom(),

records: list(map()),

ttl: non_neg_integer() | nil,

query_time_ms: non_neg_integer()

}


@spec resolve(String.t(), atom(), keyword()) :: {:ok, dns_result()} | {:error, term()}

def resolve(domain, type \\ :a, opts \\ []) when type in @record_types do

timeout = Keyword.get(opts, :timeout, @default_timeout)

nameservers = Keyword.get(opts, :nameservers, [])


start_time = System.monotonic_time(:millisecond)

domain_charlist = String.to_charlist(domain)


resolve_opts =

if nameservers != [] do

[{:nameservers, Enum.map(nameservers, &parse_nameserver/1)}]

else

[]

end


result = :inet_res.resolve(domain_charlist, :in, type, resolve_opts, timeout)

elapsed = System.monotonic_time(:millisecond) - start_time


case result do

{:ok, msg} ->

records = extract_records(msg, type)

{:ok, %{

domain: domain,

record_type: type,

records: records,

ttl: extract_min_ttl(msg),

query_time_ms: elapsed

}}


{:error, reason} ->

{:error, {reason, domain, type}}

end

end


@spec resolve_all(String.t(), keyword()) :: map()

def resolve_all(domain, opts \\ []) do

@record_types

|> Task.async_stream(fn type -> {type, resolve(domain, type, opts)} end,

max_concurrency: 4, timeout: 10_000)

|> Enum.reduce(%{domain: domain, records: %{}}, fn

{:ok, {type, {:ok, result}}}, acc ->

put_in(acc, [:records, type], result.records)


{:ok, {type, {:error, _}}}, acc ->

put_in(acc, [:records, type], [])


{:exit, _}, acc ->

acc

end)

end

end


Zone Transfer Detection


Misconfigured DNS servers that allow zone transfers (AXFR) expose the entire domain's record set. Detecting this misconfiguration is both a security finding and an intelligence opportunity:



defmodule Prismatic.OSINT.DNS.ZoneTransfer do

@moduledoc """

Detects DNS zone transfer (AXFR) misconfiguration.

Attempts AXFR against all authoritative nameservers for a domain.

"""


require Logger


@spec check(String.t()) :: {:vulnerable, list(map())} | :secure | {:error, term()}

def check(domain) do

case Prismatic.OSINT.DNS.Resolver.resolve(domain, :ns) do

{:ok, %{records: ns_records}} ->

results =

ns_records

|> Enum.map(& &1.value)

|> Enum.map(fn ns -> {ns, attempt_axfr(domain, ns)} end)


vulnerable = Enum.filter(results, fn {_ns, result} -> match?({:ok, _}, result) end)


if vulnerable != [] do

{:vulnerable, Enum.map(vulnerable, fn {ns, {:ok, records}} ->

%{nameserver: ns, record_count: length(records), records: records}

end)}

else

:secure

end


{:error, reason} ->

{:error, reason}

end

end


defp attempt_axfr(domain, nameserver) do

domain_charlist = String.to_charlist(domain)


case resolve_nameserver_ip(nameserver) do

{:ok, ip} ->

case :inet_res.resolve(domain_charlist, :in, :axfr,

[{:nameservers, [{ip, 53}]}], 10_000) do

{:ok, msg} -> {:ok, extract_axfr_records(msg)}

{:error, _} -> :refused

end


{:error, _} ->

:unresolvable

end

end

end


Email Security Scoring (SPF/DKIM/DMARC)


Email authentication records reveal an organization's security maturity. We parse and score all three major email authentication mechanisms:


RecordPurposeScore WeightBest Practice

|--------|---------|-------------|---------------|

SPFAuthorized mail senders0.30-all (hard fail) DKIMMessage integrity verification0.352048-bit RSA or Ed25519 DMARCPolicy enforcement + reporting0.35p=reject with rua/ruf

defmodule Prismatic.OSINT.DNS.EmailSecurity do

@moduledoc """

Scores email security posture by analyzing SPF, DKIM, and DMARC records.

"""


@spec score(String.t()) :: {:ok, map()} | {:error, term()}

def score(domain) do

with {:ok, spf} <- check_spf(domain),

{:ok, dmarc} <- check_dmarc(domain),

dkim_result <- check_dkim(domain) do

spf_score = score_spf(spf)

dkim_score = score_dkim(dkim_result)

dmarc_score = score_dmarc(dmarc)


overall = spf_score 0.30 + dkim_score 0.35 + dmarc_score * 0.35


{:ok, %{

domain: domain,

overall_score: Float.round(overall, 2),

grade: score_to_grade(overall),

spf: %{record: spf, score: spf_score, details: analyze_spf(spf)},

dkim: %{score: dkim_score, details: dkim_result},

dmarc: %{record: dmarc, score: dmarc_score, details: analyze_dmarc(dmarc)}

}}

end

end


defp check_spf(domain) do

case Prismatic.OSINT.DNS.Resolver.resolve(domain, :txt) do

{:ok, %{records: records}} ->

spf = Enum.find(records, fn r ->

String.starts_with?(r.value, "v=spf1")

end)

{:ok, spf && spf.value}


error -> error

end

end


defp score_spf(nil), do: 0.0

defp score_spf(record) do

cond do

String.contains?(record, "-all") -> 1.0

String.contains?(record, "~all") -> 0.7

String.contains?(record, "?all") -> 0.3

String.contains?(record, "+all") -> 0.1

true -> 0.5

end

end


defp analyze_dmarc(nil), do: %{policy: :none, configured: false}

defp analyze_dmarc(record) do

policy = extract_dmarc_tag(record, "p")

rua = extract_dmarc_tag(record, "rua")

ruf = extract_dmarc_tag(record, "ruf")

pct = extract_dmarc_tag(record, "pct") || "100"


%{

policy: policy,

configured: true,

aggregate_reporting: rua != nil,

forensic_reporting: ruf != nil,

percentage: String.to_integer(pct)

}

end


defp score_to_grade(score) when score >= 0.9, do: "A"

defp score_to_grade(score) when score >= 0.7, do: "B"

defp score_to_grade(score) when score >= 0.5, do: "C"

defp score_to_grade(score) when score >= 0.3, do: "D"

defp score_to_grade(_), do: "F"

end


Passive DNS Integration


Passive DNS data comes from sensors that record DNS queries and responses without actively querying targets. We integrate with passive DNS providers to build historical resolution timelines:


ProviderCoverageHistory DepthRate Limit

|----------|----------|---------------|------------|

Farsight DNSDBGlobal10+ yearsPer-query pricing SecurityTrailsGlobal5+ years50/month (free) PassiveTotalGlobal8+ years15/day (community) VirusTotal pDNSGlobal3+ yearsIncluded with VT API

@spec passive_lookup(String.t(), keyword()) :: {:ok, list(map())} | {:error, term()}

def passive_lookup(domain, opts \\ []) do

providers = Keyword.get(opts, :providers, [:security_trails, :virustotal])


results =

providers

|> Task.async_stream(&fetch_passive(&1, domain), max_concurrency: 3, timeout: 15_000)

|> Enum.flat_map(fn

{:ok, {:ok, records}} -> records

_ -> []

end)

|> Enum.uniq_by(&{&1.rrname, &1.rrtype, &1.rdata})

|> Enum.sort_by(& &1.first_seen, {:desc, DateTime})


{:ok, results}

end


DNSSEC Validation


DNSSEC adds cryptographic signatures to DNS responses, preventing cache poisoning and man-in-the-middle attacks. Validating DNSSEC provides insight into an organization's DNS security posture and helps detect tampering in active investigations.


The DNS intelligence module integrates with the platform's entity enrichment pipeline. When an investigation discovers a domain, the module automatically resolves all record types, checks email security, queries passive DNS history, and scores the overall DNS security posture. Results feed into the confidence scoring engine, where DNS-derived intelligence is weighted against other OSINT sources to produce a composite reliability assessment.

Tags

dns osint elixir dnssec passive-dns

Related Glossary Terms