Skip to content

Commit

Permalink
Improve record analyses and add sentry
Browse files Browse the repository at this point in the history
  • Loading branch information
Dainii committed Mar 8, 2024
1 parent 1a850fb commit 833d6c7
Show file tree
Hide file tree
Showing 22 changed files with 258 additions and 111 deletions.
5 changes: 4 additions & 1 deletion .env.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DATABASE_HOST=localhost
DATABASE_PORT=5432

# Redis connection URL
REDIS_URL=
REDIS_URL=redis://127.0.0.1:6379/0

# Keys for data encryption
# Can be generated with "bundle exec rails db:encryption:init"
Expand All @@ -26,3 +26,6 @@ METADATA_ENDPOINT=
# Or a IDP service url and a certificate
IDP_SERVICE_URL=
IDP_CERTIFICATE=

# Enable sentry
SENTRY_DSN=
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ gem 'rotp'
gem 'rqrcode'
gem 'webauthn'

# Sentry
gem 'sentry-rails'
gem 'sentry-ruby'

# Dnsruby is a pure Ruby DNS client library which implements a stub resolver.
# It aims to comply with all DNS RFCs, including DNSSEC NSEC3 support.
gem 'dnsruby'
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ GEM
safety_net_attestation (0.4.0)
jwt (~> 2.0)
securerandom (0.3.1)
sentry-rails (5.16.1)
railties (>= 5.0)
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
sequel (5.77.0)
bigdecimal
sequel-activerecord_connection (1.3.1)
Expand Down Expand Up @@ -486,6 +491,8 @@ DEPENDENCIES
rubocop-rspec
rubyzip
securerandom
sentry-rails
sentry-ruby
shoulda-matchers
simplecov
simplecov-cobertura
Expand Down
73 changes: 62 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,75 @@ DmarClean

DmarClean is a rails based application to analyze and display result of various dmarc report.

## TODO
## Install

Things you may want to cover:
### Clone the repository

* Ruby version
```shell
git clone https://github.com/Dainii/dmarclean.git
cd dmarclean
```

* System dependencies
### Check your Ruby version

* Configuration
```shell
ruby -v
```

* Database creation
The ouput should start with something like `ruby 3.3.0`

* Database initialization
If not, install the right ruby version using [rbenv](https://github.com/rbenv/rbenv) (it could take a while):

* How to run the test suite
```shell
rbenv install 3.3.0
```

* Services (job queues, cache servers, search engines, etc.)
### Install dependencies

* Deployment instructions
Using [Bundler](https://github.com/bundler/bundler), install the dependencies:

* ...
```shell
bundle install
```

### Set environment variables

Copy `.env.erb` to `.env`.

* Set the Database related variables
* Generate the Active Record encryption keys with `bundle exec rails db:encryption:init`

### Initialize the database

Create the Database
```shell
bundle exec rails db:create
```

Load the schema (first time only)
```shell
bundle exec rails db:schema:load
```

Apply migrations
```shell
bundle exec rails db:migrate db:seed
```

Load seeds
```shell
bundle exec rails db:seed
```

Reset the database (drop + load schema + seed)
```shell
bundle exec rails db:reset
```

## Start application

This will start the web server, the worker and Tailwind app. Details are defined in the `Procfile.dev` file

```shell
./bin/dev
```
26 changes: 13 additions & 13 deletions app/models/feedback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,28 @@ def extract_data
save
end

def fully_valid?
records.all?(&:fully_valid?)
def all_records_spf_valid?
records.all? { |record| record.spf_result == 'pass' }
end

def partially_valid?
records.any?(&:fully_valid?)
def any_records_spf_valid?
records.any? { |record| record.spf_result == 'pass' }
end

def dkim_fully_valid?
records.all?(&:dkim_valid?)
def all_records_dkim_valid?
records.all? { |record| record.dkim_result == 'pass' }
end

def dkim_partially_valid?
records.any?(&:dkim_valid?)
def any_records_dkim_valid?
records.any? { |record| record.dkim_result == 'pass' }
end

def spf_fully_valid?
records.all?(&:spf_valid?)
def all_records_dmarc_valid?
records.all?(&:dmarc)
end

def spf_partially_valid?
records.any?(&:spf_valid?)
def any_records_dmarc_valid?
records.any?(&:dmarc)
end

private
Expand Down Expand Up @@ -89,7 +89,7 @@ def extract_domain
end

def extract_policy_published
self.policy_published = raw_content['feedback']['policy_published'].except('domain')
self.policy_published = raw_content['feedback']['policy_published']
end

def extract_records
Expand Down
53 changes: 38 additions & 15 deletions app/models/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,61 @@ def update_from_hash(raw_record)
end

def dkim_authentication
return auth_results.dig('dkim', 'result') if auth_results['dkim'].instance_of?(Hash)

return auth_results['dkim'].find { |auth| auth['domain'] == feedback.domain.name }['result'] if auth_results['dkim']

'none'
if auth_results['dkim'].instance_of?(Hash)
{
result: auth_results.dig('dkim', 'result'),
selector: auth_results.dig('dkim', 'selector'),
domain: auth_results.dig('dkim', 'domain')
}
elsif auth_results['dkim'].instance_of?(Array)
{
result: auth_results['dkim'].first['result'],
selector: auth_results['dkim'].first['selector'],
domain: auth_results['dkim'].first['domain']
}
end
end

def dkim_alignment
policy_evaluated['dkim']
(identifiers['header_from'] || feedback.policy_published['domain']) == dkim_authentication[:domain]
end

def dkim_valid?
dkim_alignment == 'pass'
def dkim_result
policy_evaluated['dkim']
end

def spf_authentication
auth_results.dig('spf', 'result')
{
result: auth_results.dig('spf', 'result'),
domain: auth_results.dig('spf', 'domain')
}
end

def spf_alignment
(identifiers['header_from'] || feedback.policy_published['domain']) == spf_authentication[:domain]
end

def spf_result
policy_evaluated['spf']
end

def spf_valid?
spf_alignment == 'pass'
def dkim_pass
if feedback.policy_published['adkim'] && feedback.policy_published['adkim'] == 's'
dkim_result == 'pass' && dkim_alignment
else
dkim_result == 'pass'
end
end

def fully_valid?
dkim_valid? && spf_valid?
def spf_pass
if feedback.policy_published['aspf'] && feedback.policy_published['aspf'] == 's'
spf_result == 'pass' && spf_alignment
else
spf_result == 'pass'
end
end

def partially_valid?
dkim_valid? || spf_valid?
def dmarc
dkim_pass || spf_pass
end
end
5 changes: 3 additions & 2 deletions app/views/domains/_feedback.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
<div class="flex-auto">
<div class="flex items-start gap-x-3">
<div class="text-sm font-medium leading-6 text-gray-900 dark:text-gray-100"><%= feedback.organization.name %></div>
<%= render partial: 'domains/validity_badge', locals: { fully_valid: feedback.dkim_fully_valid?, partially_valid: feedback.dkim_partially_valid?, name: 'DKIM' } %>
<%= render partial: 'domains/validity_badge', locals: { fully_valid: feedback.spf_fully_valid?, partially_valid: feedback.spf_partially_valid?, name: 'SPF' } %>
<%= render partial: 'domains/validity_badge', locals: { all_valid: feedback.all_records_dmarc_valid?, any_valid: feedback.any_records_dmarc_valid?, name: 'Dmarc' } %>
<%= render partial: 'domains/validity_badge', locals: { all_valid: feedback.all_records_dkim_valid?, any_valid: feedback.any_records_dkim_valid?, name: 'DKIM' } %>
<%= render partial: 'domains/validity_badge', locals: { all_valid: feedback.all_records_spf_valid?, any_valid: feedback.any_records_spf_valid?, name: 'SPF' } %>
</div>
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400"><%= source_ip_list(feedback) %></div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/views/domains/_validity_badge.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<% if fully_valid %>
<% if all_valid %>
<div class="rounded-md py-1 px-2 text-xs font-medium ring-1 ring-inset text-green-700 dark:text-gray-800 bg-green-50 dark:bg-green-500 ring-green-600/20"><%= name %></div>
<% elsif partially_valid %>
<% elsif any_valid %>
<div class="rounded-md py-1 px-2 text-xs font-medium ring-1 ring-inset text-yellow-700 dark:text-gray-800 bg-yellow-50 dark:bg-yellow-600 ring-yellow-600/20"><%= name %></div>
<% else %>
<div class="rounded-md py-1 px-2 text-xs font-medium ring-1 ring-inset text-red-700 dark:text-gray-800 bg-red-50 dark:bg-red-400 ring-red-600/20"><%= name %></div>
Expand Down
6 changes: 3 additions & 3 deletions app/views/feedbacks/_organization.html.erb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="space-y-16 py-16 xl:space-y-20">
<div class="mt-6 overflow-hidden border-t border-gray-100 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 dark:bg-gray-300 dark:border-gray-400">
<div class="mt-6 overflow-hidden border-t border-gray-100 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 dark:bg-slate-800 dark:border-gray-400">
<div class="px-4 py-10 sm:px-6 lg:px-8">
<div class="mx-auto flex items-center justify-between gap-x-8 lg:mx-0">
<div class="flex items-center gap-x-6">
<img src="https://tailwindui.com/img/logos/48x48/tuple.svg" alt="" class="h-16 w-16 flex-none rounded-full ring-1 ring-gray-900/10">
<h1>
<div class="text-sm leading-6 text-gray-500"><%= t('.report_id') %> <span class="text-gray-700"><%= feedback.report_id %></span></div>
<div class="mt-1 text-base font-semibold leading-6 text-gray-900"><%= organization.name %></div>
<div class="text-sm leading-6 text-gray-500 dark:text-gray-200"><%= t('.report_id') %> <span class="text-gray-700 dark:text-gray-400"><%= feedback.report_id %></span></div>
<div class="mt-1 text-base font-semibold leading-6 text-gray-900 dark:text-gray-950 dark:font-bold"><%= organization.name %></div>
</h1>
</div>
</div>
Expand Down
32 changes: 17 additions & 15 deletions app/views/feedbacks/_record.html.erb
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
<tr>
<td class="relative py-5 pr-6">
<div class="flex gap-x-6">
<div class="flex-auto">
<div class="flex items-start gap-x-3">
<div class="text-sm font-medium leading-6 text-gray-900 dark:text-gray-100"><%= record.source_ip %>
<%= turbo_frame_tag "dns_#{record.source_ip}", src: ptr_lookup_path(record.source_ip), lazy: true do %>
<%= t('.dns_placeholder') %>
<% end %>
</div>
<%= render partial: 'domains/validity_badge', locals: { fully_valid: record.dkim_valid?, partially_valid: false, name: 'DKIM' } %>
<%= render partial: 'domains/validity_badge', locals: { fully_valid: record.spf_valid?, partially_valid: false, name: 'SPF' } %>
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0">
<div class="flex items-center">
<div class="ml-4">
<div class="font-medium text-gray-900 dark:font-bold">
<%= turbo_frame_tag "dns_#{record.source_ip}", src: ptr_lookup_path(record.source_ip), lazy: true do %>
<%= t('.dns_placeholder') %>
<% end %>
</div>
<div class="mt-1 text-gray-500 dark:text-gray-400"><%= record.source_ip %></div>
</div>
</div>
<div class="absolute bottom-0 right-full h-px w-screen bg-gray-100"></div>
<div class="absolute bottom-0 left-0 h-px w-screen bg-gray-100"></div>
</td>
<td class="py-5 text-right">
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400"><%= t('.count') %> <span class="text-gray-900 dark:text-gray-200"><%= record.count %></span></div>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
<%= render partial: 'feedbacks/result_badge', locals: { pass: record.dmarc } %>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
<%= render partial: 'feedbacks/result_badge', locals: { pass: record.dkim_pass } %>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
<%= render partial: 'feedbacks/result_badge', locals: { pass: record.spf_pass } %>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 dark:text-gray-400"><%= record.count %></td>
</tr>
55 changes: 25 additions & 30 deletions app/views/feedbacks/_records.html.erb
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
<div class="space-y-16 py-16 xl:space-y-20">
<!-- Recent activity table -->
<div>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h2 class="mx-auto max-w-2xl text-base font-semibold leading-6 text-gray-900 lg:mx-0 lg:max-w-none"><%= t('.recent_activity') %></h2>
<div class="px-4 sm:px-6 lg:px-8 space-y-16 py-16 xl:space-y-20">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-300"><%= t('.title') %></h1>
<p class="mt-2 text-sm text-gray-700 dark:text-gray-500"><%= t('.description') %></p>
</div>
<div class="mt-6 overflow-hidden border-t border-gray-100 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl lg:mx-0 lg:max-w-none">
<table class="w-full text-left">
<thead class="sr-only">
<tr>
<th><%= t('.table_head_amount') %></th>
<th class="hidden sm:table-cell"><%= t('.table_head_client') %></th>
<th><%= t('.table_head_more_details') %></th>
</tr>
</thead>
<tbody>
<tr class="text-sm leading-6 text-gray-900">
<th scope="colgroup" colspan="3" class="relative isolate py-2 font-semibold">
<time datetime="2023-03-22"><%= t('.table_content_today') %></time>
<div class="absolute inset-y-0 right-full -z-10 w-screen border-b border-gray-200 bg-gray-50"></div>
<div class="absolute inset-y-0 left-0 -z-10 w-screen border-b border-gray-200 bg-gray-50"></div>
</th>
</tr>
<% records.each do |record| %>
<%= render partial: 'feedbacks/record', locals: { record: } %>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-300 sm:pl-0"><%= t('.table_head_source') %></th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-300"><%= t('.table_head_dmarc_status') %></th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-300"><%= t('.table_head_dkim_status') %></th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-300"><%= t('.table_head_spf_status') %></th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-300"><%= t('.table_head_volume') %></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:bg-slate-800">
<% records.each do |record| %>
<%= render partial: 'feedbacks/record', locals: { record: } %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 833d6c7

Please sign in to comment.