diff --git a/Apps/MispConnectorApp/App.cs b/Apps/MispConnectorApp/App.cs
new file mode 100644
index 00000000..70e5fe16
--- /dev/null
+++ b/Apps/MispConnectorApp/App.cs
@@ -0,0 +1,591 @@
+/*
+Technitium DNS Server
+Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
+Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+using DnsServerCore.ApplicationCommon;
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using TechnitiumLibrary.Net.Dns;
+using TechnitiumLibrary.Net.Dns.EDnsOptions;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+using TechnitiumLibrary.Net.Http.Client;
+
+namespace MispConnector
+{
+ public sealed class App : IDnsApplication, IDnsRequestBlockingHandler
+ {
+ #region variables
+
+ string _domainCacheFilePath;
+ Config _config;
+ IDnsServer _dnsServer;
+ FrozenSet _domainBlocklist = FrozenSet.Empty;
+ HttpClient _httpClient;
+
+ Uri _mispApiUrl;
+
+ DnsSOARecordData _soaRecord;
+ TimeSpan _updateInterval;
+ Task _updateLoopTask;
+
+ CancellationTokenSource _appShutdownCts;
+ #endregion variables
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ _appShutdownCts?.Cancel();
+ try
+ {
+ if (_updateLoopTask != null)
+ {
+ _ = Task.WhenAny(_updateLoopTask, Task.Delay(TimeSpan.FromSeconds(2))).GetAwaiter().GetResult();
+ }
+ }
+ catch
+ {
+ }
+ finally
+ {
+ _appShutdownCts?.Dispose();
+ _httpClient?.Dispose();
+ }
+ }
+
+ #endregion IDisposable
+
+ #region public
+
+ public async Task InitializeAsync(IDnsServer dnsServer, string config)
+ {
+ _dnsServer = dnsServer;
+ try
+ {
+ _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, 60);
+
+ JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ _config = JsonSerializer.Deserialize(config, options);
+
+ Validator.ValidateObject(_config, new ValidationContext(_config), validateAllProperties: true);
+
+ string configDir = _dnsServer.ApplicationFolder;
+ Directory.CreateDirectory(configDir);
+ _domainCacheFilePath = Path.Combine(configDir, "misp_domain_cache.txt");
+
+ _updateInterval = ParseUpdateInterval(_config.UpdateInterval);
+
+ Uri mispServerUrl = new Uri(_config.MispServerUrl);
+ _mispApiUrl = new Uri(mispServerUrl, "/attributes/restSearch");
+ _httpClient = CreateHttpClient(mispServerUrl, _config.DisableTlsValidation);
+
+ await LoadBlocklistFromCacheAsync();
+ _appShutdownCts = new CancellationTokenSource();
+
+ // We do not await this, as it's designed to run for the lifetime of the app.
+ _updateLoopTask = StartUpdateLoopAsync(_appShutdownCts.Token);
+ Task _ = _updateLoopTask.ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ _dnsServer.WriteLog($"FATAL: Update loop terminated unexpectedly: {t.Exception?.GetBaseException().Message}");
+ _dnsServer.WriteLog(t.Exception);
+ }
+ }, TaskContinuationOptions.OnlyOnFaulted);
+ }
+ catch (Exception ex)
+ {
+ _dnsServer.WriteLog($"FATAL: MISP Connector failed to initialize. Check configuration. Error: {ex.Message}");
+ _dnsServer.WriteLog(ex);
+ }
+ }
+
+ public Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP)
+ {
+ return Task.FromResult(false);
+ }
+
+ public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP)
+ {
+ if (_config?.EnableBlocking != true)
+ {
+ return Task.FromResult(null);
+ }
+
+ DnsQuestionRecord question = request.Question[0];
+ bool domainBlocked = IsDomainBlocked(question.Name, out string blockedDomain);
+ if (!domainBlocked)
+ {
+ return Task.FromResult(null);
+ }
+
+ string blockingReport = $"source=misp-connector;domain={blockedDomain}";
+
+ EDnsOption[] options = null;
+ if (_config.AddExtendedDnsError && request.EDNS is not null)
+ {
+ options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, string.Empty)) };
+ }
+
+ if (_config.AllowTxtBlockingReport && question.Type == DnsResourceRecordType.TXT)
+ {
+ DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(string.Empty)) };
+ return Task.FromResult(new DnsDatagram(
+ ID: request.Identifier,
+ isResponse: true,
+ OPCODE: DnsOpcode.StandardQuery,
+ authoritativeAnswer: false,
+ truncation: false,
+ recursionDesired: request.RecursionDesired,
+ recursionAvailable: true,
+ authenticData: false,
+ checkingDisabled: false,
+ RCODE: DnsResponseCode.NoError,
+ question: request.Question,
+ answer: answer,
+ authority: null,
+ additional: null,
+ udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize,
+ ednsFlags: EDnsHeaderFlags.None,
+ options: options
+ ));
+ }
+
+ DnsResourceRecord[] authority = { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) };
+ return Task.FromResult(new DnsDatagram(
+ ID: request.Identifier,
+ isResponse: true,
+ OPCODE: DnsOpcode.StandardQuery,
+ authoritativeAnswer: true,
+ truncation: false,
+ recursionDesired: request.RecursionDesired,
+ recursionAvailable: true,
+ authenticData: false,
+ checkingDisabled: false,
+ RCODE: DnsResponseCode.NxDomain,
+ question: request.Question,
+ answer: null,
+ authority: authority,
+ additional: null,
+ udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize,
+ ednsFlags: EDnsHeaderFlags.None,
+ options: options
+ ));
+ }
+
+ #endregion public
+
+ #region private
+ private async Task StartUpdateLoopAsync(CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(5, 30)), cancellationToken);
+ using (PeriodicTimer timer = new PeriodicTimer(_updateInterval))
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await UpdateIocsAsync(cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ _dnsServer.WriteLog("Update loop is shutting down gracefully.");
+ break;
+ }
+ catch (Exception ex)
+ {
+ _dnsServer.WriteLog($"FATAL: The MispConnector update task failed unexpectedly. Error: {ex.Message}");
+ _dnsServer.WriteLog(ex);
+ }
+
+ await timer.WaitForNextTickAsync(cancellationToken);
+ }
+ }
+ }
+ private static TimeSpan ParseUpdateInterval(string interval)
+ {
+ if (string.IsNullOrWhiteSpace(interval) || interval.Length < 2)
+ {
+ throw new FormatException("Update interval is not in a valid format (e.g., '60m', '2h', '7d').");
+ }
+
+ string unit = interval.Substring(interval.Length - 1).ToLowerInvariant();
+ string valueString = interval.Substring(0, interval.Length - 1);
+
+ if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) || value <= 0)
+ {
+ throw new FormatException($"Invalid numeric value '{valueString}' in update interval.");
+ }
+
+ switch (unit)
+ {
+ case "m":
+ return TimeSpan.FromMinutes(value);
+
+ case "h":
+ return TimeSpan.FromHours(value);
+
+ case "d":
+ return TimeSpan.FromDays(value);
+
+ default:
+ throw new FormatException($"Invalid unit '{unit}' in update interval. Allowed units are 'm', 'h', 'd'.");
+ }
+ }
+
+ private async Task CheckTcpPortAsync(Uri serverUri, CancellationToken cancellationToken)
+ {
+ string host = serverUri.DnsSafeHost;
+ int port = serverUri.Port;
+ TimeSpan timeout = TimeSpan.FromSeconds(5);
+
+ _dnsServer.WriteLog($"Performing pre-flight TCP check for {host}:{port} with a {timeout.TotalSeconds}-second timeout...");
+
+ try
+ {
+ using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(timeout).Token))
+ using (TcpClient client = new TcpClient())
+ {
+ await client.ConnectAsync(host, port, cts.Token);
+ }
+
+ _dnsServer.WriteLog($"Pre-flight TCP check successful for {host}:{port}.");
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ _dnsServer.WriteLog($"ERROR: Pre-flight TCP check failed: Connection to {host}:{port} timed out after {timeout.TotalSeconds} seconds. Check firewall rules or network route.");
+ return false;
+ }
+ catch (SocketException ex)
+ {
+ _dnsServer.WriteLog($"ERROR: Pre-flight TCP check failed: A network error occurred for {host}:{port}. Error: {ex.Message}");
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _dnsServer.WriteLog($"ERROR: An unexpected error occurred during the pre-flight TCP check for {host}:{port}. Error: {ex.Message}");
+ return false;
+ }
+ }
+
+ private HttpClient CreateHttpClient(Uri serverUrl, bool disableTlsValidation)
+ {
+ SocketsHttpHandler handler = new SocketsHttpHandler
+ {
+ Proxy = _dnsServer.Proxy,
+ UseProxy = _dnsServer.Proxy != null,
+ SslOptions = new SslClientAuthenticationOptions(),
+ ConnectTimeout = TimeSpan.FromSeconds(15)
+ };
+
+ if (disableTlsValidation)
+ {
+ handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
+ _dnsServer.WriteLog($"WARNING: TLS certificate validation is DISABLED for MISP server: {serverUrl}");
+ }
+
+ return new HttpClient(new HttpClientNetworkHandler(handler, _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default, _dnsServer));
+ }
+
+ private async Task> FetchIocFromMispAsync(CancellationToken cancellationToken)
+ {
+ HashSet iocSet = new HashSet(StringComparer.OrdinalIgnoreCase);
+ int page = 1;
+ int limit = _config.PaginationLimit;
+ bool hasMorePages = true;
+
+ _dnsServer.WriteLog($"Starting paginated fetch from MISP API with a page size of {limit}...");
+ const int maxRetries = 3;
+
+ while (hasMorePages)
+ {
+ int attempt = 0;
+ MispResponse mispResponse = null;
+
+ while (attempt < maxRetries)
+ {
+ attempt++;
+ try
+ {
+ MispRequestBody requestBody = new MispRequestBody
+ {
+ Type = "domain",
+ To_ids = true,
+ Deleted = false,
+ Last = _config.MaxIocAge,
+ Limit = limit,
+ Page = page
+ };
+ StringContent requestContent = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
+
+ using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _mispApiUrl) { Content = requestContent };
+ request.Headers.Add("Authorization", _config.MispApiKey);
+ request.Headers.Add("Accept", "application/json");
+
+ _dnsServer.WriteLog($"Fetching page {page}, attempt {attempt}/{maxRetries}...");
+ using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ // This is a definitive failure from the server (e.g., 403, 500).
+ // We should not retry this. Abort immediately.
+ string errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
+ throw new HttpRequestException($"MISP API returned a non-success status code: {(int)response.StatusCode}. Body: {errorBody}", null, response.StatusCode);
+ }
+
+ await using (Stream responseStream = await response.Content.ReadAsStreamAsync(cancellationToken))
+ mispResponse = await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken);
+
+ break;
+ }
+ catch (Exception ex) when (ex is HttpRequestException || ex is SocketException || ex is OperationCanceledException)
+ {
+ // These are likely transient network errors, so we should retry.
+ _dnsServer.WriteLog($"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}");
+ if (attempt < maxRetries)
+ {
+ TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000));
+ _dnsServer.WriteLog($"Waiting for {delay.TotalSeconds:F1} seconds before retrying...");
+ await Task.Delay(delay, cancellationToken);
+ }
+ else
+ {
+ // All retries have failed for this page.
+ _dnsServer.WriteLog($"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle.");
+ throw;
+ }
+ }
+ }
+
+ List attributes = mispResponse?.Response?.Attribute;
+ if (attributes == null || attributes.Count == 0)
+ {
+ hasMorePages = false;
+ continue;
+ }
+
+ foreach (MispAttribute attribute in attributes)
+ {
+ string ioc = attribute.Value?.Trim().ToLowerInvariant();
+ if (!string.IsNullOrEmpty(ioc))
+ {
+ if (DnsClient.IsDomainNameValid(ioc))
+ {
+ iocSet.Add(ioc);
+ }
+ }
+ }
+
+ // Assumption: If we received fewer items than our limit, it must be the last page.
+ if (attributes.Count < limit)
+ {
+ hasMorePages = false;
+ }
+ else
+ {
+ page++;
+ }
+ }
+
+ _dnsServer.WriteLog($"Finished paginated fetch. Freezing {iocSet.Count} IOCs for optimal read performance...");
+ return iocSet;
+ }
+
+ private bool IsDomainBlocked(string domain, out string foundZone)
+ {
+ FrozenSet currentBlocklist = _domainBlocklist;
+
+ ReadOnlySpan currentSpan = domain.AsSpan();
+
+ while (true)
+ {
+ // To look up in a HashSet, we must provide a string.
+ string key = new string(currentSpan);
+ if (currentBlocklist.TryGetValue(key, out foundZone))
+ {
+ return true;
+ }
+
+ int dotIndex = currentSpan.IndexOf('.');
+ if (dotIndex == -1)
+ {
+ break; // No more parent domains.
+ }
+
+ // Slice to the parent domain view. No allocation here.
+ currentSpan = currentSpan.Slice(dotIndex + 1);
+ }
+
+ foundZone = null;
+ return false;
+ }
+
+ private async Task LoadBlocklistFromCacheAsync()
+ {
+ if (File.Exists(_domainCacheFilePath))
+ {
+ try
+ {
+ FrozenSet domains = (await File.ReadAllLinesAsync(_domainCacheFilePath)).ToHashSet(StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase);
+ Interlocked.Exchange(ref _domainBlocklist, domains);
+ _dnsServer.WriteLog($"MISP Connector: Loaded {domains.Count} domains from cache.");
+ }
+ catch (IOException ex)
+ {
+ _dnsServer.WriteLog($"ERROR: Failed to read cache file '{_domainCacheFilePath}'. Error: {ex.Message}");
+ }
+ }
+ }
+
+ private async Task UpdateIocsAsync(CancellationToken cancellationToken)
+ {
+ if (!await CheckTcpPortAsync(new Uri(_config.MispServerUrl), cancellationToken))
+ {
+ return;
+ }
+
+ _dnsServer.WriteLog("MISP Connector: Starting IOC update...");
+
+ HashSet tmpDomains = await FetchIocFromMispAsync(cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+ FrozenSet domains = tmpDomains.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
+
+ if (!domains.SetEquals(_domainBlocklist))
+ {
+ await WriteIocsToCacheAsync(domains, cancellationToken);
+ Interlocked.Exchange(ref _domainBlocklist, domains);
+ _dnsServer.WriteLog($"MISP Connector: Successfully updated blocklist with {domains.Count} domains.");
+ }
+ else
+ {
+ _dnsServer.WriteLog("MISP data has not changed. No update to blocklist or cache is necessary.");
+ }
+ }
+
+ private async Task WriteIocsToCacheAsync(FrozenSet iocs, CancellationToken cancellationToken)
+ {
+ string tempPath = _domainCacheFilePath + ".tmp";
+ await File.WriteAllLinesAsync(tempPath, iocs, cancellationToken);
+ File.Move(tempPath, _domainCacheFilePath, true);
+ }
+
+ #endregion private
+
+ #region properties
+
+ public string Description
+ {
+ get
+ {
+ return "A focused connector that imports domain IOCs from a MISP server to block malicious domains using direct REST API calls.";
+ }
+ }
+
+ #endregion properties
+
+ private class Config
+ {
+ [JsonPropertyName("addExtendedDnsError")]
+ public bool AddExtendedDnsError { get; set; } = true;
+
+ [JsonPropertyName("allowTxtBlockingReport")]
+ public bool AllowTxtBlockingReport { get; set; } = true;
+
+ [JsonPropertyName("disableTlsValidation")]
+ public bool DisableTlsValidation { get; set; } = false;
+
+ [JsonPropertyName("enableBlocking")]
+ public bool EnableBlocking { get; set; } = true;
+ [JsonPropertyName("maxIocAge")]
+ [Required(ErrorMessage = "maxIocAge is a required configuration property.")]
+ [RegularExpression(@"^\d+[mhd]$", ErrorMessage = "Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').", MatchTimeoutInMilliseconds = 3000)]
+ public string MaxIocAge { get; set; }
+
+ [JsonPropertyName("mispApiKey")]
+ [Required(ErrorMessage = "mispApiKey is a required configuration property.")]
+ [MinLength(1, ErrorMessage = "mispApiKey cannot be empty.")]
+ public string MispApiKey { get; set; }
+
+ [JsonPropertyName("mispServerUrl")]
+ [Required(ErrorMessage = "mispServerUrl is a required configuration property.")]
+ [Url(ErrorMessage = "mispServerUrl must be a valid URL.")]
+ public string MispServerUrl { get; set; }
+ [JsonPropertyName("paginationLimit")]
+ public int PaginationLimit { get; set; } = 5000;
+
+ [JsonPropertyName("updateInterval")]
+ [Required(ErrorMessage = "updateInterval is a required configuration property.")]
+ [RegularExpression(@"^\d+[mhd]$", ErrorMessage = "Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').", MatchTimeoutInMilliseconds = 3000)]
+ public string UpdateInterval { get; set; }
+ }
+
+ private class MispAttribute
+ {
+ [JsonPropertyName("value")]
+ public string Value { get; set; }
+ }
+
+ private class MispRequestBody
+ {
+ [JsonPropertyName("deleted")]
+ public bool Deleted { get; set; }
+
+ [JsonPropertyName("last")]
+ public string Last { get; set; }
+
+ [JsonPropertyName("limit")]
+ public int Limit { get; set; }
+
+ [JsonPropertyName("page")]
+ public int Page { get; set; }
+
+ [JsonPropertyName("to_ids")]
+ public bool To_ids { get; set; }
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+ }
+
+ private class MispResponse
+ {
+ [JsonPropertyName("response")]
+ public MispResponseData Response { get; set; }
+ }
+
+ private class MispResponseData
+ {
+ [JsonPropertyName("Attribute")]
+ public List Attribute { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Apps/MispConnectorApp/MispConnectorApp.csproj b/Apps/MispConnectorApp/MispConnectorApp.csproj
new file mode 100644
index 00000000..acba681d
--- /dev/null
+++ b/Apps/MispConnectorApp/MispConnectorApp.csproj
@@ -0,0 +1,43 @@
+
+
+
+ net8.0
+ false
+ 8.0
+ false
+ Technitium
+ Technitium DNS Server
+ Zafer Balkan
+ MispConnectorApp
+ MispConnector
+ https://technitium.com/dns/
+ https://github.com/TechnitiumSoftware/DnsServer
+ Block domain names extracted MISP instance by querying IOCs.\n\nNote! This app works independent of the DNS server's built-in blocking feature. The options configured in DNS server Settings section does not apply to this app.
+ false
+ Library
+
+
+
+
+ false
+
+
+
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll
+ false
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll
+ false
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Apps/MispConnectorApp/README.md b/Apps/MispConnectorApp/README.md
new file mode 100644
index 00000000..5b8a4a6c
--- /dev/null
+++ b/Apps/MispConnectorApp/README.md
@@ -0,0 +1,43 @@
+# MISP Connector for Technitium DNS Server
+
+A plugin that pulls malicious domain names from MISP feeds and enforces blocking in Technitium DNS.
+
+It maintains in-memory blocklists with disk-backed caching and periodically refreshes from the source.
+
+## Features
+
+- Retrieves indicators of compromise (IOCs) aka. malicious domain names from a MISP server via its REST API.
+- Handles paginated fetches with exponential backoff and retry on transient failures.
+- Stores the latest blocklist in memory for fast lookup and persists it to disk for faster startup.
+- Blocks matching DNS requests by returning NXDOMAIN or, for TXT queries when enabled, a human-readable blocking report.
+- Optionally includes extended DNS error metadata.
+- Configurable refresh interval and age window for which indicators are considered.
+- Optional disabling of TLS certificate validation with explicit warning in logs.
+
+## Configuration
+
+Supply a JSON configuration like the following:
+
+```json
+{
+ "enableBlocking": true,
+ "mispServerUrl": "https://misp.example.com",
+ "mispApiKey": "YourMispApiKeyHere",
+ "disableTlsValidation": false,
+ "updateInterval": "2h",
+ "maxIocAge": "15d",
+ "allowTxtBlockingReport": true,
+ "paginationLimit": 5000,
+ "addExtendedDnsError": true
+}
+```
+
+- You can disable the app without uninstalling.
+- You can disable TLS validation for test instances and homelabs, but **it is not recommended use this option in production**.
+- The `maxIocAge` option is used for filtering IOCs wih `lastSeen` attributes on MISP. So, you can dynamically filter for recent campaigns.
+- The `allowTxtBlockingReport` rewrites the response with a blocking report.
+- The `addExtendedDnsError` is useful when logs are exported to a SIEM. The blocking report gets added to EDNS payload of the package.
+
+# Acknowledgement
+
+Thanks to everyone who has been part of or contributed to [MISP Project](https://www.misp-project.org/) for being an amazing resource.
\ No newline at end of file
diff --git a/Apps/MispConnectorApp/dnsApp.config b/Apps/MispConnectorApp/dnsApp.config
new file mode 100644
index 00000000..e5e76a05
--- /dev/null
+++ b/Apps/MispConnectorApp/dnsApp.config
@@ -0,0 +1,11 @@
+{
+ "enableBlocking": true,
+ "mispServerUrl": "https://misp.example.com",
+ "mispApiKey": "YourMispApiKeyHere",
+ "disableTlsValidation": false,
+ "updateInterval": "2h",
+ "maxIocAge": "30d",
+ "allowTxtBlockingReport": true,
+ "paginationLimit": 5000,
+ "addExtendedDnsError": true
+}
diff --git a/DnsServer.sln b/DnsServer.sln
index 24ad9414..676a1040 100644
--- a/DnsServer.sln
+++ b/DnsServer.sln
@@ -67,6 +67,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsSqlServerApp", "Ap
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsMySqlApp", "Apps\QueryLogsMySqlApp\QueryLogsMySqlApp.csproj", "{699E2A1D-D917-4825-939E-65CDB2B16A96}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MispConnectorApp", "Apps\MispConnectorApp\MispConnectorApp.csproj", "{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -197,6 +199,10 @@ Global
{699E2A1D-D917-4825-939E-65CDB2B16A96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -228,6 +234,7 @@ Global
{0A9B7F39-80DA-4084-AD47-8707576927ED} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{6F655C97-FD43-4FE1-B15A-6C783D2D91C9} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{699E2A1D-D917-4825-939E-65CDB2B16A96} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
+ {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6747BB6D-2826-4356-A213-805FBCCF9201}