mirror of
https://github.com/Smaug123/dmarc-fsharp
synced 2025-10-05 07:28:41 +00:00
Initial commit
This commit is contained in:
18
.config/dotnet-tools.json
Normal file
18
.config/dotnet-tools.json
Normal 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
40
.editorconfig
Normal 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
16
.github/dependabot.yml
vendored
Normal 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
169
.github/workflows/dotnet.yaml
vendored
Normal 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
12
.gitignore
vendored
Normal 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
28
Directory.Build.props
Normal 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>
|
16
Dmarc.App/Dmarc.App.fsproj
Normal file
16
Dmarc.App/Dmarc.App.fsproj
Normal 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
20
Dmarc.App/Program.fs
Normal 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
|
27
Dmarc.Test/Dmarc.Test.fsproj
Normal file
27
Dmarc.Test/Dmarc.Test.fsproj
Normal 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>
|
17
Dmarc.Test/EmbeddedResource.fs
Normal file
17
Dmarc.Test/EmbeddedResource.fs
Normal 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
287
Dmarc.Test/TestParse.fs
Normal 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
47
Dmarc.Test/example.xml
Normal 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
28
Dmarc.sln
Normal 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
13
Dmarc/Dmarc.fsproj
Normal 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
684
Dmarc/Domain.fs
Normal 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
34
Dmarc/XmlPatterns.fs
Normal 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
21
LICENSE
Normal 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
3
README.md
Normal 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.
|
16
analyzers/analyzers.fsproj
Normal file
16
analyzers/analyzers.fsproj
Normal 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
61
flake.lock
generated
Normal 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
67
flake.nix
Normal 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
99
nix/deps.nix
Normal 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";
|
||||
})
|
||||
]
|
Reference in New Issue
Block a user