Statistics pages

Norore 9 months ago committed by aeris
parent 4280db5e4c
commit b79153d477
  1. 3
      .rspec
  2. 23
      app/controllers/statistics_controller.rb
  3. 14
      app/controllers/stats_controller.rb
  4. 2
      app/helpers/statistics_helper.rb
  5. 69
      app/javascript/css/application.scss
  6. 40
      app/javascript/js/stats/grades.js
  7. 72
      app/javascript/js/stats/index.js
  8. 7
      app/javascript/packs/application.js
  9. 24
      app/lib/matomo.rb
  10. 7
      app/views/statistics/grade.html.erb
  11. 164
      app/views/statistics/index.html.erb
  12. 7
      app/views/stats/grade.html.erb
  13. 167
      app/views/stats/index.html.erb
  14. 29
      bin/rspec
  15. 39
      bin/stats
  16. 9
      config/routes.rb
  17. 1
      db/schema.rb
  18. 3
      package.json

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

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

@ -0,0 +1,2 @@
module StatisticsHelper
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;
}

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

@ -0,0 +1,24 @@
class Matomo
module Helpers
def matomo_tag
config = Matomo
return unless config.enabled?
render partial: 'matomo', locals: { config: config }
end
end
cattr_reader :url, :path, :site
def self.load
@@url = ENV['MATOMO_URL']
@@site = ENV['MATOMO_SITE']
@@disabled = ENV['MATOMO_DISABLED']
end
def self.enabled?
@@disabled.nil? && self.url && self.site
end
ActionView::Base.include Helpers
self.load
end

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

@ -75,3 +75,42 @@ sites.each do |type, domains|
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

@ -16,8 +16,17 @@ 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

@ -33,5 +33,4 @@ ActiveRecord::Schema[7.0].define(version: 2022_06_26_133648) do
t.jsonb "data"
t.index ["name", "date"], name: "index_stats_on_name_and_date", unique: true
end
end

@ -5,8 +5,11 @@
"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",
"chart.js": "^3.7.1",
"chartjs-plugin-datalabels": "^2.0.0",
"font-awesome": "^4.7.0",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12"

Loading…
Cancel
Save