Compare commits

...

3 Commits

  1. 3
      .rspec
  2. 4
      Gemfile
  3. 10
      Gemfile-2.3.lock
  4. 2
      Procfile
  5. 2
      Procfile.sidekiq
  6. 6
      app/controllers/check_controller.rb
  7. 2
      app/controllers/stats_controller.rb
  8. 69
      app/javascript/css/application.scss
  9. 1
      app/javascript/packs/application.js
  10. 99
      app/models/analysis.rb
  11. 13
      app/views/check/show.html.erb
  12. 13
      app/views/ssh/show.html.erb
  13. 167
      app/views/stats/index.html.erb
  14. 29
      bin/rspec
  15. 97
      bin/stats
  16. 8
      config/locales/en.yml
  17. 16
      config/locales/fr.yml
  18. 2
      config/routes.rb
  19. 16
      db/migrate/20220626133648_add_refresh_to_analysis.rb
  20. 4
      db/schema.rb
  21. 1
      package.json
  22. 55
      spec/models/analysis_spec.rb

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

@ -17,6 +17,9 @@ 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 'amazing_print'
gem 'uglifier'
gem 'sass-rails'
@ -44,7 +47,6 @@ end
group :development, :test do
gem 'pry-byebug'
gem 'amazing_print'
end
group :test do

@ -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,9 +18,9 @@ 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
flash[:warning] = "Merci d’attendre au moins #{l refresh_allowed} pour rafraîchir"
refresh_at = @analysis.refresh_at
if refresh_at || Time.now < refresh_at
flash[:warning] = "Merci d’attendre au moins #{l refresh_at} pour rafraîchir"
return redirect_to action: :show, id: @host
end
end

@ -0,0 +1,2 @@
class StatsController < ApplicationController
end

@ -87,6 +87,7 @@ td.error {
$color-critical: #d9534f;
$color-error: #e4804e;
$color-warning: #f0ad4e;
$color-effort: #6c757d;
$color-good: #beb052;
$color-best: #8db457;
$color-great: #5cb85c;
@ -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;
}

@ -1 +1,2 @@
import 'css/application'
import 'bootstrap/js/dist/tab'

@ -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

@ -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|

@ -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,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")

@ -49,29 +49,78 @@ class Analysis
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)
# 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 |name|
services = Analysis.where service: name, pending: false
grades = Hash.new 0
tls = %i[tls1_2_only tls1_2 tls ssl].to_h { [_1, 0] }
ciphers = %i[good bad].to_h { [_1, 0] }
pfs = %i[pfs_only pfs no_pfs].to_h { [_1, 0] }
progress = ProgressBar.create title: name, total: services.count,
format: '%t | %c/%u %W | %e'
services.find_in_batches do |batch|
batch.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
progress.increment
end
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
ap "grades_for_#{name}" => grades
Stat.create! "grades_for_#{name}", grades
ap "tls_for_#{name}" => tls
Stat.create! "tls_for_#{name}", tls
ap "ciphers_for_#{name}" => ciphers
Stat.create! "ciphers_for_#{name}", ciphers
ap "pfs_for_#{name}" => pfs
Stat.create! "pfs_for_#{name}", pfs
end

@ -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
@ -60,6 +61,19 @@ fr:
Fingerprint: Empreinte
"Fingerprint: %{fingerprint}": "Empreinte : %{fingerprint}"
stats:
index:
ciphers:
title: Suites de chiffrement supportées
good: Uniquement des sécurisées
bad: Au moins une non sécurisée
tls:
title: Versions de SSL/TLS supportées
tls1_2_only: TLSv1.2+
tls1_2: TLSv1.2 supporté
tls: TLSv1.0+ mais pas TLSv1.2
ssl: SSLv2 ou SSLv3 supporté
date:
abbr_day_names:
- dim
@ -262,5 +276,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

@ -16,7 +16,7 @@ Rails.application.routes.draw do
root 'site#index'
post '/' => 'site#check'
get 'sites' => 'site#sites'
resources :stats, only: %i[index]
%i[banks insurances gouv.fr].each do |name|
get name, controller: :sites, action: name

@ -0,0 +1,16 @@
class AddRefreshToAnalysis < ActiveRecord::Migration[7.0]
def up
add_column :analyses, :refresh_at, :timestamp
delay = Rails.configuration.refresh_delay
progress = ProgressBar.create total: Analysis.count, format: '%t (%c/%C) %W %e'
Analysis.all.each do |analysis|
analysis.update_column :refresh_at, analysis.updated_at + delay
progress.increment
end
end
def down
remove_column :analyses, :refresh_at
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_03_26_181216) do
ActiveRecord::Schema[7.0].define(version: 2022_06_26_133648) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -23,6 +23,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_03_26_181216) do
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.jsonb "args"
t.datetime "refresh_at", precision: nil
t.index ["service", "host", "args"], name: "index_analyses_on_service_and_host_and_args", unique: true
end
@ -32,5 +33,4 @@ ActiveRecord::Schema[7.0].define(version: 2022_03_26_181216) do
t.jsonb "data"
t.index ["name", "date"], name: "index_stats_on_name_and_date", unique: true
end
end

@ -5,6 +5,7 @@
"author": "aeris <aeris@imirhil.fr>",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@popperjs/core": "^2.11.5",
"@rails/webpacker": "5.4.3",
"bootstrap": "^5.1.3",
"font-awesome": "^4.7.0",

@ -0,0 +1,55 @@
require 'rails_helper'
describe Analysis do
describe '::find_refresh_delay' do
it 'must return host value' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=PT1M')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.minute
end
it 'must recurse domain value' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
expect(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=PT1M')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.minute
end
it 'must not test TLD' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
expect(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.hour
end
it 'must support debug mode' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=debug')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to be_nil
end
end
end
Loading…
Cancel
Save