diff --git a/CHANGELOG b/CHANGELOG index c272b13e9..8b28a2f46 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ [v#.#.#] ([month] [YYYY]) + - Show don't gate: Business Intelligence - UI: add dark mode support to the login and setup wizard pages - [entity]: - [future tense verb] [feature] diff --git a/app/assets/javascripts/hera.js b/app/assets/javascripts/hera.js index b1b2bf904..454f8051e 100644 --- a/app/assets/javascripts/hera.js +++ b/app/assets/javascripts/hera.js @@ -63,6 +63,7 @@ //= require hera/modules/uploads //= require hera/pages/activities +//= require hera/pages/bi_index //= require hera/pages/boards //= require hera/pages/issues //= require hera/pages/nodes/new_form diff --git a/app/assets/javascripts/hera/pages/bi_index.js b/app/assets/javascripts/hera/pages/bi_index.js new file mode 100644 index 000000000..7ae58fe88 --- /dev/null +++ b/app/assets/javascripts/hera/pages/bi_index.js @@ -0,0 +1,26 @@ +document.addEventListener('turbo:load', function () { + if ($('body.static_pages.bi_index').length) { + $(document).on('dradis:fetch turbo:frame-render', function (event) { + const $widgetFilter = $( + event.target.closest('[data-behavior~=fetch]') + ).find('[data-behavior~=widget-filter]'); + + if ($widgetFilter.length) { + if (event.type == 'turbo:frame-render') { + window.initBehaviors( + event.target + .closest('[data-behavior~=fetch]') + .querySelector('[data-behavior~=widget-content]') + ); + } + + $widgetFilter.off('change').on('change', function (e) { + const $container = $(e.target).parents('[data-behavior~=fetch]'); + $container.find('[data-behavior~=fetch-loader]').removeClass('d-none'); + $container.find('[data-behavior~=widget-content]').addClass('d-none'); + e.target.closest('form').requestSubmit(); + }); + } + }); + } +}); diff --git a/app/assets/stylesheets/hera/views.scss b/app/assets/stylesheets/hera/views.scss index 7bfede4e3..0c6325341 100644 --- a/app/assets/stylesheets/hera/views.scss +++ b/app/assets/stylesheets/hera/views.scss @@ -7,6 +7,7 @@ @import 'hera/views/noscript'; @import 'hera/views/projects'; @import 'hera/views/search'; +@import 'hera/views/static_pages'; @import 'hera/views/styles'; @import 'hera/views/uploads'; @import 'hera/views/versions'; diff --git a/app/assets/stylesheets/hera/views/_static_pages.scss b/app/assets/stylesheets/hera/views/_static_pages.scss new file mode 100644 index 000000000..90017489d --- /dev/null +++ b/app/assets/stylesheets/hera/views/_static_pages.scss @@ -0,0 +1,90 @@ +@import 'hera/variables'; + +.content-container { + &.chart { + .progress-wrap { + margin-bottom: 0.25rem; + + .issue-title { + margin-bottom: 0; + width: 25rem; + } + + .progress { + background-color: transparent; + border-radius: 0; + + .progress-bar { + background-color: var(--brand-bg); + border-radius: 0; + } + } + } + + } +} + +body.static_pages.bi_index { + .form-container { + flex-grow: 1; + margin-left: 1rem; + max-width: 12rem; + } + + .content-container { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 12.5rem; + + .bg-icon { + font-size: 2.5rem; + opacity: 0.08; + position: absolute; + right: 0; + top: 0; + + i { + color: var(--brand-bg); + } + } + + .delta { + align-items: center; + color: var(--text-muted); + display: flex; + font-size: 1.2rem; + margin-bottom: 0; + + &.negative { + color: var(--text-error); + } + + &.positive { + color: var(--text-success); + } + + &-icon { + font-size: 0.65rem; + margin: 0 0.25rem 0.1rem 0; + } + } + + .title { + line-height: 2.3rem; + } + + &.chart.property-item { + min-height: 18rem; + } + + &.chart.top-issues { + min-height: 26.5rem; + } + + &.pro-teaser { + color: var(--text-default); + cursor: pointer; + } + } +} diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 9666d7e8b..c1e76ac6c 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -1,7 +1,19 @@ class StaticPagesController < AuthenticatedController + before_action :set_bi_stats, only: [:bi_index] + before_action :set_bi_issues_data, only: [:bi_insights_issues, :bi_insights_top_issues] before_action :set_entries, only: [:issuelib_index, :issuelib_import] before_action :set_tickets, only: [:remediationtracker_index] + def bi_index; end + + def bi_insights_issues + render partial: 'static_pages/bi_issue_stats' + end + + def bi_insights_top_issues + render partial: 'static_pages/bi_top_issues' + end + def issuelib_index; end def issuelib_import @@ -17,47 +29,56 @@ def remediationtracker_index; end private - def set_entries - @entries = issuelib_entries - end - - def set_tickets - @tickets = tickets - end - - def tickets + def bi_top_project_properties [ { - title: 'SQL Injection in Login Form', - category: 'Security', - state: 'Open', - assignee: 'James T. Kirk', - due_at: Date.today - 1.week, - overdue: true + name: 'Web Application', + current_year_count: 12, + last_year_count: 9, + yoy_delta: 33, + top_issues: [ + { title: 'Cross-Site Scripting (XSS)', count: 12, percent: 100, color: '#d62728' }, + { title: 'SQL Injection', count: 10, percent: 83, color: '#d62728' }, + { title: 'Cross-Site Request Forgery (CSRF)', count: 7, percent: 58, color: '#ff7f0e' } + ] }, { - title: 'Outdated SSL Certificate', - category: 'Infrastructure', - state: 'In Progress', - assignee: 'Nyota Uhura', - due_at: Date.today, - overdue: true - }, + name: 'Network Pentest', + current_year_count: 6, + last_year_count: 8, + yoy_delta: -25, + top_issues: [ + { title: 'Security Misconfiguration', count: 6, percent: 100, color: '#ff7f0e' }, + { title: 'Outdated SSL/TLS Configuration', count: 4, percent: 67, color: '#ff7f0e' }, + { title: 'Missing HTTP Security Headers', count: 2, percent: 33, color: '#6baed6' } + ] + } + ] + end + + def bi_top_team_properties + [ { - title: 'Cross-Site Scripting in Search Bar', - category: 'Application', - state: 'On Hold', - assignee: 'Montgomery Scott', - due_at: Date.today + 1.week, - overdue: false + name: 'Financial Services', + current_year_count: 8, + last_year_count: 5, + yoy_delta: 60, + top_issues: [ + { title: 'SQL Injection', count: 8, percent: 100, color: '#d62728' }, + { title: 'Cross-Site Scripting (XSS)', count: 6, percent: 75, color: '#d62728' }, + { title: 'Broken Authentication', count: 4, percent: 50, color: '#9467bd' } + ] }, { - title: 'Missing HTTP Security Headers', - category: 'Compliance', - state: 'Closed', - assignee: nil, - due_at: Date.today + 1.month, - overdue: false + name: 'Healthcare', + current_year_count: 5, + last_year_count: 3, + yoy_delta: 67, + top_issues: [ + { title: 'Sensitive Data Exposure', count: 5, percent: 100, color: '#9467bd' }, + { title: 'Insufficient Logging', count: 3, percent: 60, color: '#6baed6' }, + { title: 'SQL Injection', count: 2, percent: 40, color: '#d62728' } + ] } ] end @@ -65,41 +86,129 @@ def tickets def issuelib_entries [ { - title: 'Auto-complete in password field', - state: 'published', + title: 'Auto-complete in password field', + state: 'published', created_at: Date.today - 3.months, updated_at: Date.today - 1.week }, { - title: 'DOM-based cross-site scripting (XSS)', - state: 'published', + title: 'DOM-based cross-site scripting (XSS)', + state: 'published', created_at: Date.today - 6.weeks, updated_at: Date.today - 3.days }, { - title: 'Insufficient cross-site request forgery (CSRF) protection', - state: 'published', + title: 'Insufficient cross-site request forgery (CSRF) protection', + state: 'published', created_at: Date.today - 2.months, updated_at: Date.today - 2.weeks }, { - title: 'Reflected cross-site scripting (XSS)', - state: 'published', + title: 'Reflected cross-site scripting (XSS)', + state: 'published', created_at: Date.today - 5.weeks, updated_at: Date.today - 4.days }, { - title: 'Insecure Direct Object Reference (IDOR)', - state: 'ready_for_review', + title: 'Insecure Direct Object Reference (IDOR)', + state: 'ready_for_review', created_at: Date.today - 2.weeks, updated_at: Date.today - 1.day }, { - title: 'Server-Side Request Forgery (SSRF)', - state: 'draft', + title: 'Server-Side Request Forgery (SSRF)', + state: 'draft', created_at: Date.today - 3.days, updated_at: Date.today } ] end + + def set_bi_issues_data + current_year_start = Time.current.beginning_of_year + last_year_start = 1.year.ago.beginning_of_year + last_year_end = 1.year.ago + + @selected_tag = params[:tag].presence + @selected_tag = nil unless @selected_tag && Tag.exists?(name: @selected_tag) + + filtered_issues = @selected_tag ? Issue.joins(:tags).where(tags: { name: @selected_tag }) : Issue.all + now = Time.current + + current_issues_count = filtered_issues.where(created_at: current_year_start..now).count + last_issues_count = filtered_issues.where(created_at: last_year_start..last_year_end).count + + @bi_issues = { + current_year_count: current_issues_count, + last_year_count: last_issues_count, + yoy_delta: yoy_delta(current_issues_count, last_issues_count) + } + @bi_top_issues = filtered_issues.where(created_at: current_year_start..now) + .includes(:tags) + .group_by(&:title) + .map { |title, group| { title: title, count: group.size, issue: group.first } } + .sort_by { |stats| -stats[:count] } + .first(10) + end + + def set_bi_stats + @bi_contributors = { current_year_count: 4, last_year_count: 3, yoy_delta: 33 } + @bi_projects = { current_year_count: 1, last_year_count: 1, yoy_delta: 0 } + @bi_teams = { current_year_count: 14, last_year_count: 11, yoy_delta: 27 } + @bi_top_project_properties = bi_top_project_properties + @bi_top_team_properties = bi_top_team_properties + end + + def set_entries + @entries = issuelib_entries + end + + def set_tickets + @tickets = tickets + end + + def tickets + [ + { + title: 'SQL Injection in Login Form', + category: 'Security', + state: 'Open', + assignee: 'James T. Kirk', + due_at: Date.today - 1.week, + overdue: true + }, + { + title: 'Outdated SSL Certificate', + category: 'Infrastructure', + state: 'In Progress', + assignee: 'Nyota Uhura', + due_at: Date.today, + overdue: true + }, + { + title: 'Cross-Site Scripting in Search Bar', + category: 'Application', + state: 'On Hold', + assignee: 'Montgomery Scott', + due_at: Date.today + 1.week, + overdue: false + }, + { + title: 'Missing HTTP Security Headers', + category: 'Compliance', + state: 'Closed', + assignee: nil, + due_at: Date.today + 1.month, + overdue: false + } + ] + end + + def yoy_delta(current, previous) + if previous.zero? + current > 0 ? 100 : 0 + else + ((current - previous).to_f / previous * 100).round + end + end end diff --git a/app/helpers/static_pages_helper.rb b/app/helpers/static_pages_helper.rb new file mode 100644 index 000000000..7833bbd0e --- /dev/null +++ b/app/helpers/static_pages_helper.rb @@ -0,0 +1,37 @@ +module StaticPagesHelper + def issue_bar_color(issue) + issue.tags.first&.color || 'var(--untagged-color)' + end + + def delta_icon(delta) + case + when delta > 0 then 'fa-arrow-up' + when delta < 0 then 'fa-arrow-down' + else 'fa-arrows-up-down' + end + end + + def tag_options(tags) + [ + [ + 'All', + nil, + { data: { + 'combobox-option-color': 'var(--text-default)', + 'combobox-option-icon': 'fa-solid fa-tags' + } } + ] + ] + tags.uniq(&:name).map do |tag| + [ + tag.display_name, + tag.name, + { + data: { + 'combobox-option-color': tag.color, + 'combobox-option-icon': 'fa-solid fa-tag' + } + } + ] + end + end +end diff --git a/app/views/layouts/hera/navbar/main_nav/_tools_menu.html.erb b/app/views/layouts/hera/navbar/main_nav/_tools_menu.html.erb index a7dec21ae..775f57a42 100644 --- a/app/views/layouts/hera/navbar/main_nav/_tools_menu.html.erb +++ b/app/views/layouts/hera/navbar/main_nav/_tools_menu.html.erb @@ -8,6 +8,7 @@ <%= render_view_hooks('tools_menu') %> <% if !defined?(Dradis::Pro) %>
created this year
+from <%= @bi_issues[:last_year_count] %> during Jan 1 - <%= Date.today.prev_year.strftime('%b %d %Y') %>
+<%= issue[:title] %>
+No issues created this year yet.
+ <% end %> +from <%= property[:last_year_count] %> during Jan 1 - <%= Date.today.prev_year.strftime('%b %d %Y') %>
+<%= issue[:title] %>
+from <%= property[:last_year_count] %> during Jan 1 - <%= Date.today.prev_year.strftime('%b %d %Y') %>
+<%= issue[:title] %>
+The BI Dashboard surfaces stats and analytics across your engagements so you can identify trends and draw conclusions from your security data.
+Get an overview of your Projects and Issues from the current year, and surface your most common findings to prioritize your security investments.
+More about Business Intelligence.
+created this year
+from <%= @bi_projects[:last_year_count] %> during Jan 1 - <%= Date.today.prev_year.strftime('%b %d %Y') %>
+