Initial commit

This commit is contained in:
Smaug123
2024-06-01 21:35:18 +01:00
commit b37eddc6e3
22 changed files with 1723 additions and 0 deletions

18
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,18 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "6.3.4",
"commands": [
"fantomas"
]
},
"fsharp-analyzers": {
"version": "0.26.0",
"commands": [
"fsharp-analyzers"
]
}
}
}

40
.editorconfig Normal file
View File

@@ -0,0 +1,40 @@
root=true
[*]
charset=utf-8
trim_trailing_whitespace=true
insert_final_newline=true
indent_style=space
indent_size=4
# ReSharper properties
resharper_xml_indent_size=2
resharper_xml_max_line_length=100
resharper_xml_tab_width=2
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
indent_style=space
indent_size=2
[*.{fs,fsi}]
fsharp_bar_before_discriminated_union_declaration=true
fsharp_space_before_uppercase_invocation=true
fsharp_space_before_class_constructor=true
fsharp_space_before_member=true
fsharp_space_before_colon=true
fsharp_space_before_semicolon=true
fsharp_multiline_bracket_style=aligned
fsharp_newline_between_type_definition_and_members=true
fsharp_align_function_signature_to_indentation=true
fsharp_alternative_long_member_definitions=true
fsharp_multi_line_lambda_closing_newline=true
fsharp_experimental_keep_indent_in_branch=true
fsharp_max_value_binding_width=80
fsharp_max_record_width=0
max_line_length=120
end_of_line=lf
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
indent_style=space
indent_size=2
tab_width=2

16
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
ignore:
# Target the lowest version of FSharp.Core, for max compat
- dependency-name: "FSharp.Core"

169
.github/workflows/dotnet.yaml vendored Normal file
View File

@@ -0,0 +1,169 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json
name: .NET
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
NUGET_XMLDOC_MODE: ''
DOTNET_MULTILEVEL_LOOKUP: 0
jobs:
build:
strategy:
matrix:
config:
- Release
- Debug
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Restore dependencies
run: nix develop --command dotnet restore
- name: Build
run: nix develop --command dotnet build --no-restore --configuration ${{matrix.config}}
- name: Test
run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
analyzers:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Prepare analyzers
run: nix develop --command dotnet restore analyzers/analyzers.fsproj
- name: Build project
run: nix develop --command dotnet build ./Dmarc/Dmarc.fsproj
- name: Run analyzers
run: nix run .#fsharp-analyzers -- --project ./Dmarc/Dmarc.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/*/ --verbosity detailed --report ./analysis.sarif --treat-as-error GRA-STRING-001 GRA-STRING-002 GRA-STRING-003 GRA-UNIONCASE-001 GRA-INTERPOLATED-001 GRA-TYPE-ANNOTATE-001 GRA-VIRTUALCALL-001 GRA-IMMUTABLECOLLECTIONEQUALITY-001 GRA-JSONOPTS-001 GRA-LOGARGFUNCFULLAPP-001 GRA-DISPBEFOREASYNC-001 --exclude-analyzers PartialAppAnalyzer
build-nix:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Build
run: nix build
check-dotnet-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run Fantomas
run: nix run .#fantomas -- --check .
check-nix-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run Alejandra
run: nix develop --command alejandra --check .
linkcheck:
name: Check links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run link checker
run: nix develop --command markdown-link-check README.md
flake-check:
name: Check flake
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Flake check
run: nix flake check
nuget-pack:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Restore dependencies
run: nix develop --command dotnet restore
- name: Build
run: nix develop --command dotnet build --no-restore --configuration Release
- name: Pack
run: nix develop --command dotnet pack --configuration Release
- name: Upload NuGet artifact
uses: actions/upload-artifact@v4
with:
name: nuget-package-plugin
path: Dmarc/bin/Release/Dmarc.*.nupkg
expected-pack:
needs: [nuget-pack]
runs-on: ubuntu-latest
steps:
- name: Download NuGet artifact (plugin)
uses: actions/download-artifact@v4
with:
name: nuget-package-plugin
path: packed-plugin
- name: Check NuGet contents
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
run: if [[ $(find packed-plugin -maxdepth 1 -name 'Dmarc.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
all-required-checks-complete:
needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check, analyzers, nuget-pack, expected-pack]
runs-on: ubuntu-latest
steps:
- run: echo "All required checks complete."

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.sln.DotSettings.user
.DS_Store
result
.analyzerpackages/
analysis.sarif
.direnv/

28
Directory.Build.props Normal file
View File

@@ -0,0 +1,28 @@
<Project>
<PropertyGroup>
<DebugType Condition=" '$(DebugType)' == '' ">embedded</DebugType>
<Deterministic>true</Deterministic>
<NetCoreTargetingPackRoot>[UNDEFINED]</NetCoreTargetingPackRoot>
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DebugType>embedded</DebugType>
<WarnOn>FS3388,FS3559</WarnOn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133" PrivateAssets="all"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com"/>
</ItemGroup>
<!--
SourceLink doesn't support F# deterministic builds out of the box,
so tell SourceLink that our source root is going to be remapped.
-->
<Target Name="MapSourceRoot" BeforeTargets="_GenerateSourceLinkFile" Condition="'$(SourceRootMappedPathsFeatureSupported)' != 'true'">
<ItemGroup>
<SourceRoot Update="@(SourceRoot)">
<MappedPath>Z:\CheckoutRoot\WoofWare.Myriad\</MappedPath>
</SourceRoot>
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dmarc\Dmarc.fsproj" />
</ItemGroup>
</Project>

20
Dmarc.App/Program.fs Normal file
View File

@@ -0,0 +1,20 @@
namespace Dmarc.App
open System.IO
open System.Xml
open Dmarc
module Program =
[<EntryPoint>]
let main argv =
let file =
match argv with
| [| file |] -> file
| _ -> failwith "Call with exactly one arg, the XML file to parse"
use s = File.OpenRead file
let doc = XmlDocument ()
doc.Load s
let feedback = Feedback.ofXml doc.["feedback"]
0

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="EmbeddedResource.fs" />
<Compile Include="TestParse.fs" />
<EmbeddedResource Include="example.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageReference Include="NUnit" Version="4.1.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dmarc\Dmarc.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
namespace Dmarc.Test
open System.IO
open System.Reflection
[<RequireQualifiedAccess>]
module EmbeddedResource =
let read (fileName : string) : Stream =
let assy = Assembly.GetExecutingAssembly ()
let fileName = $"%s{assy.GetName().Name}.%s{fileName}"
let s = assy.GetManifestResourceStream fileName
if isNull s then
let names = assy.GetManifestResourceNames () |> String.concat "\n"
failwith $"Could not find resource %s{fileName}. Available:\n%s{names}"
s

287
Dmarc.Test/TestParse.fs Normal file
View File

@@ -0,0 +1,287 @@
namespace Dmarc.Test
open System
open System.Net
open Dmarc
open NUnit.Framework
open System.Xml
open FsUnitTyped
[<TestFixture>]
module TestParse =
let expectedDateRange =
{
Begin = DateTimeOffset (2024, 05, 26, 00, 00, 00, TimeSpan.Zero)
End = DateTimeOffset (2024, 05, 26, 23, 59, 59, TimeSpan.Zero)
}
[<Test>]
let ``Can parse DateRange`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let dateRangeNode =
doc.["feedback"].FirstChild.ChildNodes
|> Seq.cast<XmlNode>
|> Seq.filter (fun i -> i.Name = "date_range")
|> Seq.exactlyOne
let actual =
if isNull dateRangeNode then
failwith "no version found"
else
DateRange.ofXml dateRangeNode
actual |> shouldEqual expectedDateRange
let expectedReportMetadata =
{
OrgName = Some "google.com"
Email = "noreply-dmarc-support@google.com"
ExtraContactInfo = Uri "https://support.google.com/a/answer/2466580"
ReportId = "12345678901234567890"
DateRange = expectedDateRange
Error = []
}
[<Test>]
let ``Can parse ReportMetadata`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let reportMetadataNode = doc.["feedback"].FirstChild
let actual =
if isNull reportMetadataNode then
failwith "no report metadata node found"
else
reportMetadataNode.Name |> shouldEqual "report_metadata"
ReportMetadata.ofXml reportMetadataNode
actual |> shouldEqual expectedReportMetadata
let expectedPolicyPublished =
{
Domain = "example.com"
DkimAlignment = Some Alignment.Relaxed
SpfAlignment = Some Alignment.Relaxed
Policy = Disposition.None
SubdomainPolicy = Disposition.None
Percentage = 100
FailureOptions = None
}
[<Test>]
let ``Can parse PolicyPublished`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let policyPublishedNode = doc.["feedback"].ChildNodes.[1]
let actual =
if isNull policyPublishedNode then
failwith "no policy published node found"
else
policyPublishedNode.Name |> shouldEqual "policy_published"
PolicyPublished.ofXml policyPublishedNode
actual |> shouldEqual expectedPolicyPublished
let expectedPolicyEvaluated : PolicyEvaluated =
{
Disposition = Disposition.None
Dkim = DmarcResult.Pass
Spf = DmarcResult.Pass
Reason = []
}
[<Test>]
let ``Can parse PolicyEvaluated`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let policyEvaluatedNode = doc.["feedback"].ChildNodes.[2].FirstChild.LastChild
let actual =
if isNull policyEvaluatedNode then
failwith "no policy evaluated node found"
else
policyEvaluatedNode.Name |> shouldEqual "policy_evaluated"
PolicyEvaluated.ofXml policyEvaluatedNode
actual |> shouldEqual expectedPolicyEvaluated
let expectedRow : Row =
{
SourceIp = IPAddress.Parse "192.168.0.1"
Count = 1
Policy = expectedPolicyEvaluated
}
[<Test>]
let ``Can parse Row`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let rowNode = doc.["feedback"].ChildNodes.[2].FirstChild
let actual =
if isNull rowNode then
failwith "no row node found"
else
rowNode.Name |> shouldEqual "row"
Row.ofXml rowNode
actual |> shouldEqual expectedRow
let expectedIdentifier =
{
EnvelopeTo = None
EnvelopeFrom = None
HeaderFrom = "example.com"
}
[<Test>]
let ``Can parse Identifiers`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let idNode = doc.["feedback"].ChildNodes.[2].ChildNodes.[1]
let actual =
if isNull idNode then
failwith "no identifiers node found"
else
idNode.Name |> shouldEqual "identifiers"
Identifier.ofXml idNode
actual |> shouldEqual expectedIdentifier
let expectedDkim : DkimAuthResult =
{
Domain = "example.com"
Result = DkimResult.Pass
Selector = Some "mySelector"
HumanResult = None
}
[<Test>]
let ``Can parse DKIM`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let node = doc.["feedback"].ChildNodes.[2].LastChild.FirstChild
let actual =
if isNull node then
failwith "no dkim node found"
else
node.Name |> shouldEqual "dkim"
DkimAuthResult.ofXml node
actual |> shouldEqual expectedDkim
let expectedSpf : SpfAuthResult =
{
Domain = "example.com"
Scope = None
Result = SpfResult.Pass
}
[<Test>]
let ``Can parse SPF`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let node = doc.["feedback"].ChildNodes.[2].LastChild.LastChild
let actual =
if isNull node then
failwith "no spf node found"
else
node.Name |> shouldEqual "spf"
SpfAuthResult.ofXml node
actual |> shouldEqual expectedSpf
let expectedAuthResults =
{
Dkim = [ expectedDkim ]
SpfHead = expectedSpf
SpfTail = []
}
[<Test>]
let ``Can parse auth results`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let node = doc.["feedback"].LastChild.LastChild
let actual =
if isNull node then
failwith "no spf node found"
else
node.Name |> shouldEqual "auth_results"
AuthResult.ofXml node
actual |> shouldEqual expectedAuthResults
let expectedRecord : Record =
{
Row = expectedRow
Identifiers = expectedIdentifier
AuthResults = expectedAuthResults
}
[<Test>]
let ``Can parse record`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let node = doc.["feedback"].LastChild
let actual =
if isNull node then
failwith "no spf node found"
else
node.Name |> shouldEqual "record"
Record.ofXml node
actual |> shouldEqual expectedRecord
let expectedFeedback =
{
Version = None
ReportMetadata = expectedReportMetadata
PolicyPublished = expectedPolicyPublished
Records = [ expectedRecord ]
}
[<Test>]
let ``Can parse feedback`` () =
use example = EmbeddedResource.read "example.xml"
let doc = XmlDocument ()
doc.Load example
let node = doc.["feedback"]
let actual =
if isNull node then
failwith "no feedback node found"
else
node.Name |> shouldEqual "feedback"
Feedback.ofXml node
actual |> shouldEqual expectedFeedback

47
Dmarc.Test/example.xml Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
<report_id>12345678901234567890</report_id>
<date_range>
<begin>1716681600</begin>
<end>1716767999</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<pct>100</pct>
<np>none</np>
</policy_published>
<record>
<row>
<source_ip>192.168.0.1</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<result>pass</result>
<selector>mySelector</selector>
</dkim>
<spf>
<domain>example.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>

28
Dmarc.sln Normal file
View File

@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc", "Dmarc\Dmarc.fsproj", "{35773DF5-99B3-4624-A57D-A076E8E7DCC9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc.App", "Dmarc.App\Dmarc.App.fsproj", "{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc.Test", "Dmarc.Test\Dmarc.Test.fsproj", "{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Release|Any CPU.Build.0 = Release|Any CPU
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Release|Any CPU.Build.0 = Release|Any CPU
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

13
Dmarc/Dmarc.fsproj Normal file
View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="XmlPatterns.fs" />
<Compile Include="Domain.fs" />
<PackageReference Update="FSharp.Core" Version="6.0.0" />
</ItemGroup>
</Project>

684
Dmarc/Domain.fs Normal file
View File

@@ -0,0 +1,684 @@
namespace Dmarc
open System
open System.Net
open System.Xml
type DateRange =
{
Begin : DateTimeOffset
End : DateTimeOffset
}
static member ofXml (node : XmlNode) : DateRange =
if node.ChildNodes.Count <> 2 then
failwith $"expected exactly two nodes in DateRange, got %i{node.ChildNodes.Count}: %s{node.InnerXml}"
let beginContents, endContents =
match node.FirstChild, node.LastChild with
| OneChildNode "begin" (NoChildrenNode (Int64 beginNode)),
OneChildNode "end" (NoChildrenNode (Int64 endNode))
| OneChildNode "end" (NoChildrenNode (Int64 endNode)),
OneChildNode "begin" (NoChildrenNode (Int64 beginNode)) -> beginNode, endNode
| c1, c2 -> failwith $"Expected a begin and an end node in DateRange, got %s{c1.Name} and %s{c2.Name}"
{
Begin = DateTimeOffset.FromUnixTimeSeconds beginContents
End = DateTimeOffset.FromUnixTimeSeconds endContents
}
type ReportMetadata =
{
OrgName : string option
Email : string
ExtraContactInfo : Uri
ReportId : string
DateRange : DateRange
Error : string list
}
static member ofXml (node : XmlNode) : ReportMetadata =
if not node.HasChildNodes then
failwith "expected report_metadata node to have children, but it did not"
let mutable orgName = None
let mutable email = None
let mutable extraContactInfo = None
let mutable reportId = None
let mutable dateRange = None
let mutable errors = ResizeArray ()
for i in node.ChildNodes do
match i with
| OneChildNode "org_name" (NoChildrenNode v) ->
match orgName with
| None -> orgName <- Some v
| Some v2 -> failwith $"Duplicate values for org_name, %s{v2} and %s{v}"
| OneChildNode "email" (NoChildrenNode v) ->
match email with
| None -> email <- Some v
| Some v2 -> failwith $"Duplicate values for email, %s{v2} and %s{v}"
| OneChildNode "report_id" (NoChildrenNode v) ->
match reportId with
| None -> reportId <- Some v
| Some v2 -> failwith $"Duplicate values for reportId, %s{v2} and %s{v}"
| OneChildNode "extra_contact_info" (NoChildrenNode v) ->
match extraContactInfo with
| None -> extraContactInfo <- Some (Uri v)
| Some v2 -> failwith $"Duplicate values for extra_contact_info, %O{v2} and %s{v}"
| NodeWithChildren "date_range" ->
match dateRange with
| None -> dateRange <- Some (DateRange.ofXml i)
| Some v2 -> failwith $"Duplicate values for date_range, %O{v2} and %s{i.InnerText}"
| OneChildNode "error" (NoChildrenNode v) -> errors.Add v
| _ -> failwith $"Unrecognised node %s{i.Name}: %s{i.InnerText}"
let email =
email |> Option.defaultWith (fun () -> failwith "expected email, got none")
let reportId =
reportId
|> Option.defaultWith (fun () -> failwith "expected report_id, got none")
let extraContactInfo =
extraContactInfo
|> Option.defaultWith (fun () -> failwith "expected extra_contact_info, got none")
let dateRange =
dateRange
|> Option.defaultWith (fun () -> failwith "expected date_range, got none")
{
Error = errors |> Seq.toList
OrgName = orgName
Email = email
ExtraContactInfo = extraContactInfo
ReportId = reportId
DateRange = dateRange
}
type Alignment =
| Relaxed
| Strict
static member ofString (s : string) : Alignment =
match s with
| "r" -> Alignment.Relaxed
| "s" -> Alignment.Strict
| _ -> failwith $"Didn't recognise alignment %s{s}"
[<RequireQualifiedAccess>]
type Disposition =
| None
| Quarantine
| Reject
static member ofString (s : string) : Disposition =
match s with
| "none" -> Disposition.None
| "quarantine" -> Disposition.Quarantine
| "reject" -> Disposition.Reject
| _ -> failwith $"Didn't recognise disposition %s{s}"
type PolicyPublished =
{
Domain : string
DkimAlignment : Alignment option
SpfAlignment : Alignment option
Policy : Disposition
SubdomainPolicy : Disposition
Percentage : int
/// Mandated by RFC-7489 but absent from Google's response.
FailureOptions : string option
}
static member ofXml (node : XmlNode) : PolicyPublished =
if not node.HasChildNodes then
failwith "expected policy_published node to have children, but it did not"
let mutable domain = None
let mutable dkimAlignment = None
let mutable spfAlignment = None
let mutable policy = None
let mutable subdomainPolicy = None
let mutable percentage = None
let mutable failureOptions = None
for i in node.ChildNodes do
match i with
| OneChildNode "domain" (NoChildrenNode v) ->
match domain with
| None -> domain <- Some v
| Some v2 -> failwith $"domain appeared twice, values %s{v2} and %s{v}"
| OneChildNode "adkim" (NoChildrenNode v) ->
match dkimAlignment with
| None -> dkimAlignment <- Some (Alignment.ofString v)
| Some v2 -> failwith $"dkimAlignment appeared twice, values %O{v2} and %s{v}"
| OneChildNode "aspf" (NoChildrenNode v) ->
match spfAlignment with
| None -> spfAlignment <- Some (Alignment.ofString v)
| Some v2 -> failwith $"spfAlignment appeared twice, values %O{v2} and %s{v}"
| OneChildNode "p" (NoChildrenNode v) ->
match policy with
| None -> policy <- Some (Disposition.ofString v)
| Some v2 -> failwith $"policy appeared twice, values %O{v2} and %s{v}"
| OneChildNode "sp" (NoChildrenNode v) ->
match subdomainPolicy with
| None -> subdomainPolicy <- Some (Disposition.ofString v)
| Some v2 -> failwith $"subdomain policy appeared twice, values %O{v2} and %s{v}"
| OneChildNode "pct" (NoChildrenNode (Int v)) ->
match percentage with
| None -> percentage <- Some v
| Some v2 -> failwith $"percentage appeared twice, values %i{v2} and %i{v}"
| OneChildNode "fo" (NoChildrenNode v) ->
match failureOptions with
| None -> failureOptions <- Some v
| Some v2 -> failwith $"failure options appeared twice, values %s{v2} and %s{v}"
| OneChildNode "np" (NoChildrenNode _) ->
// RFC-7489 doesn't mention this but Google returns it
()
| _ -> failwith $"Unrecognised node: %s{i.Name}, %s{i.InnerText}"
let domain =
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
let policy =
policy |> Option.defaultWith (fun () -> failwith "expected policy, got none")
let subdomainPolicy =
subdomainPolicy
|> Option.defaultWith (fun () -> failwith "expected subdomainPolicy, got none")
let percentage =
percentage
|> Option.defaultWith (fun () -> failwith "expected percentage, got none")
{
Domain = domain
DkimAlignment = dkimAlignment
SpfAlignment = spfAlignment
Policy = policy
SubdomainPolicy = subdomainPolicy
Percentage = percentage
FailureOptions = failureOptions
}
type DmarcResult =
| Pass
| Fail
static member ofString (s : string) : DmarcResult =
match s with
| "pass" -> DmarcResult.Pass
| "fail" -> DmarcResult.Fail
| _ -> failwith $"Unrecognised DMARC result: %s{s}"
type PolicyOverride =
| Forwarded
| SampledOut
| TrustedForwarder
| MailingList
| LocalPolicy
| Other
static member ofString (s : string) : PolicyOverride =
match s with
| "forwarded" -> PolicyOverride.Forwarded
| "sampled_out" -> PolicyOverride.SampledOut
| "trusted_forwarder" -> PolicyOverride.TrustedForwarder
| "mailing_list" -> PolicyOverride.MailingList
| "local_policy" -> PolicyOverride.LocalPolicy
| "other" -> PolicyOverride.Other
| _ -> failwith $"unrecognised policy override: %s{s}"
type PolicyOverrideReason =
{
Type : PolicyOverride
Comment : string option
}
static member ofXml (node : XmlNode) : PolicyOverrideReason =
if not node.HasChildNodes then
failwith "expected policy override reason node to have children, but it did not"
let mutable ty = None
let mutable comment = None
for i in node.ChildNodes do
match i with
| OneChildNode "type" (NoChildrenNode v) ->
match ty with
| None -> ty <- Some (PolicyOverride.ofString v)
| Some v2 -> failwith $"type appeared twice, values %O{v2} and %s{v}"
| OneChildNode "comment" (NoChildrenNode v) ->
match comment with
| None -> comment <- Some v
| Some v2 -> failwith $"comment appeared twice, values %s{v2} and %s{v}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let ty =
ty
|> Option.defaultWith (fun () -> failwith "expected policy override, got none")
{
Type = ty
Comment = comment
}
type PolicyEvaluated =
{
Disposition : Disposition
Dkim : DmarcResult
Spf : DmarcResult
Reason : PolicyOverrideReason list
}
static member ofXml (node : XmlNode) : PolicyEvaluated =
if not node.HasChildNodes then
failwith "expected policy evaluation node to have children, but it did not"
let mutable disp = None
let mutable dkim = None
let mutable spf = None
let reason = ResizeArray ()
for i in node.ChildNodes do
match i with
| OneChildNode "disposition" (NoChildrenNode v) ->
match disp with
| None -> disp <- Some (Disposition.ofString v)
| Some v2 -> failwith $"disposition appeared twice, values %O{v2} and %s{v}"
| OneChildNode "dkim" (NoChildrenNode v) ->
match dkim with
| None -> dkim <- Some (DmarcResult.ofString v)
| Some v2 -> failwith $"dkim appeared twice, values %O{v2} and %s{v}"
| OneChildNode "spf" (NoChildrenNode v) ->
match spf with
| None -> spf <- Some (DmarcResult.ofString v)
| Some v2 -> failwith $"spf appeared twice, values %O{v2} and %s{v}"
| OneChildNode "reason" v -> reason.Add (PolicyOverrideReason.ofXml v)
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let spf = spf |> Option.defaultWith (fun () -> failwith "expected spf, got none")
let dkim = dkim |> Option.defaultWith (fun () -> failwith "expected dkim, got none")
let disp =
disp |> Option.defaultWith (fun () -> failwith "expected disposition, got none")
{
Disposition = disp
Dkim = dkim
Spf = spf
Reason = reason |> Seq.toList
}
type Row =
{
SourceIp : IPAddress
Count : int
Policy : PolicyEvaluated
}
static member ofXml (node : XmlNode) : Row =
if not node.HasChildNodes then
failwith "expected policy evaluation node to have children, but it did not"
let mutable source = None
let mutable count = None
let mutable policy = None
for i in node.ChildNodes do
match i with
| OneChildNode "source_ip" (NoChildrenNode v) ->
match source with
| None -> source <- Some (IPAddress.Parse v)
| Some v2 -> failwith $"source appeared twice, values %O{v2} and %s{v}"
| OneChildNode "count" (NoChildrenNode (Int v)) ->
match count with
| None -> count <- Some v
| Some v2 -> failwith $"count appeared twice, values %i{v2} and %i{v}"
| NodeWithChildren "policy_evaluated" ->
match policy with
| None -> policy <- Some (PolicyEvaluated.ofXml i)
| Some v2 -> failwith $"policy_evaluated appeared twice, values %O{v2} and %s{i.InnerText}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let source =
source |> Option.defaultWith (fun () -> failwith "expected source, got none")
let count =
count |> Option.defaultWith (fun () -> failwith "expected count, got none")
let policy =
policy
|> Option.defaultWith (fun () -> failwith "expected policy_evaluated, got none")
{
SourceIp = source
Count = count
Policy = policy
}
type Identifier =
{
EnvelopeTo : string option
/// According to the RFC, this is required, but Google doesn't return it
EnvelopeFrom : string option
HeaderFrom : string
}
static member ofXml (node : XmlNode) : Identifier =
if not node.HasChildNodes then
failwith "expected identifiers node to have children, but it did not"
let mutable envelopeTo = None
let mutable envelopeFrom = None
let mutable headerFrom = None
for i in node.ChildNodes do
match i with
| OneChildNode "header_from" (NoChildrenNode v) ->
match headerFrom with
| None -> headerFrom <- Some v
| Some v2 -> failwith $"header_from appeared twice, values %O{v2} and %s{v}"
| OneChildNode "envelope_to" (NoChildrenNode v) ->
match envelopeTo with
| None -> envelopeTo <- Some v
| Some v2 -> failwith $"envelope_to appeared twice, values %s{v2} and %s{v}"
| OneChildNode "envelope_from" (NoChildrenNode v) ->
match envelopeFrom with
| None -> envelopeFrom <- Some v
| Some v2 -> failwith $"envelope_from appeared twice, values %O{v2} and %s{v}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let headerFrom =
headerFrom
|> Option.defaultWith (fun () -> failwith "expected header_from, got none")
{
HeaderFrom = headerFrom
EnvelopeFrom = envelopeFrom
EnvelopeTo = envelopeTo
}
[<RequireQualifiedAccess>]
type DkimResult =
| None
| Pass
| Fail
| Policy
| Neutral
| TempError
| PermError
static member ofString (s : string) : DkimResult =
match s with
| "none" -> DkimResult.None
| "pass" -> DkimResult.Pass
| "fail" -> DkimResult.Fail
| "policy" -> DkimResult.Policy
| "neutral" -> DkimResult.Neutral
| "temperror" -> DkimResult.TempError
| "permerror" -> DkimResult.PermError
| _ -> failwith $"Unrecognised DKIM result: %s{s}"
type DkimAuthResult =
{
Domain : string
Selector : string option
Result : DkimResult
HumanResult : string option
}
static member ofXml (node : XmlNode) : DkimAuthResult =
if not node.HasChildNodes then
failwith "expected dkim auth result node to have children, but it did not"
let mutable domain = None
let mutable selector = None
let mutable result = None
let mutable humanResult = None
for i in node.ChildNodes do
match i with
| OneChildNode "domain" (NoChildrenNode v) ->
match domain with
| None -> domain <- Some v
| Some v2 -> failwith $"domain appeared twice, values %O{v2} and %s{v}"
| OneChildNode "selector" (NoChildrenNode v) ->
match selector with
| None -> selector <- Some v
| Some v2 -> failwith $"selctor appeared twice, values %s{v2} and %s{v}"
| OneChildNode "result" (NoChildrenNode v) ->
match result with
| None -> result <- Some (DkimResult.ofString v)
| Some v2 -> failwith $"result appeared twice, values %O{v2} and %s{v}"
| OneChildNode "human_result" (NoChildrenNode v) ->
match humanResult with
| None -> humanResult <- Some v
| Some v2 -> failwith $"human_result appeared twice, values %s{v2} and %s{v}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let domain =
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
let result =
result |> Option.defaultWith (fun () -> failwith "expected result, got none")
{
Domain = domain
Selector = selector
Result = result
HumanResult = humanResult
}
type SpfDomainScope =
| Helo
| Mfrom
static member ofString (s : string) : SpfDomainScope =
match s with
| "helo" -> SpfDomainScope.Helo
| "mfrom" -> SpfDomainScope.Mfrom
| _ -> failwith $"Unrecognised SPF domain scope: %s{s}"
[<RequireQualifiedAccess>]
type SpfResult =
| None
| Neutral
| Pass
| Fail
| SoftFail
| TempError
| PermError
static member ofString (s : string) : SpfResult =
match s with
| "none" -> SpfResult.None
| "neutral" -> SpfResult.Neutral
| "pass" -> SpfResult.Pass
| "fail" -> SpfResult.Fail
| "softfail" -> SpfResult.SoftFail
| "unknown"
| "temperror" -> SpfResult.TempError
| "error"
| "permerror" -> SpfResult.PermError
| _ -> failwith $"Unrecognised SPF result: %s{s}"
type SpfAuthResult =
{
Domain : string
/// Mandatory according to the RFC, but not supplied by Google
Scope : SpfDomainScope option
Result : SpfResult
}
static member ofXml (node : XmlNode) : SpfAuthResult =
if not node.HasChildNodes then
failwith "expected spf auth result to have children, but it did not"
let mutable domain = None
let mutable scope = None
let mutable result = None
for i in node.ChildNodes do
match i with
| OneChildNode "domain" (NoChildrenNode v) ->
match domain with
| None -> domain <- Some v
| Some v2 -> failwith $"domain appeared twice, values %s{v2} and %s{v}"
| OneChildNode "result" (NoChildrenNode v) ->
match result with
| None -> result <- Some (SpfResult.ofString v)
| Some v2 -> failwith $"result appeared twice, values %O{v2} and %s{v}"
| OneChildNode "scope" (NoChildrenNode v) ->
match scope with
| None -> scope <- Some (SpfDomainScope.ofString v)
| Some v2 -> failwith $"human_result appeared twice, values %O{v2} and %s{v}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let domain =
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
let result =
result |> Option.defaultWith (fun () -> failwith "expected result, got none")
{
Domain = domain
Scope = scope
Result = result
}
type AuthResult =
{
Dkim : DkimAuthResult list
SpfHead : SpfAuthResult
SpfTail : SpfAuthResult list
}
static member ofXml (node : XmlNode) : AuthResult =
if not node.HasChildNodes then
failwith "expected auth result to have children, but it did not"
let dkim = ResizeArray ()
let mutable spfHead = None
let spfTail = ResizeArray ()
for i in node.ChildNodes do
match i with
| NodeWithChildren "dkim" -> dkim.Add (DkimAuthResult.ofXml i)
| NodeWithChildren "spf" ->
let v = SpfAuthResult.ofXml i
match spfHead with
| None -> spfHead <- Some v
| Some _ -> spfTail.Add v
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let spfHead =
spfHead
|> Option.defaultWith (fun () -> failwith "expected spf to have at least one element, got none")
{
Dkim = dkim |> Seq.toList
SpfHead = spfHead
SpfTail = spfTail |> Seq.toList
}
type Record =
{
Row : Row
Identifiers : Identifier
AuthResults : AuthResult
}
static member ofXml (node : XmlNode) : Record =
if not node.HasChildNodes then
failwith "expected record result to have children, but it did not"
let mutable row = None
let mutable identifiers = None
let mutable authResult = None
for i in node.ChildNodes do
match i with
| NodeWithChildren "auth_results" ->
match authResult with
| None -> authResult <- Some (AuthResult.ofXml i)
| Some v2 -> failwith $"auth_results appeared twice, values %O{v2} and %s{i.InnerText}"
| NodeWithChildren "row" ->
match row with
| None -> row <- Some (Row.ofXml i)
| Some v2 -> failwith $"row appeared twice, values %O{v2} and %s{i.InnerText}"
| NodeWithChildren "identifiers" ->
match identifiers with
| None -> identifiers <- Some (Identifier.ofXml i)
| Some v2 -> failwith $"identifiers appeared twice, values %O{v2} and %s{i.InnerText}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let row = row |> Option.defaultWith (fun () -> failwith "expected row, got none")
let identifiers =
identifiers
|> Option.defaultWith (fun () -> failwith "expected identifiers, got none")
let authResult =
authResult
|> Option.defaultWith (fun () -> failwith "expected auth_results, got none")
{
Row = row
Identifiers = identifiers
AuthResults = authResult
}
type Feedback =
{
/// strictly speaking a decimal; also mandatory according to the RFC but not
/// supplied by Google
Version : string option
ReportMetadata : ReportMetadata
PolicyPublished : PolicyPublished
Records : Record list
}
static member ofXml (node : XmlNode) : Feedback =
if not node.HasChildNodes then
failwith "expected record result to have children, but it did not"
let mutable version = None
let mutable reportMetadata = None
let mutable policyPublished = None
let records = ResizeArray ()
for i in node.ChildNodes do
match i with
| NodeWithChildren "record" -> records.Add (Record.ofXml i)
| NodeWithChildren "policy_published" ->
match policyPublished with
| None -> policyPublished <- Some (PolicyPublished.ofXml i)
| Some v2 -> failwith $"policy_published appeared twice, values %O{v2} and %s{i.InnerText}"
| NodeWithChildren "report_metadata" ->
match reportMetadata with
| None -> reportMetadata <- Some (ReportMetadata.ofXml i)
| Some v2 -> failwith $"report_metadata appeared twice, values %O{v2} and %s{i.InnerText}"
| OneChildNode "version" (NoChildrenNode v) ->
match version with
| None -> version <- Some v
| Some v2 -> failwith $"version appeared twice, values %O{v2} and %s{v}"
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
let policyPublished =
policyPublished
|> Option.defaultWith (fun () -> failwith "expected policy_published, got none")
let reportMetadata =
reportMetadata
|> Option.defaultWith (fun () -> failwith "expected report_metadata, got none")
{
Records = records |> Seq.toList
PolicyPublished = policyPublished
ReportMetadata = reportMetadata
Version = version
}

34
Dmarc/XmlPatterns.fs Normal file
View File

@@ -0,0 +1,34 @@
namespace Dmarc
open System.Xml
[<AutoOpen>]
module internal XmlPatterns =
[<return : Struct>]
let (|NodeWithChildren|_|) (expectedName : string) (node : XmlNode) : unit voption =
if node.Name = expectedName && node.HasChildNodes then
ValueSome ()
else
ValueNone
let (|OneChildNode|_|) (expectedName : string) (node : XmlNode) : XmlNode option =
if node.Name = expectedName && node.HasChildNodes && node.ChildNodes.Count = 1 then
Some (node.FirstChild)
else
None
let (|NoChildrenNode|_|) (node : XmlNode) : string option =
if node.HasChildNodes then None else Some node.Value
[<return : Struct>]
let (|Int64|_|) (s : string) : int64 voption =
match System.Int64.TryParse s with
| false, _ -> ValueNone
| true, v -> ValueSome v
[<return : Struct>]
let (|Int|_|) (s : string) : int voption =
match System.Int32.TryParse s with
| false, _ -> ValueNone
| true, v -> ValueSome v

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Patrick Stevens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# DMARC parser for F#
Based on the [RFC](https://datatracker.ietf.org/doc/html/rfc7489#appendix-C), with concessions to match what Google actually reported to me in its own reports.

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.Build.NoTargets/1.0.80"> <!-- This is not a project we want to build. -->
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
<RestorePackagesPath>../.analyzerpackages/</RestorePackagesPath>
<TargetFramework>net6.0</TargetFramework>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages> <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose.-->
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.10.0]" />
</ItemGroup>
</Project>

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717112898,
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

67
flake.nix Normal file
View File

@@ -0,0 +1,67 @@
{
description = "DKIM/DMARC parsing";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = {
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
pname = "Dmarc-dotnet";
dotnet-sdk = pkgs.dotnet-sdk_8;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
version = "0.1";
dotnetTool = dllOverride: toolName: toolVersion: sha256:
pkgs.stdenvNoCC.mkDerivation rec {
name = toolName;
version = toolVersion;
nativeBuildInputs = [pkgs.makeWrapper];
src = pkgs.fetchNuGet {
pname = name;
version = version;
sha256 = sha256;
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
};
installPhase = let
dll =
if isNull dllOverride
then name
else dllOverride;
in ''
runHook preInstall
mkdir -p "$out/lib"
cp -r ./bin/* "$out/lib"
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${dll}.dll"
runHook postInstall
'';
};
in {
packages = {
fantomas = dotnetTool null "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version (builtins.head (builtins.filter (elem: elem.pname == "fantomas") ((import ./nix/deps.nix) {fetchNuGet = x: x;}))).sha256;
fsharp-analyzers = dotnetTool "FSharp.Analyzers.Cli" "fsharp-analyzers" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fsharp-analyzers.version (builtins.head (builtins.filter (elem: elem.pname == "fsharp-analyzers") ((import ./nix/deps.nix) {fetchNuGet = x: x;}))).sha256;
default = pkgs.buildDotnetModule {
inherit pname version dotnet-sdk dotnet-runtime;
name = "Dmarc-dotnet";
src = ./.;
projectFile = "./Dmarc.App/Dmarc.App.fsproj";
testProjectFile = "./Dmarc.Test/Dmarc.Test.fsproj";
nugetDeps = ./nix/deps.nix; # `nix build .#default.passthru.fetch-deps && ./result` and put the result here
doCheck = true;
};
};
devShell = pkgs.mkShell {
buildInputs = [dotnet-sdk];
packages = [
pkgs.alejandra
pkgs.nodePackages.markdown-link-check
pkgs.shellcheck
];
};
});
}

99
nix/deps.nix Normal file
View File

@@ -0,0 +1,99 @@
# This file was automatically generated by passthru.fetch-deps.
# Please dont edit it manually, your changes might get overwritten!
{fetchNuGet}: [
(fetchNuGet {
pname = "fantomas";
version = "6.3.4";
sha256 = "1bf57pzvl0i1bgic2vf08mqlzzbd5kys1ip9klrhm4f155ksm9fm";
})
(fetchNuGet {
pname = "fsharp-analyzers";
version = "0.26.0";
sha256 = "0xgv5kvbwfdvcp6s8x7xagbbi4s3mqa4ixni6pazqvyflbgnah7b";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "6.0.0";
sha256 = "1hjhvr39c1vpgrdmf8xln5q86424fqkvy9nirkr29vl2461d2039";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "8.0.300";
sha256 = "158xxr9hnhz2ibyzzp2d249angvxfc58ifflm4g3hz8qx9zxaq04";
})
(fetchNuGet {
pname = "FsUnit";
version = "6.0.0";
sha256 = "18q3p0z155znwj1l0qq3vq9nh9wl2i4mlfx4pmrnia4czr0xdkmb";
})
(fetchNuGet {
pname = "Microsoft.Build.Tasks.Git";
version = "8.0.0";
sha256 = "0055f69q3hbagqp8gl3nk0vfn4qyqyxsxyy7pd0g7wm3z28byzmx";
})
(fetchNuGet {
pname = "Microsoft.CodeCoverage";
version = "17.10.0";
sha256 = "0s0v7jmrq85n356xv7zixvwa4z94fszjcr5vll8x4im1a2lp00f9";
})
(fetchNuGet {
pname = "Microsoft.NET.Test.Sdk";
version = "17.10.0";
sha256 = "13g8fwl09li8fc71nk13dgkb7gahd4qhamyg2xby7am63nlchhdf";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Platforms";
version = "1.1.0";
sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm";
})
(fetchNuGet {
pname = "Microsoft.SourceLink.Common";
version = "8.0.0";
sha256 = "0xrr8yd34ij7dqnyddkp2awfmf9qn3c89xmw2f3npaa4wnajmx81";
})
(fetchNuGet {
pname = "Microsoft.SourceLink.GitHub";
version = "8.0.0";
sha256 = "1gdx7n45wwia3yvang3ls92sk3wrymqcx9p349j8wba2lyjf9m44";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.ObjectModel";
version = "17.10.0";
sha256 = "07j69cw8r39533w4p39mnj00kahazz38760in3jfc45kmlcdb26x";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.TestHost";
version = "17.10.0";
sha256 = "1bl471s7fx9jycr0cc8rylwf34mrvlg9qn1an6l86nisavfcyb7v";
})
(fetchNuGet {
pname = "Nerdbank.GitVersioning";
version = "3.6.133";
sha256 = "1cdw8krvsnx0n34f7fm5hiiy7bs6h3asvncqcikc0g46l50w2j80";
})
(fetchNuGet {
pname = "NETStandard.Library";
version = "2.0.3";
sha256 = "1fn9fxppfcg4jgypp2pmrpr6awl3qz1xmnri0cygpkwvyx27df1y";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "13.0.1";
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
})
(fetchNuGet {
pname = "NUnit";
version = "4.1.0";
sha256 = "0fj6xwgqaxq3mrai86bklclfmjkzf038mrslwfqf4ignaz9f7g5j";
})
(fetchNuGet {
pname = "NUnit3TestAdapter";
version = "4.5.0";
sha256 = "1srx1629s0k1kmf02nmz251q07vj6pv58mdafcr5dr0bbn1fh78i";
})
(fetchNuGet {
pname = "System.Reflection.Metadata";
version = "1.6.0";
sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
})
]