Compare commits

...

8 Commits

  1. 3
      .rspec
  2. 12
      Gemfile
  3. 10
      Gemfile-2.3.lock
  4. 2
      Procfile
  5. 2
      Procfile.sidekiq
  6. 4
      app/controllers/check_controller.rb
  7. 1
      app/controllers/site_controller.rb
  8. 14
      app/controllers/sites_controller.rb
  9. 23
      app/controllers/statistics_controller.rb
  10. 14
      app/controllers/stats_controller.rb
  11. 2
      app/helpers/application_helper.rb
  12. 17
      app/helpers/check_helper.rb
  13. 48
      app/helpers/sites_helper.rb
  14. 2
      app/helpers/statistics_helper.rb
  15. 81
      app/javascript/css/application.scss
  16. 40
      app/javascript/js/stats/grades.js
  17. 72
      app/javascript/js/stats/index.js
  18. 7
      app/javascript/packs/application.js
  19. 6
      app/lib/matomo.rb
  20. 99
      app/models/analysis.rb
  21. 10
      app/models/stat.rb
  22. 50
      app/views/application/sites.html.erb
  23. 15
      app/views/check/show.html.erb
  24. 13
      app/views/ssh/show.html.erb
  25. 7
      app/views/statistics/grade.html.erb
  26. 164
      app/views/statistics/index.html.erb
  27. 7
      app/views/stats/grade.html.erb
  28. 167
      app/views/stats/index.html.erb
  29. 29
      bin/rspec
  30. 116
      bin/stats
  31. 1
      config/initializers/matomo.rb
  32. 8
      config/locales/en.yml
  33. 3
      config/locales/fr.yml
  34. 15
      config/routes.rb
  35. 67
      config/sites.yml
  36. 11
      db/migrate/20220326181216_create_stats.rb
  37. 16
      db/migrate/20220626133648_add_refresh_to_analysis.rb
  38. 26
      db/schema.rb
  39. 3
      package.json
  40. 55
      spec/models/analysis_spec.rb
  41. 65
      spec/models/stat_spec.rb
  42. 64
      spec/rails_helper.rb
  43. 94
      spec/spec_helper.rb
  44. 0
      test/controllers/.keep
  45. 4
      test/controllers/check_controller_test.rb
  46. 4
      test/controllers/https_controller_test.rb
  47. 4
      test/controllers/site_controller_test.rb
  48. 4
      test/controllers/smtp_controller_test.rb
  49. 4
      test/controllers/ssh_controller_test.rb
  50. 19
      test/controllers/tls_controller_test.rb
  51. 4
      test/controllers/xmpp_controller_test.rb
  52. 0
      test/fixtures/.keep
  53. 0
      test/helpers/.keep
  54. 0
      test/integration/.keep
  55. 0
      test/mailers/.keep
  56. 0
      test/models/.keep
  57. 3
      test/test_helper.rb

@ -0,0 +1,3 @@
--require spec_helper
--format progress
--format html --out tmp/rspec.html

@ -17,6 +17,8 @@ gem 'sidekiq-workflow', git: 'https://git.imirhil.fr/aeris/sidekiq-workflow.git'
gem 'simpleidn'
gem 'http_accept_language'
gem 'recursive-open-struct'
gem 'ruby-progressbar'
gem 'public_suffix'
gem 'uglifier'
gem 'sass-rails'
@ -28,7 +30,6 @@ gem 'cryptcheck', '~> 2.0.0', path: '../engine'
group :development do
gem 'foreman'
gem 'amazing_print'
gem 'listen'
gem 'spring'
@ -42,3 +43,12 @@ group :development do
gem 'guard-livereload', require: false
gem 'rack-livereload'
end
group :development, :test do
gem 'pry-byebug'
gem 'amazing_print'
end
group :test do
gem 'rspec-rails'
end

@ -29,8 +29,6 @@ GEM
colorize (0.8.1)
connection_pool (2.2.5)
dotenv (2.7.6)
foreman (0.87.2)
http_accept_language (2.1.1)
httparty (0.20.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
@ -42,11 +40,9 @@ GEM
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
parallel (1.19.2)
pg (1.2.3)
rack (2.0.9)
rack-protection (2.2.0)
rack
recursive-open-struct (1.1.3)
redis (4.4.0)
redlock (1.2.2)
redis (>= 3.0.0, < 5.0)
@ -73,13 +69,9 @@ DEPENDENCIES
amazing_print
cryptcheck (~> 2.0.0)!
dotenv
foreman
http_accept_language
pg
recursive-open-struct
sidekiq
sidekiq-workflow!
simpleidn
BUNDLED WITH
2.3.9
2.3.10

@ -1,4 +1,2 @@
web: bundle exec guard -i
webpack: bundle exec webpack-dev-server
sidekiq: bundle exec sidekiq -q default
sidekiq_1_0: BUNDLE_GEMFILE=Gemfile-2.3 bin/sidekiq 1.0 -q tls_1_0

@ -0,0 +1,2 @@
sidekiq: bundle exec sidekiq -q default
sidekiq_1_0: BUNDLE_GEMFILE=Gemfile-2.3 bin/sidekiq 1.0 -q tls_1_0

@ -18,8 +18,8 @@ class CheckController < ApplicationController
def refresh
unless @analysis.pending
if Rails.env == 'production'
refresh_allowed = @analysis.updated_at + Rails.configuration.refresh_delay
if Time.now < refresh_allowed
refresh_at = @analysis.refresh_at
if refresh_at || Time.now < refresh_at
flash[:warning] = "Merci d’attendre au moins #{l refresh_allowed} pour rafraîchir"
return redirect_to action: :show, id: @host
end

@ -36,7 +36,6 @@ class SiteController < ApplicationController
end
def help
end
def about

@ -0,0 +1,14 @@
class SitesController < ApplicationController
@@sites = YAML.load_file Rails.root.join 'config/sites.yml'
@@sites.keys.each do |name|
define_method(name) { sites name }
end
private
def sites(name)
@name = name
@sites = Stat[:"sites_#{name}"].data
render :sites
end
end

@ -0,0 +1,23 @@
class StatisticsController < ApplicationController
TODAY = Date.today
def show
service = params.fetch :id
respond_to do |format|
format.json do
json = Stat["grades_for_#{service}"].data
render json: json, status: :ok
end
end
end
def ciphers
service = params.fetch :id
render json: Stat["ciphers_for_#{service}"].data
end
def tls
service = params.fetch :id
render json: Stat["tls_for_#{service}"].data
end
end

@ -0,0 +1,14 @@
class StatsController < ApplicationController
@@sites = YAML.load_file Rails.root.join 'config/sites.yml'
def banks
sites :banks
end
private
def sites(name)
@sites = @@sites.fetch name.to_s
render 'stats/sites'
end
end

@ -1,2 +0,0 @@
module ApplicationHelper
end

@ -1,16 +1,14 @@
module CheckHelper
private def __label(value, color, state = true)
color = :default unless color
color = "state-#{color}" if state
"<span class=\"badge badge-#{color}\">#{value}</span>"
end
include ActionView::Helpers::TagHelper
def label(value, color, state = true)
__label(value, color, state).html_safe
color = :default unless color
color = "state-#{color}" if state
content_tag :span, value.to_s.html_safe, class: [:badge, :"badge-#{color}"]
end
def cell(value, color, state = true)
"<td class=\"badge-state-#{color}\">#{value}</td>".html_safe
content_tag :td, value, class: :"table-#{color}"
end
def labels(level, states, state = true)
@ -22,7 +20,7 @@ module CheckHelper
else
value ? :success : :danger
end
__label name, color, state
label name, color, state
end.join(' ').html_safe
end
@ -30,7 +28,7 @@ module CheckHelper
::CryptCheck::State.collect do |level|
states[level].each_pair
.select { |_, v| v == true }
.collect { |name, _| __label name, level }
.collect { |name, _| label name, level }
end.flatten(1).join(' ').html_safe
end
@ -54,6 +52,7 @@ module CheckHelper
end
def rank_label(rank)
rank = rank&.to_sym
l = %i(0 V T X).include? rank
label rank, rank_color(rank), !l
end

@ -0,0 +1,48 @@
module SitesHelper
include CheckHelper
def domain_cell(domain, grade)
link = link_to domain, https_show_path(domain)
content_tag :th, (rank_label(grade) + ' ' + link).html_safe
end
def tls_cell(tls)
return unless tls
color = case tls.to_sym
when :tls1_2_only
:success
when :tls1_2
:error
else
:critical
end
content = content_tag :div, color, class: %i[sr-only]
content_tag :td, label(' ' + content, color), class: %i[text-center]
end
def ciphers_cell(ciphers)
return unless ciphers
color = case ciphers.to_sym
when :good
:success
else
:critical
end
content = content_tag :div, color, class: %i[sr-only]
content_tag :td, label(' ' + content, color), class: %i[text-center]
end
def pfs_cell(pfs)
return unless pfs
color = case pfs.to_sym
when :pfs_only
:success
when :pfs
:error
else
:critical
end
content = content_tag :div, color, class: %i[sr-only]
content_tag :td, label(' ' + content, color), class: %i[text-center]
end
end

@ -0,0 +1,2 @@
module StatisticsHelper
end

@ -87,31 +87,32 @@ td.error {
$color-critical: #d9534f;
$color-error: #e4804e;
$color-warning: #f0ad4e;
$color-effort: #6c757d;
$color-good: #beb052;
$color-best: #8db457;
$color-great: #5cb85c;
.badge-state-critical, td.badge-state-critical {
.badge-state-critical, td.table-critical {
background-color: $color-critical;
}
.badge-state-error, td.badge-state-error {
.badge-state-error, td.table-error {
background-color: $color-error;
}
.badge-state-warning, td.badge-state-warning {
.badge-state-warning {
background-color: $color-warning;
}
.badge-state-good, td.badge-state-good {
.badge-state-good, td.table-good {
background-color: $color-good;
}
.badge-state-best, td.badge-state-best {
.badge-state-best, td.table-best {
background-color: $color-best;
}
.badge-state-great, td.badge-state-great {
.badge-state-great, td.table-great {
background-color: $color-great;
}
@ -195,3 +196,71 @@ table.scoring img {
max-width: 140px;
}
}
/** Navigation tabs */
.tab-content {
border: 1px solid $nav-pills-link-active-bg;
border-bottom-left-radius: .3rem;
border-bottom-right-radius: .3rem;
}
.nav-pills .nav-link {
border-radius: .3rem;
font-weight: bold;
}
.nav-pills .nav-link.active {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
font-weight: bolder;
}
/** Chart CSS */
.cumulative-datas {
position: relative;
display: flex;
flex-direction: row;
width: 100%;
&-content {
position: absolute;
height: 2rem;
}
}
.cumulative-data {
height: 2rem;
line-height: 2rem;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: .1rem;
padding-left: .3rem;
}
/** Progress bar */
.progress {
box-shadow: 0 0 .1rem $dark;
height: 1.4rem;
}
.progress-critical {
background-color: $color-critical;
}
.progress-error {
background-color: $color-error;
}
.progress-warning {
background-color: $color-warning;
}
.progress-effort {
background-color: $color-effort;
}
.progress-good {
background-color: $color-good;
}
.progress-best {
background-color: $color-best;
}
.progress-great {
background-color: $color-great;
}

@ -0,0 +1,40 @@
document.addEventListener("DOMContentLoaded", () => {
let gradesChart = document.getElementById('gradesChart').getContext('2d')
let background = [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
]
let createGradesChart = new Chart(gradesChart, {
type: 'bar'
})
let generateGraphs = function () {
fetch(window.location.href, {
method: "GET",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
}).then((response) => {
response.json().then((data) => {
if (response.status === 200) {
createGradesChart.data.labels = data["labels"]
createGradesChart.data.datasets = [{
label: 'Number',
data: data["dataset"],
backgroundColor: background
}]
createGradesChart.update()
}
})
});
}
generateGraphs()
});

@ -0,0 +1,72 @@
document.addEventListener("DOMContentLoaded", () => {
let background = [
'#5cb85c', // A+
'#5cb85c', // A
'#8db457', // B+
'#8db457', // B
'#beb052', // C+
'#beb052', // C
'#6c757d', // D
'#f0ad4e', // E
'#e4804e', // F
'#d9534f' // G
]
for (const service of ["https", "smtp", "tls", "xmpp"]) {
const name = service.replace(/^\w/, c => c.toUpperCase())
console.info(`grades${name}Chart`)
const canvas = document.getElementById(`grades${name}Chart`).getContext('2d')
// const chart = new Chart(canvas, {
// type: 'pie',
// options: {
// interaction: {
// intersect: false,
// mode: 'dataset',
// }
// }
// })
const chart = new Chart(canvas, {
type: 'bar',
options: {
interaction: {
intersect: false,
mode: 'dataset',
},
plugins: {
datalabels: {
anchor: 'end',
align: 'top',
formatter: ((value, context) => {
const index = context.dataIndex
const keys = Object.keys(context.dataset.data)
value = context.dataset.data[keys[index]]
return value
}),
color: "black",
font: {
weight: "bold",
}
}
}
}
})
fetch(`/statistics/${service}.json`).then((response) => {
if (response.status === 200) {
response.json().then((data) => {
const labels = ["A+", "A", "B+", "B", "C+", "C", "D", "E", "F", "G"]
const dataset = JSON.parse(JSON.stringify(data, labels, 0))
chart.data.labels = labels
chart.data.datasets = [{
label: 'Number of request',
data: dataset,
backgroundColor: background
}]
chart.update()
})
}
});
}
}
)
;

@ -1 +1,8 @@
import 'css/application'
import Chart from 'chart.js/auto'
import ChartDataLabels from 'chartjs-plugin-datalabels'
global.Chart = Chart
Chart.register(ChartDataLabels)
import 'js/stats/grades'

@ -10,13 +10,13 @@ class Matomo
cattr_reader :url, :path, :site
def self.load
@@url = ENV.fetch 'MATOMO_URL'
@@site = ENV.fetch 'MATOMO_SITE'
@@url = ENV['MATOMO_URL']
@@site = ENV['MATOMO_SITE']
@@disabled = ENV['MATOMO_DISABLED']
end
def self.enabled?
self.url && self.site && @@disabled.nil?
@@disabled.nil? && self.url && self.site
end
ActionView::Base.include Helpers

@ -1,36 +1,67 @@
class Analysis < ApplicationRecord
enum service: %i[https smtp xmpp tls ssh].collect { |e| [e, e.to_s] }.to_h
validates :service, presence: true
validates :host, presence: true
def self.[](service, host, args)
key = self.key service, host, args
self.find_by key
end
def self.pending!(service, host, args)
key = self.key service, host, args
analysis = self.find_or_create_by! key
analysis.pending!
end
def pending!
self.update! pending: true
self
end
def self.post!(service, host, args, result)
analysis = self[service, host, args]
analysis.post! result
end
def post!(result)
self.update! pending: false, result: result
end
private
def self.key(service, host, args)
{ service: service, host: host, args: args }
end
enum service: %i[https smtp xmpp tls ssh].collect { |e| [e, e.to_s] }.to_h
validates :service, presence: true
validates :host, presence: true
def self.[](service, host, args)
key = self.key service, host, args
self.find_by key
end
def self.pending!(service, host, args)
key = self.key service, host, args
analysis = self.find_or_create_by! key
analysis.pending!
end
def pending!
self.update! pending: true
self
end
def self.post!(service, host, args, result)
analysis = self[service, host, args]
analysis.post! result
end
RESOLVER = ::Resolv::DNS.new
DNS_TXT_FORMAT = /^cryptcheck=(.*)/
DEBUG = 'debug'
DEFAULT_REFRESH_DELAY = Rails.configuration.refresh_delay
def find_refresh_delay
host = self.host
loop do
break unless host && PublicSuffix.valid?(host)
RESOLVER.getresources(host, ::Resolv::DNS::Resource::IN::TXT).each do |txt|
txt.strings.each do |value|
if match = DNS_TXT_FORMAT.match(value)
match = match[1]
return if match == DEBUG
begin
delay = ::ActiveSupport::Duration.parse match
delay = DEFAULT_REFRESH_DELAY if delay > DEFAULT_REFRESH_DELAY
return delay
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
end
end
end
end
_, host = host.split '.', 2
end
DEFAULT_REFRESH_DELAY
rescue
DEFAULT_REFRESH_DELAY
end
def post!(result)
refresh_at = self.find_refresh_delay&.since
self.update! pending: false, refresh_at: refresh_at, result: result
end
private
def self.key(service, host, args)
{ service: service, host: host, args: args }
end
end

@ -0,0 +1,10 @@
class Stat < ApplicationRecord
def self.create!(name, data, date = Date.today)
self.delete_by name: name, date: date
super name: name, date: date, data: data
end
def self.[](name)
self.where(name: name).order(date: :desc).limit(1).first
end
end

@ -0,0 +1,50 @@
<h1><%= @name.to_s.capitalize %></h1>
<h2>Columns meaning</h2>
<div class="container">
<div class="form-group row">
<div class="col-sm-1">TLS:</div>
<div class="col-sm-11">
<%= label(' ' + content_tag(:span, :success, class: %i[sr-only]), :success) %> TLS1.2 only supported
<%= label(' ' + content_tag(:span, :error, class: %i[sr-only]), :error) %> No TLS1.2 supported
<%= label(' ' + content_tag(:span, :critical, class: %i[sr-only]), :critical) %> SSLv2 or SSLv3 supported
</div>
</div>
<div class="row">
<div class="col-sm-1">Ciphers:</div>
<div class="col-sm-11">
<%= label(' ' + content_tag(:span, :success, class: %i[sr-only]), :success) %> Only safe cipher supported
<%= label(' ' + content_tag(:span, :critical, class: %i[sr-only]), :critical) %> Unsafe cipher supported
</div>
</div>
<div class="row">
<div class="col-sm-1">PFS:</div>
<div class="col-sm-11">
<%= label(' ' + content_tag(:span, :success, class: %i[sr-only]), :success) %> Only PFS cipher supported
<%= label(' ' + content_tag(:span, :error, class: %i[sr-only]), :error) %> PFS cipher but also no-PFS cipher supported
<%= label(' ' + content_tag(:span, :critical, class: %i[sr-only]), :critical) %> No PFS supported
</div>
</div>
<br/>
<table class="table table-sm">
<thead>
<tr>
<%= content_tag :th, t('.domain') %>
<%= content_tag :th, t('.tls'), class: %i[text-center] %>
<%= content_tag :th, t('.ciphers'), class: %i[text-center] %>
<%= content_tag :th, t('.pfs'), class: %i[text-center] %>
</tr>
</thead>
<tbody>
<% @sites.sort_by { _2.fetch('grade') || 'Z' }.each do |domain, stat| %>
<tr>
<%= domain_cell domain, stat.fetch('grade') %>
<%= tls_cell stat.fetch 'tls' %>
<%= ciphers_cell stat.fetch 'ciphers' %>
<%= pfs_cell stat.fetch 'pfs' %>
</tr>
<% end %>
</tbody>
</table>

@ -1,16 +1,19 @@
<div class="container">
<div class="row">
<div class="col-sm-11">
<div class="col-sm-10">
<h1>
[<%= self.type.to_s.upcase %>] <%= @host %>
<span class="small">(<%= l @analysis.updated_at %>)</span>
</h1>
</div>
<% if Time.now - @analysis.updated_at >= Rails.configuration.refresh_delay %>
<div class="col-sm-1">
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
</div>
<div class="col-sm-2 text-end">
<% if @analysis.refresh_at.nil? || Time.now >= @analysis.refresh_at %>
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
<% else %>
<%= button_tag t('Refresh not available', date: l(@analysis.refresh_at, format: :time)),
type: :button, class: %i(btn btn-outline-secondary), disabled: true %>
<% end %>
</div>
</div>
<% @result.each do |host|
@ -30,7 +33,7 @@
<div class="row">
<div class="col-sm-12">
<h2>
<%= rank_label host[:grade].to_sym %>
<%= rank_label host[:grade] %>
<%= host[:ip] %> : <%= host[:port] %>
<span class="small">(<%= host[:hostname] %>)</span></h2>
</div>

@ -1,15 +1,18 @@
<div class="container">
<div class="row">
<div class="col-sm-11">
<div class="col-sm-10">
<h1>
[SSH] <%= @host %> <span class="small">(<%= l @analysis.updated_at %>)</span>
</h1>
</div>
<% if Time.now - @analysis.updated_at >= Rails.configuration.refresh_delay %>
<div class="col-sm-1">
<%= link_to t('Refresh'), {action: :refresh}, class: %i(btn btn-default) %>
<div class="col-sm-2 text-end">
<% if Time.now >= @analysis.refresh_at %>
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
<% else %>
<%= button_tag t('Refresh not available', date: l(@analysis.refresh_at, format: :time)),
type: :button, class: %i(btn btn-outline-secondary), disabled: true %>
<% end %>
</div>
<% end %>
</div>
<%
@result.each do |host|

@ -0,0 +1,7 @@
<div>
Nombre de recherche <%= params[:service] %> : <%= @services.size %>
</div>
<div>
<canvas id="gradesChart"></canvas>
</div>

@ -0,0 +1,164 @@
<% colors = {
"A+" => 'great',
"good" => 'great',
"A" => 'great',
"B+" => 'best',
"B" => 'best',
"C+" => 'good',
"C" => 'good',
"D" => 'effort',
"E" => 'warning',
"F" => 'error',
"G" => 'critical',
"bad" => 'critical',
"ssl" => 'critical',
"tls" => 'error',
"tls1_2" => 'effort',
"tls1_2_only" => 'great'
} %>
<ul class="nav nav-pills nav-fill" id="pills-tab" role="Navigation stats list">
<% %i[https smtp tls xmpp].each do |service| %>
<li class="nav-item" role="presentation">
<button class="nav-link<%= " active btn-dark" if service.to_s == "https" %>"
id="nav-<%= service %>-pill" data-bs-toggle="pill"
data-bs-target="#nav-<%= service %>" type="button" role="Button to show <%= service %> stats" aria-controls="nav-<%= service %>" aria-selected="true">
<%= service.to_s.upcase %>
</button>
</li>
<% end %>
</ul>
<div class="tab-content p-2 mb-4" id="nav-pillsContent">
<% %i[https smtp tls xmpp].each do |service| %>
<div class="tab-pane fade show <%= "active" if service.to_s == "https" %>"
id="nav-<%= service %>" role="<%= service.to_s %>> stats"
aria-labelledby="nav-<%= service %>-pill">
<h2>Grades for service <%= service.to_s.upcase %></h2>
<%
grades = Stat["grades_for_#{service}"]
total = grades.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with a grade.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<th>Grade</th>
<th>Percent</th>
<th>Number</th>
<th>Visual</th>
</tr>
</thead>
<tbody>
<% sorted_grades = grades.data.keys.sort &CryptCheck::Grade.method(:compare)
sorted_grades.each do |grade|
unless %w(T V).include?(grade)
number = grades.data[grade].to_i
percent = (number.to_f / total.to_f) * 100.0
color = CryptCheck::Grade::GRADE_STATUS.fetch grade.to_sym %>
<tr>
<td><%= grade %></td>
<td><%= percent.round %>%</td>
<td><%= number %></td>
<td>
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %> border-dark"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end
end %>
</tbody>
</table>
<h2>Ciphers for service <%= service.to_s.upcase %></h2>
<%
ciphers = Stat["ciphers_for_#{service}"]
total = ciphers.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with a cipher.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<th>Grade</th>
<th>Percent</th>
<th>Number</th>
<th>Visual</th>
</tr>
</thead>
<tbody>
<% sorted_grades = ciphers.data.keys.sort &CryptCheck::Grade.method(:compare)
sorted_grades.each do |grade|
number = ciphers.data[grade]
percent = (number.to_f / total.to_f) * 100.0
color = colors[grade]
%>
<tr>
<td><%= grade.capitalize %></td>
<td><%= percent.round %>%</td>
<td><%= number %></td>
<td>
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %>"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<h2>TLS for service <%= service.to_s.upcase %></h2>
<% tls = Stat["tls_for_#{service}"]
total = tls.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with TLS.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<th>Grade</th>
<th>Percent</th>
<th>Number</th>
<th>Visual</th>
</tr>
</thead>
<tbody>
<%
sorted_grades = tls.data.keys.sort &CryptCheck::Grade.method(:compare)
sorted_grades.each do |grade|
number = tls.data[grade]
percent = (number.to_f / total.to_f) * 100.0
color = colors[grade]
%>
<tr>
<td><%= grade.capitalize %></td>
<td><%= percent.round %>%</td>
<td><%= number %></td>
<td>
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %>"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>

@ -0,0 +1,7 @@
<div>
Nombre de recherche <%= params[:service] %> : <%= @services.size %>
</div>
<div>
<canvas id="gradesChart"></canvas>
</div>

@ -0,0 +1,167 @@
<% colors = {
"A+" => 'great',
"good" => 'great',
"A" => 'great',
"B+" => 'best',
"B" => 'best',
"C+" => 'good',
"C" => 'good',
"D" => 'effort',
"E" => 'warning',
"F" => 'error',
"G" => 'critical',
"bad" => 'critical',
"ssl" => 'critical',
"tls" => 'error',
"tls1_2" => 'effort',
"tls1_2_only" => 'great'
} %>
<ul class="nav nav-pills nav-fill" id="pills-tab" role="Navigation stats list">
<% %i[https smtp tls xmpp].each do |service| %>
<li class="nav-item" role="presentation">
<button class="nav-link<%= " active btn-dark" if service.to_s == "https" %>"
id="nav-<%= service %>-pill" data-bs-toggle="pill"
data-bs-target="#nav-<%= service %>" type="button" role="Button to show <%= service %> stats" aria-controls="nav-<%= service %>" aria-selected="true">
<%= service.to_s.upcase %>
</button>
</li>
<% end %>
</ul>
<div class="tab-content p-2 mb-4" id="nav-pillsContent">
<% %i[https smtp tls xmpp].each do |service| %>
<div class="tab-pane fade show <%= "active" if service.to_s == "https" %>"
id="nav-<%= service %>" role="<%= service.to_s %>> stats"
aria-labelledby="nav-<%= service %>-pill">
<h2>Grades for service <%= service.to_s.upcase %></h2>
<%
grades = Stat["grades_for_#{service}"]
total = grades.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with a grade.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<th>Grade</th>
<th>Count</th>
<th></th>
</tr>
</thead>
<tbody>
<% sorted_grades = grades.data.keys.sort &CryptCheck::Grade.method(:compare)
sorted_grades.each do |grade|
unless %w(T V).include?(grade)
count = grades.data[grade].to_i
percent = (count.to_f / total.to_f) * 100.0
color = CryptCheck::Grade::GRADE_STATUS.fetch grade.to_sym %>
<tr>
<td class="col-4"><%= grade.capitalize %></td>
<td class="col-4"><%= count %>
(<%= percent.round %>%)
</td>
<td class="col-4">
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %> border-dark"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end
end %>
</tbody>
</table>
<h2>Ciphers for service <%= service.to_s.upcase %></h2>
<%
name = "ciphers_for_#{service}"
ciphers = Stat[name]
total = ciphers.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with a cipher.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<%= content_tag :th, t(:'.ciphers.title') %>
<th>Count</th>
<th></th>
</tr>
</thead>
<tbody>
<%
ciphers = ciphers.data
%w[good bad].each do |grade|
count = ciphers.fetch grade
percent = (count.to_f / total.to_f) * 100.0
color = colors[grade]
%>
<tr>
<td class="col-4"><%= t ".ciphers.#{grade}" %></td>
<td class="col-4"><%= count %> (<%= percent.round %>
%)
</td>
<td class="col-4">
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %>"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<h2>TLS for service <%= service.to_s.upcase %></h2>
<%
name = "tls_for_#{service}"
tls = Stat[name]
total = tls.data.collect { _2 }.sum
%>
<p>Over <%= total %> URL tested with TLS.</p>
<table class="table table-bordered table-striped">
<thead class="bg-dark text-light">
<tr>
<%= content_tag :th, t(:'.tls.title') %>
<th>Count</th>
<th></th>
</tr>
</thead>
<tbody>
<%
tls = tls.data
%w[tls1_2_only tls1_2 tls ssl].each do |grade|
count = tls.fetch grade
percent = (count.to_f / total.to_f) * 100.0
color = colors[grade]
%>
<tr>
<td class="col-4"><%= t ".tls.#{grade}" %></td>
<td class="col-4"><%= count %> (<%= percent.round %>
%)
</td>
<td class="col-4">
<div class="progress bg-light">
<div class="progress-bar progress-<%= color %>"
style="width: <%= percent.round %>%" role="progressbar"
aria-valuenow="<%= percent.round %>" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rspec' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rspec-core", "rspec")

@ -0,0 +1,116 @@
#!./bin/rails runner
# Profit from open class to add stats methods only on this script
class Analysis
def grade
grades = self.result.collect { _1['grade'] }.compact
CryptCheck::Grade.worst grades
end
def tls
return unless (result = self.result)
protocols = result.collect { |r| r.dig('handshakes', 'protocols')
&.collect { |p| p['protocol'].to_sym } }
.compact.flatten.uniq
return :ssl unless (protocols & %i[SSLv2 SSLv3]).empty?
return :tls unless protocols.include? :TLSv1_2
return :tls1_2_only if protocols == %i[TLSv1_2]
:tls1_2
end
def ciphers
return unless (result = self.result)
status = result.collect do |r|
r.dig('handshakes', 'ciphers')&.collect do |c|
s = CryptCheck::Tls::Cipher
.new(nil, c.fetch('name')).status
CryptCheck::State.good_or_bad s
end
end.compact.flatten.uniq
return :bad if status.include? :bad
:good
end
def pfs
return unless (result = self.result)
ciphers = result.collect do |r|
r.dig('handshakes', 'ciphers')&.collect do |c|
CryptCheck::Tls::Cipher
.new(nil, c.fetch('name'))
.pfs?
end
end.compact.flatten.uniq
return :no_pfs unless ciphers.include? true
return :pfs_only unless ciphers.include? false
:pfs
end
end
sites = YAML.load_file Rails.root.join 'config/sites.yml'
workflows = []
sites.each do |type, domains|
domains.each do |domain|
puts "Refreshing #{domain}"
@analysis = Analysis.pending! :https, domain, 443
workflows << CheckWorkflow.start!(:https, @analysis.host, *@analysis.args)
end
end
workflows.each &:wait
sites.each do |type, domains|
domains = domains.collect do |domain|
analysis = Analysis[:https, domain, 443]
stats = {
grade: analysis.grade,
tls: analysis.tls,
ciphers: analysis.ciphers,
pfs: analysis.pfs
}
[domain, stats]
end.to_h
Stat.create! :"sites_#{type}", domains
end
# general stat
services = Analysis.group(:service).order(service: :asc).count
Stat.create! :request_per_service, { labels: services.keys, dataset: services.values }
# grade per service for https, smtp, tls and xmpp
%i[https smtp tls xmpp].each do |service_name|
services = Analysis.where service: service_name, pending: false
services.each do |service|
if (g = service.grade)
grades[g] += 1
end
if (t = service.tls)
tls[t] += 1
end
if (c = service.ciphers)
ciphers[c] += 1
end
if (p = service.pfs)
pfs[p] += 1
end
end
ap "grades_for_#{service}" => grades
Stat.create! "grades_for_#{service}", grades
ap "tls_for_#{service}" => tls
Stat.create! "tls_for_#{service}", tls
ap "ciphers_for_#{service}" => ciphers
Stat.create! "ciphers_for_#{service}", ciphers
ap "pfs_for_#{service}" => pfs
Stat.create! "pfs_for_#{service}", pfs
end

@ -1 +0,0 @@
require 'matomo'

@ -18,6 +18,7 @@ en:
"Error during analysis:": "Error during analysis:"
Refresh: Refresh
Refresh not available: Refresh available at %{date}
Protocol: Protocol
Protocols: Protocols
@ -78,3 +79,10 @@ en:
great:
hsts: This server supports HSTS with long duration
time:
formats:
default: "%d/%m/%Y %H:%M:%S %:z"
long: "%d/%m/%Y %H:%M:%S"
short: "%d %b %Hh%M"
time: "%H:%M:%S"

@ -18,6 +18,7 @@ fr:
"Error during analysis:": "Erreur durant l’analyse :"
Refresh: Rafraîchir
Refresh not available: Rafraîchissement disponible à %{date}
Protocol: Protocole
Protocols: Protocoles
@ -262,5 +263,7 @@ fr:
am: am
formats:
default: "%d/%m/%Y %H:%M:%S %:z"
long: "%d/%m/%Y %H:%M:%S"
short: "%d %b %Hh%M"
time: "%H:%M:%S"
pm: pm

@ -2,7 +2,7 @@ Rails.application.routes.draw do
%i[https smtp xmpp tls ssh].each do |type|
namespace type, id: /[^\/]+/ do
get '/', action: :index
get ':id/', action: :show
get ':id/', action: :show, as: :show
get ':id/refresh', action: :refresh, as: :refresh
end
end
@ -16,8 +16,21 @@ Rails.application.routes.draw do
root 'site#index'
post '/' => 'site#check'
resources :statistics, only: %i[index show]
get 'sites' => 'site#sites'
resources :statistics, only: %i[index show] do
member do
get "ciphers", to: "statistics#ciphers"
get "tls", to: "statistics#tls"
end
end
%i[banks insurances gouv.fr].each do |name|
get name, controller: :sites, action: name
end
if Rails.env.development?
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'

</
@ -0,0 +1,67 @@
banks:
- admin.vybecard.com
- app.n26.com
- app.nickel.eu
- app.qonto.com
- clients.boursorama.com
- clients.cmavignon.com
- connexion-mabanque.bnpparibas
- ebanking-ch3.ubs.com
- epargnants.interepargne.natixis.fr
- espace-client.hellobank.fr
- espace-client.lanef.com
- espaceclient.axa.fr
- linxea-zen.avepargne.fr
- m.ing.fr
- mabanque.bnpparibas
- mabanque.fortuneo.fr
- mon.cmb.fr
- monespace.lcl.fr
- particuliers.societegenerale.fr
- secure.bforbank.com
- transatplan.banquetransatlantique.com
- voscomptesenligne.labanquepostale.fr
- www.altaprofits.com
- www.aviva.fr
- www.banque-rhone-alpes.fr
- www.banquepopulaire.fr
- www.bred.fr
- www.caisse-epargne.fr
- www.cic.fr
- www.credit-agricole.fr
- www.credit-cooperatif.coop
- www.creditmutuel.fr
- www.hsbc.fr
- www.ibps.sud.banquepopulaire.fr
- www.icgauth.banquebcp.fr
- www.labanquepostale.fr
- www.mgen.fr
- www.monabanq.com
- www.previ-direct.com
insurances:
- adherent.gie-afer.fr
- authentification.groupama.fr
- connect.axa.fr
- connect.maif.fr
- connect.sogarep.fr
- epargnant.amundi-ee.com
- espace-assure.gmf.fr
- espace-client.allianz.fr
- espace-client.mma.fr
- espace-personnel.direct-assurance.fr
- espaceperso.mutuelledesmotards.fr
- harmonie-et-moi.fr
- myswisslife.fr