Commit 7581879a authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by James Fargher

Add initial statistics for WAF installations

parent a7298fff
......@@ -17,6 +17,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
include UsageStatistics
default_value_for :ingress_type, :nginx
default_value_for :modsecurity_enabled, true
......@@ -29,6 +30,10 @@ module Clusters
enum modsecurity_mode: { logging: 0, blocking: 1 }
scope :modsecurity_not_installed, -> { where(modsecurity_enabled: nil) }
scope :modsecurity_enabled, -> { where(modsecurity_enabled: true) }
scope :modsecurity_disabled, -> { where(modsecurity_enabled: false) }
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
......
......@@ -35,6 +35,7 @@ module Clusters
has_many :projects, through: :cluster_projects, class_name: '::Project'
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
has_many :deployments, inverse_of: :cluster
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
......
# frozen_string_literal: true
# rubocop: disable CodeReuse/ActiveRecord
module Clusters
module Applications
##
# This service measures usage of the Modsecurity Web Application Firewall across the entire
# instance's deployed environments.
#
# The default configuration is`AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE=DetectionOnly` so we
# measure non-default values via definition of either ci_variables or ci_pipeline_variables.
# Since both these values are encrypted, we must decrypt and count them in memory.
#
# NOTE: this service is an approximation as it does not yet take into account `environment_scope` or `ci_group_variables`.
##
class IngressModsecurityUsageService
ADO_MODSEC_KEY = "AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE"
def initialize(blocking_count: 0, disabled_count: 0)
@blocking_count = blocking_count
@disabled_count = disabled_count
end
def execute
conditions = -> { merge(::Environment.available).merge(::Deployment.success).where(key: ADO_MODSEC_KEY) }
ci_pipeline_var_enabled =
::Ci::PipelineVariable
.joins(pipeline: { environments: :last_visible_deployment })
.merge(conditions)
.order('deployments.environment_id, deployments.id DESC')
ci_var_enabled =
::Ci::Variable
.joins(project: { environments: :last_visible_deployment })
.merge(conditions)
.merge(
# Give priority to pipeline variables by excluding from dataset
::Ci::Variable.joins(project: :environments).where.not(
environments: { id: ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) deployments.environment_id') }
)
).select('DISTINCT ON (deployments.environment_id) ci_variables.*')
sum_modsec_config_counts(
ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) ci_pipeline_variables.*')
)
sum_modsec_config_counts(ci_var_enabled)
{
ingress_modsecurity_blocking: @blocking_count,
ingress_modsecurity_disabled: @disabled_count
}
end
private
# These are encrypted so we must decrypt and count in memory
def sum_modsec_config_counts(dataset)
dataset.each do |var|
case var.value
when "On" then @blocking_count += 1
when "Off" then @disabled_count += 1
# `else` could be default or any unsupported user input
end
end
end
end
end
end
---
title: Add indexes on ingress, enabled clusters and successful deployments
merge_request: 28331
author:
type: performance
# frozen_string_literal: true
class AddIndexOnModsecurityToIngress < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_clusters_applications_ingress_on_modsecurity'
disable_ddl_transaction!
def up
add_concurrent_index :clusters_applications_ingress, [:modsecurity_enabled, :modsecurity_mode, :cluster_id], name: INDEX_NAME
end
def down
remove_concurrent_index :clusters_applications_ingress, [:modsecurity_enabled, :modsecurity_mode, :cluster_id], name: INDEX_NAME
end
end
# frozen_string_literal: true
class AddIndexOnSuccessfulDeploymentAndEnvironmentIdToDeployments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_successful_deployments_on_cluster_id_and_environment_id'
disable_ddl_transaction!
def up
add_concurrent_index :deployments, [:cluster_id, :environment_id], where: 'status = 2', name: INDEX_NAME
end
def down
remove_concurrent_index :deployments, [:cluster_id, :environment_id], where: 'status = 2', name: INDEX_NAME
end
end
# frozen_string_literal: true
class AddIndexOnEnabledClusters < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_enabled_clusters_on_id'
disable_ddl_transaction!
def up
add_concurrent_index :clusters, [:id], where: 'enabled = true', name: INDEX_NAME
end
def down
remove_concurrent_index :clusters, [:id], where: 'enabled = true', name: INDEX_NAME
end
end
......@@ -9289,6 +9289,8 @@ CREATE UNIQUE INDEX index_clusters_applications_helm_on_cluster_id ON public.clu
CREATE UNIQUE INDEX index_clusters_applications_ingress_on_cluster_id ON public.clusters_applications_ingress USING btree (cluster_id);
CREATE INDEX index_clusters_applications_ingress_on_modsecurity ON public.clusters_applications_ingress USING btree (modsecurity_enabled, modsecurity_mode, cluster_id);
CREATE UNIQUE INDEX index_clusters_applications_jupyter_on_cluster_id ON public.clusters_applications_jupyter USING btree (cluster_id);
CREATE INDEX index_clusters_applications_jupyter_on_oauth_application_id ON public.clusters_applications_jupyter USING btree (oauth_application_id);
......@@ -9421,6 +9423,8 @@ CREATE UNIQUE INDEX index_emails_on_email ON public.emails USING btree (email);
CREATE INDEX index_emails_on_user_id ON public.emails USING btree (user_id);
CREATE INDEX index_enabled_clusters_on_id ON public.clusters USING btree (id) WHERE (enabled = true);
CREATE INDEX index_environments_on_auto_stop_at ON public.environments USING btree (auto_stop_at) WHERE (auto_stop_at IS NOT NULL);
CREATE INDEX index_environments_on_name_varchar_pattern_ops ON public.environments USING btree (name varchar_pattern_ops);
......@@ -10525,6 +10529,8 @@ CREATE INDEX index_subscriptions_on_project_id ON public.subscriptions USING btr
CREATE UNIQUE INDEX index_subscriptions_on_subscribable_and_user_id_and_project_id ON public.subscriptions USING btree (subscribable_id, subscribable_type, user_id, project_id);
CREATE INDEX index_successful_deployments_on_cluster_id_and_environment_id ON public.deployments USING btree (cluster_id, environment_id) WHERE (status = 2);
CREATE UNIQUE INDEX index_suggestions_on_note_id_and_relative_order ON public.suggestions USING btree (note_id, relative_order);
CREATE UNIQUE INDEX index_system_note_metadata_on_description_version_id ON public.system_note_metadata USING btree (description_version_id) WHERE (description_version_id IS NOT NULL);
......@@ -13542,6 +13548,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200401095430
20200401211005
20200402001106
20200402115013
20200402115623
20200402123926
20200402124802
20200402135250
......@@ -13607,6 +13615,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200420104303
20200420104323
20200420115948
20200420141733
20200420162730
20200420172113
20200420172752
......
......@@ -304,8 +304,10 @@ but commented out to help encourage others to add to it in the future. -->
|licenses_list_views|counts||
|user_preferences_group_overview_details|counts||
|user_preferences_group_overview_security_dashboard|counts||
|ingress_modsecurity_logging|counts||
|ingress_modsecurity_blocking|counts||
|ingress_modsecurity_disabled|counts||
|ingress_modsecurity_not_installed|counts||
|dependency_list_usages_total|counts||
|epics|counts||
|feature_flags|counts||
......
......@@ -91,11 +91,17 @@ module Gitlab
def batch_fetch(start, finish, mode)
# rubocop:disable GitlabSecurity/PublicSend
@relation.select(@column).public_send(mode).where(@column => start..(finish - 1)).count
@relation.select(@column).public_send(mode).where(between_condition(start, finish)).count
end
private
def between_condition(start, finish)
return @column.between(start..(finish - 1)) if @column.is_a?(Arel::Attributes::Attribute)
{ @column => start..(finish - 1) }
end
def actual_start(start)
start || @relation.minimum(@column) || 0
end
......
......@@ -273,7 +273,24 @@ module Gitlab
end
def ingress_modsecurity_usage
::Clusters::Applications::IngressModsecurityUsageService.new.execute
##
# This method measures usage of the Modsecurity Web Application Firewall across the entire
# instance's deployed environments.
#
# NOTE: this service is an approximation as it does not yet take into account if environment
# is enabled and only measures applications installed using GitLab Managed Apps (disregards
# CI-based managed apps).
#
# More details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28331#note_318621786
##
column = ::Deployment.arel_table[:environment_id]
{
ingress_modsecurity_logging: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.logging), column),
ingress_modsecurity_blocking: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.blocking), column),
ingress_modsecurity_disabled: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_disabled), column),
ingress_modsecurity_not_installed: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_not_installed), column)
}
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -318,6 +335,13 @@ module Gitlab
rescue ActiveRecord::StatementInvalid
{ projects_jira_server_active: -1, projects_jira_cloud_active: -1, projects_jira_active: -1 }
end
def successful_deployments_with_cluster(scope)
scope
.joins(cluster: :deployments)
.merge(Clusters::Cluster.enabled)
.merge(Deployment.success)
end
# rubocop: enable CodeReuse/ActiveRecord
def jira_import_usage
......
......@@ -77,6 +77,24 @@ FactoryBot.define do
trait :no_helm_installed do
cluster factory: %i(cluster provided_by_gcp)
end
trait :modsecurity_blocking do
modsecurity_enabled { true }
modsecurity_mode { :blocking }
end
trait :modsecurity_logging do
modsecurity_enabled { true }
modsecurity_mode { :logging }
end
trait :modsecurity_disabled do
modsecurity_enabled { false }
end
trait :modsecurity_not_installed do
modsecurity_enabled { nil }
end
end
factory :clusters_applications_cert_manager, class: 'Clusters::Applications::CertManager' do
......
......@@ -35,6 +35,10 @@ describe Gitlab::Database::BatchCount do
expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(5)
end
it 'counts with Arel column' do
expect(described_class.batch_count(model, model.arel_table[:id])).to eq(5)
end
it 'counts table with batch_size 50K' do
expect(described_class.batch_count(model, batch_size: 50_000)).to eq(5)
end
......@@ -98,6 +102,10 @@ describe Gitlab::Database::BatchCount do
expect(described_class.batch_distinct_count(model, "#{model.table_name}.#{column}")).to eq(2)
end
it 'counts with Arel column' do
expect(described_class.batch_distinct_count(model, model.arel_table[column])).to eq(2)
end
it 'counts with :column field with batch_size of 50K' do
expect(described_class.batch_distinct_count(model, column, batch_size: 50_000)).to eq(2)
end
......
......@@ -319,18 +319,132 @@ describe Gitlab::UsageData, :aggregate_failures do
describe '#ingress_modsecurity_usage' do
subject { described_class.ingress_modsecurity_usage }
it 'gathers variable data' do
allow_any_instance_of(
::Clusters::Applications::IngressModsecurityUsageService
).to receive(:execute).and_return(
{
ingress_modsecurity_blocking: 1,
ingress_modsecurity_disabled: 2
}
)
let(:environment) { create(:environment) }
let(:project) { environment.project }
let(:environment_scope) { '*' }
let(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project]) }
let(:ingress_mode) { :modsecurity_blocking }
let!(:ingress) { create(:clusters_applications_ingress, ingress_mode, cluster: cluster) }
context 'when cluster is disabled' do
let(:cluster) { create(:cluster, :disabled, projects: [project]) }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'when deployment is unsuccessful' do
let!(:deployment) { create(:deployment, :failed, environment: environment, project: project, cluster: cluster) }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'when deployment is successful' do
let!(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
context 'when modsecurity is in blocking mode' do
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(1)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'when modsecurity is in logging mode' do
let(:ingress_mode) { :modsecurity_logging }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(1)
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'when modsecurity is disabled' do
let(:ingress_mode) { :modsecurity_disabled }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(1)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'when modsecurity is not installed' do
let(:ingress_mode) { :modsecurity_not_installed }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(1)
end
end
context 'with multiple projects' do
let(:environment_2) { create(:environment) }
let(:project_2) { environment_2.project }
let(:cluster_2) { create(:cluster, environment_scope: environment_scope, projects: [project_2]) }
let!(:ingress_2) { create(:clusters_applications_ingress, :modsecurity_logging, cluster: cluster_2) }
let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster_2) }
it 'gathers non-duplicated ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(1)
expect(subject[:ingress_modsecurity_blocking]).to eq(1)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'with multiple deployments' do
let!(:deployment_2) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
it 'gathers non-duplicated ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(1)
expect(subject[:ingress_modsecurity_disabled]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'with multiple projects' do
let(:environment_2) { create(:environment) }
let(:project_2) { environment_2.project }
let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster) }
let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project, project_2]) }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
context 'with multiple environments' do
let!(:environment_2) { create(:environment, project: project) }
let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project, cluster: cluster) }
it 'gathers ingress data' do
expect(subject[:ingress_modsecurity_logging]).to eq(0)
expect(subject[:ingress_modsecurity_blocking]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::IngressModsecurityUsageService do
describe '#execute' do
ADO_MODSEC_KEY = Clusters::Applications::IngressModsecurityUsageService::ADO_MODSEC_KEY
let(:project_with_ci_var) { create(:environment).project }
let(:project_with_pipeline_var) { create(:environment).project }
subject { described_class.new.execute }
context 'with multiple projects' do
let(:pipeline1) { create(:ci_pipeline, :with_job, project: project_with_pipeline_var) }
let(:pipeline2) { create(:ci_pipeline, :with_job, project: project_with_ci_var) }
let!(:deployment_with_pipeline_var) do
create(
:deployment,
:success,
environment: project_with_pipeline_var.environments.first,
project: project_with_pipeline_var,
deployable: pipeline1.builds.last
)
end
let!(:deployment_with_project_var) do
create(
:deployment,
:success,
environment: project_with_ci_var.environments.first,
project: project_with_ci_var,
deployable: pipeline2.builds.last
)
end
context 'mixed data' do
let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, key: ADO_MODSEC_KEY, value: "On") }
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, key: ADO_MODSEC_KEY, value: "Off") }
it 'gathers variable data' do
expect(subject[:ingress_modsecurity_blocking]).to eq(1)
expect(subject[:ingress_modsecurity_disabled]).to eq(1)
end
end
context 'blocking' do
let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "On" } }
let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, **modsec_values) }
it 'gathers variable data' do
expect(subject[:ingress_modsecurity_blocking]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
end
end
context 'disabled' do
let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "Off" } }
let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, **modsec_values) }
it 'gathers variable data' do
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(2)
end
end
end
context 'when set as both ci and pipeline variables' do
let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "Off" } }
let(:pipeline) { create(:ci_pipeline, :with_job, project: project_with_ci_var) }
let!(:deployment) do
create(
:deployment,
:success,
environment: project_with_ci_var.environments.first,
project: project_with_ci_var,
deployable: pipeline.builds.last
)
end
let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline, **modsec_values) }
it 'wont double-count projects' do
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(1)
end
it 'gives precedence to pipeline variable' do
pipeline_variable.update(value: "On")
expect(subject[:ingress_modsecurity_blocking]).to eq(1)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
end
end
context 'when a project has multiple environments' do
let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "On" } }
let!(:env1) { project_with_pipeline_var.environments.first }
let!(:env2) { create(:environment, project: project_with_pipeline_var) }
let!(:pipeline_with_2_deployments) do
create(:ci_pipeline, :with_job, project: project_with_ci_var).tap do |pip|
pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
end
end
let!(:deployment1) do
create(
:deployment,
:success,
environment: env1,
project: project_with_pipeline_var,
deployable: pipeline_with_2_deployments.builds.last
)
end
let!(:deployment2) do
create(
:deployment,
:success,
environment: env2,
project: project_with_pipeline_var,
deployable: pipeline_with_2_deployments.builds.last
)
end
context 'when set as ci variable' do
let!(:ci_variable) { create(:ci_variable, project: project_with_pipeline_var, **modsec_values) }
it 'gathers variable data' do
expect(subject[:ingress_modsecurity_blocking]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
end
end
context 'when set as pipeline variable' do
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_with_2_deployments, **modsec_values) }
it 'gathers variable data' do
expect(subject[:ingress_modsecurity_blocking]).to eq(2)
expect(subject[:ingress_modsecurity_disabled]).to eq(0)
end
end
end
context 'when an environment has multiple deployments' do
let!(:env) { project_with_pipeline_var.environments.first }
let!(:pipeline_first) do
create(:ci_pipeline, :with_job, project: project_with_pipeline_var).tap do |pip|
pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
end
end
let!(:pipeline_last) do
create(:ci_pipeline, :with_job, project: project_with_pipeline_var).tap do |pip|
pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
end
end
let!(:deployment_first) do
create(
:deployment,
:success,
environment: env,
project: project_with_pipeline_var,
deployable: pipeline_first.builds.last
)
end
let!(:deployment_last) do
create(
:deployment,
:success,
environment: env,
project: project_with_pipeline_var,
deployable: pipeline_last.builds.last
)
end
context 'when set as pipeline variable' do
let!(:first_pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_first, key: ADO_MODSEC_KEY, value: "On") }
let!(:last_pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_last, key: ADO_MODSEC_KEY, value: "Off") }
it 'gives precedence to latest deployment' do
expect(subject[:ingress_modsecurity_blocking]).to eq(0)
expect(subject[:ingress_modsecurity_disabled]).to eq(1)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment