diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cfe2f8cbb3f..94df809a50b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-9e6f5f40e6eb44655b6acfd5dc222af04333a4f2
+521bb978da8780aca690136e78a3ad388726c8ad
diff --git a/Gemfile b/Gemfile
index e0820947158..9040737ff50 100644
--- a/Gemfile
+++ b/Gemfile
@@ -440,7 +440,7 @@ gem 'activerecord-explain-analyze', '~> 0.1', require: false
gem 'oauth2', '~> 1.4'
# Health check
-gem 'health_check', '~> 2.6.0'
+gem 'health_check', '~> 3.0'
# System information
gem 'vmstat', '~> 2.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index fbe5cfff1f1..54e21068b29 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -518,8 +518,8 @@ GEM
hashie (3.6.0)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
- health_check (2.6.0)
- rails (>= 4.0)
+ health_check (3.0.0)
+ railties (>= 5.0)
heapy (0.1.4)
hipchat (1.5.2)
httparty
@@ -1283,7 +1283,7 @@ DEPENDENCIES
hamlit (~> 2.11.0)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
- health_check (~> 2.6.0)
+ health_check (~> 3.0)
hipchat (~> 1.5.0)
html-pipeline (~> 2.12)
html2text
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index d61e6995551..2348e0719ca 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -3,6 +3,7 @@ import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
import icon from '../../vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
@@ -10,6 +11,7 @@ export default {
totalTime,
limitWarning,
icon,
+ GlIcon,
},
props: {
items: {
@@ -52,7 +54,8 @@ export default {
- {{ mergeRequest.state.toUpperCase() }}
+
+ {{ __('CLOSED') }}
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index f4fe605f0a2..ef4d5338046 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -5,7 +5,6 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-users-over-license-callout',
'.js-admin-licensed-user-count-threshold',
'.js-buy-pipeline-minutes-notification-callout',
- '.js-alerts-moved-alert',
'.js-token-expiry-callout',
];
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index db40b0bed77..e4e7b26720d 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -9,7 +9,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :authorize_read_group!, only: :index
before_action :find_todos, only: [:index, :destroy_all]
- track_unique_visits :index, target_id: 'u_analytics_todos'
+ track_unique_visits :index, target_id: 'u_todos'
def index
@sort = params[:sort]
diff --git a/app/models/event.rb b/app/models/event.rb
index 56d7742c51a..92609144576 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -8,6 +8,7 @@ class Event < ApplicationRecord
include CreatedAtFilterable
include Gitlab::Utils::StrongMemoize
include UsageStatistics
+ include ShaAttribute
default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
@@ -48,6 +49,8 @@ class Event < ApplicationRecord
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
+ sha_attribute :fingerprint
+
enum action: ACTIONS, _suffix: true
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
@@ -82,6 +85,10 @@ class Event < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') }
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
+ scope :for_fingerprint, ->(fingerprint) do
+ fingerprint.present? ? where(fingerprint: fingerprint) : none
+ end
+ scope :for_action, ->(action) { where(action: action) }
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 6a4b4472a03..3dc5caf6119 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -98,6 +98,7 @@ class WikiPage
def slug
attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
end
+ alias_method :id, :slug # required to use build_stubbed
alias_method :to_param, :slug
@@ -265,8 +266,8 @@ class WikiPage
'../shared/wikis/wiki_page'
end
- def id
- page.version.to_s
+ def sha
+ page.version&.sha
end
def title_changed?
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index ad36fe70b3a..abbb68aaf94 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -100,25 +100,21 @@ class EventCreateService
# @param [WikiPage::Meta] wiki_page_meta The event target
# @param [User] author The event author
# @param [Symbol] action One of the Event::WIKI_ACTIONS
+ # @param [String] fingerprint The de-duplication fingerprint
#
- # @return a tuple of event and either :found or :created
- def wiki_event(wiki_page_meta, author, action)
+ # The fingerprint, if provided, should be sufficient to find duplicate events.
+ # Suitable values would be, for example, the current page SHA.
+ #
+ # @return [Event] the event
+ def wiki_event(wiki_page_meta, author, action, fingerprint)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- if duplicate = existing_wiki_event(wiki_page_meta, action)
- return duplicate
- end
-
- event = create_record_event(wiki_page_meta, author, action)
- # Ensure that the event is linked in time to the metadata, for non-deletes
- unless event.destroyed_action?
- time_stamp = wiki_page_meta.updated_at
- event.update_columns(updated_at: time_stamp, created_at: time_stamp)
- end
-
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
- event
+ duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
+ return duplicate if duplicate.present?
+
+ create_record_event(wiki_page_meta, author, action, fingerprint.presence)
end
def approve_mr(merge_request, current_user)
@@ -127,44 +123,37 @@ class EventCreateService
private
- def existing_wiki_event(wiki_page_meta, action)
- if Event.actions.fetch(action) == Event.actions[:destroyed]
- most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first
- return most_recent if most_recent.present? && Event.actions[most_recent.action] == Event.actions[action]
- else
- Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first
- end
- end
-
- def create_record_event(record, current_user, status)
+ def create_record_event(record, current_user, status, fingerprint = nil)
create_event(record.resource_parent, current_user, status,
- target_id: record.id, target_type: record.class.name)
+ fingerprint: fingerprint,
+ target_id: record.id,
+ target_type: record.class.name)
end
# If creating several events, this method will insert them all in a single
# statement
#
- # @param [[Eventable, Symbol]] a list of pairs of records and a valid status
+ # @param [[Eventable, Symbol, String]] a list of tuples of records, a valid status, and fingerprint
# @param [User] the author of the event
- def create_record_events(pairs, current_user)
+ def create_record_events(tuples, current_user)
base_attrs = {
created_at: Time.now.utc,
updated_at: Time.now.utc,
author_id: current_user.id
}
- attribute_sets = pairs.map do |record, status|
+ attribute_sets = tuples.map do |record, status, fingerprint|
action = Event.actions[status]
raise IllegalActionError, "#{status} is not a valid status" if action.nil?
parent_attrs(record.resource_parent)
.merge(base_attrs)
- .merge(action: action, target_id: record.id, target_type: record.class.name)
+ .merge(action: action, fingerprint: fingerprint, target_id: record.id, target_type: record.class.name)
end
result = Event.insert_all(attribute_sets, returning: %w[id])
- pairs.each do |record, status|
+ tuples.each do |record, status, _|
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: status, event_target: record.class, author_id: current_user.id)
end
@@ -198,7 +187,11 @@ class EventCreateService
)
attributes.merge!(parent_attrs(resource_parent))
- Event.create!(attributes)
+ if attributes[:fingerprint].present?
+ Event.safe_find_or_create_by!(attributes)
+ else
+ Event.create!(attributes)
+ end
end
def parent_attrs(resource_parent)
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index b3937a10a70..f9de72f2d5f 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -41,7 +41,12 @@ module Git
end
def create_event_for(change)
- event_service.execute(change.last_known_slug, change.page, change.event_action)
+ event_service.execute(
+ change.last_known_slug,
+ change.page,
+ change.event_action,
+ change.sha
+ )
end
def event_service
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
index 14e622dd147..562c43487e9 100644
--- a/app/services/git/wiki_push_service/change.rb
+++ b/app/services/git/wiki_push_service/change.rb
@@ -33,6 +33,10 @@ module Git
strip_extension(raw_change.old_path || raw_change.new_path)
end
+ def sha
+ change[:newrev]
+ end
+
private
attr_reader :raw_change, :change, :wiki
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 2967684f7bc..fd234630633 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -44,7 +44,9 @@ module WikiPages
end
def create_wiki_event(page)
- response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
+ response = WikiPages::EventCreateService
+ .new(current_user)
+ .execute(slug_for_page(page), page, event_action, fingerprint(page))
log_error(response.message) if response.error?
end
@@ -52,6 +54,10 @@ module WikiPages
def slug_for_page(page)
page.slug
end
+
+ def fingerprint(page)
+ page.sha
+ end
end
end
diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb
index d59c27bb92a..ab5abe1c82b 100644
--- a/app/services/wiki_pages/destroy_service.rb
+++ b/app/services/wiki_pages/destroy_service.rb
@@ -21,5 +21,9 @@ module WikiPages
def event_action
:destroyed
end
+
+ def fingerprint(page)
+ page.wiki.repository.head_commit.sha
+ end
end
end
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
index 0453c90d693..ebfc2414f9e 100644
--- a/app/services/wiki_pages/event_create_service.rb
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -9,11 +9,11 @@ module WikiPages
@author = author
end
- def execute(slug, page, action)
+ def execute(slug, page, action, event_fingerprint)
event = Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
- ::EventCreateService.new.wiki_event(wiki_page_meta, author, action)
+ ::EventCreateService.new.wiki_event(wiki_page_meta, author, action, event_fingerprint)
end
ServiceResponse.success(payload: { event: event })
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index d3e98bac7f9..567dbbcd498 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -45,13 +45,13 @@
= _('MERGED')
- elsif merge_request.closed?
%li.issuable-status.d-none.d-sm-inline-block
- = icon('ban')
+ = sprite_icon('cancel', size: 16, css_class: 'gl-vertical-align-text-bottom')
= _('CLOSED')
= render 'shared/merge_request_pipeline_status', merge_request: merge_request
- if merge_request.open? && merge_request.broken?
%li.issuable-pipeline-broken.d-none.d-sm-flex
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
- = icon('exclamation-triangle')
+ = sprite_icon('warning-solid', size: 16)
- if merge_request.assignees.any?
%li.d-flex
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
diff --git a/app/views/projects/services/alerts/_top.html.haml b/app/views/projects/services/alerts/_top.html.haml
index ebc93978832..ff903864370 100644
--- a/app/views/projects/services/alerts/_top.html.haml
+++ b/app/views/projects/services/alerts/_top.html.haml
@@ -1,6 +1,6 @@
.row
.col-lg-12
- .gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
+ .gl-alert.gl-alert-info{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.')
diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml
index 338414be5ab..56dfa951804 100644
--- a/app/views/projects/services/prometheus/_top.html.haml
+++ b/app/views/projects/services/prometheus/_top.html.haml
@@ -2,7 +2,7 @@
.row
.col-lg-12
- .gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
+ .gl-alert.gl-alert-info{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
diff --git a/changelogs/unreleased/225928-replace-fa-ban-icons-with-gitlab-svg-cancel-icon.yml b/changelogs/unreleased/225928-replace-fa-ban-icons-with-gitlab-svg-cancel-icon.yml
new file mode 100644
index 00000000000..e916ff86347
--- /dev/null
+++ b/changelogs/unreleased/225928-replace-fa-ban-icons-with-gitlab-svg-cancel-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-ban icons with "cancel" from GitLab SVG
+merge_request: 37067
+author:
+type: changed
diff --git a/changelogs/unreleased/ajk-event-fingerprints.yml b/changelogs/unreleased/ajk-event-fingerprints.yml
new file mode 100644
index 00000000000..92ba107d1c3
--- /dev/null
+++ b/changelogs/unreleased/ajk-event-fingerprints.yml
@@ -0,0 +1,5 @@
+---
+title: Use fingerprint column on events to ensure event uniqueness
+merge_request: 31021
+author:
+type: changed
diff --git a/changelogs/unreleased/telemetry-updates-to-analytics-caccomulator.yml b/changelogs/unreleased/telemetry-updates-to-analytics-caccomulator.yml
new file mode 100644
index 00000000000..fdcf907cb3c
--- /dev/null
+++ b/changelogs/unreleased/telemetry-updates-to-analytics-caccomulator.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude todos from general analytics accumulator ping
+merge_request: 36813
+author:
+type: changed
diff --git a/db/migrate/20200504191813_add_fingerprint_to_events.rb b/db/migrate/20200504191813_add_fingerprint_to_events.rb
new file mode 100644
index 00000000000..1171d548f10
--- /dev/null
+++ b/db/migrate/20200504191813_add_fingerprint_to_events.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class AddFingerprintToEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:events, :fingerprint)
+ with_lock_retries { add_column :events, :fingerprint, :binary }
+ end
+
+ unless check_constraint_exists?(:events, constraint_name)
+ add_check_constraint(
+ :events,
+ "octet_length(fingerprint) <= 128",
+ constraint_name,
+ validate: true
+ )
+ end
+ end
+
+ def down
+ remove_check_constraint(:events, constraint_name)
+
+ if column_exists?(:events, :fingerprint)
+ with_lock_retries { remove_column :events, :fingerprint }
+ end
+ end
+
+ def constraint_name
+ check_constraint_name(:events, :fingerprint, 'max_length')
+ end
+end
diff --git a/db/migrate/20200504200709_add_index_on_fingerprint_and_target_type_to_events.rb b/db/migrate/20200504200709_add_index_on_fingerprint_and_target_type_to_events.rb
new file mode 100644
index 00000000000..9b06d593300
--- /dev/null
+++ b/db/migrate/20200504200709_add_index_on_fingerprint_and_target_type_to_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexOnFingerprintAndTargetTypeToEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ KEYS = [:target_type, :target_id, :fingerprint]
+
+ def up
+ add_concurrent_index :events, KEYS, using: :btree, unique: true
+ end
+
+ def down
+ remove_concurrent_index :events, KEYS
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 851ec9c6575..0b1ea380f61 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11405,7 +11405,9 @@ CREATE TABLE public.events (
updated_at timestamp with time zone NOT NULL,
action smallint NOT NULL,
target_type character varying,
- group_id bigint
+ group_id bigint,
+ fingerprint bytea,
+ CONSTRAINT check_97e06e05ad CHECK ((octet_length(fingerprint) <= 128))
);
CREATE SEQUENCE public.events_id_seq
@@ -19327,6 +19329,8 @@ CREATE INDEX index_events_on_project_id_and_id ON public.events USING btree (pro
CREATE INDEX index_events_on_target_type_and_target_id ON public.events USING btree (target_type, target_id);
+CREATE UNIQUE INDEX index_events_on_target_type_and_target_id_and_fingerprint ON public.events USING btree (target_type, target_id, fingerprint);
+
CREATE INDEX index_evidences_on_release_id ON public.evidences USING btree (release_id);
CREATE INDEX index_expired_and_not_notified_personal_access_tokens ON public.personal_access_tokens USING btree (id, expires_at) WHERE ((impersonation = false) AND (revoked = false) AND (expire_notification_delivered = false));
@@ -23704,6 +23708,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200430123614
20200430130048
20200430174637
+20200504191813
+20200504200709
20200505164958
20200505171834
20200505172405
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 1ecf01ad2ea..e50e9caf50b 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -12063,6 +12063,7 @@ The type of the security scanner.
"""
enum SecurityScannerType {
CONTAINER_SCANNING
+ COVERAGE_FUZZING
DAST
DEPENDENCY_SCANNING
SAST
@@ -14937,7 +14938,7 @@ enum VulnerabilityIssueLinkType {
"""
Represents a vulnerability location. The fields with data will depend on the vulnerability report type
"""
-union VulnerabilityLocation = VulnerabilityLocationContainerScanning | VulnerabilityLocationDast | VulnerabilityLocationDependencyScanning | VulnerabilityLocationSast | VulnerabilityLocationSecretDetection
+union VulnerabilityLocation = VulnerabilityLocationContainerScanning | VulnerabilityLocationCoverageFuzzing | VulnerabilityLocationDast | VulnerabilityLocationDependencyScanning | VulnerabilityLocationSast | VulnerabilityLocationSecretDetection
"""
Represents the location of a vulnerability found by a container security scan
@@ -14959,6 +14960,36 @@ type VulnerabilityLocationContainerScanning {
operatingSystem: String
}
+"""
+Represents the location of a vulnerability found by a Coverage Fuzzing scan
+"""
+type VulnerabilityLocationCoverageFuzzing {
+ """
+ Number of the last relevant line in the vulnerable file
+ """
+ endLine: String
+
+ """
+ Path to the vulnerable file
+ """
+ file: String
+
+ """
+ Number of the first relevant line in the vulnerable file
+ """
+ startLine: String
+
+ """
+ Class containing the vulnerability
+ """
+ vulnerableClass: String
+
+ """
+ Method containing the vulnerability
+ """
+ vulnerableMethod: String
+}
+
"""
Represents the location of a vulnerability found by a DAST scan
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 6d9f102e7c4..0c07f7ce2b6 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -35342,6 +35342,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "COVERAGE_FUZZING",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"possibleTypes": null
@@ -43978,6 +43984,11 @@
"name": "VulnerabilityLocationContainerScanning",
"ofType": null
},
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationCoverageFuzzing",
+ "ofType": null
+ },
{
"kind": "OBJECT",
"name": "VulnerabilityLocationDast",
@@ -44055,6 +44066,89 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationCoverageFuzzing",
+ "description": "Represents the location of a vulnerability found by a Coverage Fuzzing scan",
+ "fields": [
+ {
+ "name": "endLine",
+ "description": "Number of the last relevant line in the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "file",
+ "description": "Path to the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startLine",
+ "description": "Number of the first relevant line in the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "vulnerableClass",
+ "description": "Class containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "vulnerableMethod",
+ "description": "Method containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "VulnerabilityLocationDast",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6df6632f3bd..230027c087e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2246,6 +2246,18 @@ Represents the location of a vulnerability found by a container security scan
| `image` | String | Name of the vulnerable container image |
| `operatingSystem` | String | Operating system that runs on the vulnerable container image |
+## VulnerabilityLocationCoverageFuzzing
+
+Represents the location of a vulnerability found by a Coverage Fuzzing scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `endLine` | String | Number of the last relevant line in the vulnerable file |
+| `file` | String | Path to the vulnerable file |
+| `startLine` | String | Number of the first relevant line in the vulnerable file |
+| `vulnerableClass` | String | Class containing the vulnerability |
+| `vulnerableMethod` | String | Method containing the vulnerability |
+
## VulnerabilityLocationDast
Represents the location of a vulnerability found by a DAST scan
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
index d1e9ef96085..226b510a1e8 100644
--- a/doc/development/telemetry/usage_ping.md
+++ b/doc/development/telemetry/usage_ping.md
@@ -618,6 +618,7 @@ appear to be associated to any of the services running, since they all appear to
| `sd` | `avg_cycle_analytics - production` | | | | |
| `missing` | `avg_cycle_analytics - production` | | | | |
| `total` | `avg_cycle_analytics` | | | | |
+| `u_todos` | | `manage` | | | Visits to /dashboard/todos |
| `g_analytics_contribution` | `analytics_unique_visits` | `manage` | | | Visits to /groups/:group/-/contribution_analytics |
| `g_analytics_insights` | `analytics_unique_visits` | `manage` | | | Visits to /groups/:group/-/insights |
| `g_analytics_issues` | `analytics_unique_visits` | `manage` | | | Visits to /groups/:group/-/issues_analytics |
@@ -629,7 +630,6 @@ appear to be associated to any of the services running, since they all appear to
| `p_analytics_insights` | `analytics_unique_visits` | `manage` | | | Visits to /:group/:project/insights |
| `p_analytics_issues` | `analytics_unique_visits` | `manage` | | | Visits to /:group/:project/-/analytics/issues_analytics |
| `p_analytics_repo` | `analytics_unique_visits` | `manage` | | | Visits to /:group/:project/-/graphs/master/charts |
-| `u_analytics_todos` | `analytics_unique_visits` | `manage` | | | Visits to /dashboard/todos |
| `i_analytics_cohorts` | `analytics_unique_visits` | `manage` | | | Visits to /-/instance_statistics/cohorts |
| `i_analytics_dev_ops_score` | `analytics_unique_visits` | `manage` | | | Visits to /-/instance_statistics/dev_ops_score |
| `analytics_unique_visits_for_any_target` | `analytics_unique_visits` | `manage` | | | Visits to any of the pages listed above |
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 70d4b513cf9..6135c90c9d3 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -387,6 +387,9 @@ analyzer containers: `DOCKER_`, `CI`, `GITLAB_`, `FF_`, `HOME`, `PWD`, `OLDPWD`,
The SAST tool emits a JSON report file. For more information, see the
[schema for this report](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json).
+The JSON report file can be downloaded from the CI pipelines page, for more
+information see [Downloading artifacts](../../../ci/pipelines/job_artifacts.md).
+
Here's an example SAST report:
```json-doc
diff --git a/lib/gitlab/analytics/unique_visits.rb b/lib/gitlab/analytics/unique_visits.rb
index 9dd7d048eec..80f001192db 100644
--- a/lib/gitlab/analytics/unique_visits.rb
+++ b/lib/gitlab/analytics/unique_visits.rb
@@ -15,7 +15,7 @@ module Gitlab
'p_analytics_insights',
'p_analytics_issues',
'p_analytics_repo',
- 'u_analytics_todos',
+ 'u_todos',
'i_analytics_cohorts',
'i_analytics_dev_ops_score'
].freeze
@@ -40,7 +40,7 @@ module Gitlab
end
def weekly_unique_visits_for_any_target(week_of: 7.days.ago)
- keys = TARGET_IDS.map { |target_id| key(target_id, week_of) }
+ keys = TARGET_IDS.select { |id| id =~ /_analytics_/ }.map { |target_id| key(target_id, week_of) }
Gitlab::Redis::SharedState.with do |redis|
redis.pfcount(*keys)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a74e9a7ff00..5bc105ee943 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2937,6 +2937,9 @@ msgstr ""
msgid "Applying suggestions..."
msgstr ""
+msgid "Approval Status"
+msgstr ""
+
msgid "Approval rules"
msgstr ""
@@ -2984,6 +2987,15 @@ msgstr ""
msgid "ApprovalRule|e.g. QA, Security, etc."
msgstr ""
+msgid "ApprovalStatusTooltip|Adheres to separation of duties"
+msgstr ""
+
+msgid "ApprovalStatusTooltip|At least one rule does not adhere to separation of duties"
+msgstr ""
+
+msgid "ApprovalStatusTooltip|Fails to adhere to separation of duties"
+msgstr ""
+
msgid "Approvals|Section: %section"
msgstr ""
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 2e3328ae4d2..5c5cce305eb 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Dashboard::TodosController do
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { { project_id: authorized_project.id } }
- let(:target_id) { 'u_analytics_todos' }
+ let(:target_id) { 'u_todos' }
end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2d313b4dcad..dbc19a22b40 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -41,6 +41,7 @@ Event:
- updated_at
- action
- author_id
+- fingerprint
WikiPage::Meta:
- id
- title
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 72e8658b308..f733c066127 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -994,7 +994,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
'p_analytics_insights' => 123,
'p_analytics_issues' => 123,
'p_analytics_repo' => 123,
- 'u_analytics_todos' => 123,
+ 'u_todos' => 123,
'i_analytics_cohorts' => 123,
'i_analytics_dev_ops_score' => 123,
'analytics_unique_visits_for_any_target' => 543
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 96baeab6809..015a86cb28b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -111,6 +111,45 @@ RSpec.describe Event do
expect(found).not_to include(false_positive)
end
end
+
+ describe '.for_fingerprint' do
+ let_it_be(:with_fingerprint) { create(:event, fingerprint: 'aaa') }
+
+ before_all do
+ create(:event)
+ create(:event, fingerprint: 'bbb')
+ end
+
+ it 'returns none if there is no fingerprint' do
+ expect(described_class.for_fingerprint(nil)).to be_empty
+ expect(described_class.for_fingerprint('')).to be_empty
+ end
+
+ it 'returns none if there is no match' do
+ expect(described_class.for_fingerprint('not-found')).to be_empty
+ end
+
+ it 'can find a given event' do
+ expect(described_class.for_fingerprint(with_fingerprint.fingerprint))
+ .to contain_exactly(with_fingerprint)
+ end
+ end
+ end
+
+ describe '#fingerprint' do
+ it 'is unique scoped to target' do
+ issue = create(:issue)
+ mr = create(:merge_request)
+
+ expect { create_list(:event, 2, target: issue, fingerprint: '1234') }
+ .to raise_error(include('fingerprint'))
+
+ expect do
+ create(:event, target: mr, fingerprint: 'abcd')
+ create(:event, target: issue, fingerprint: 'abcd')
+ create(:event, target: issue, fingerprint: 'efgh')
+ end.not_to raise_error
+ end
end
describe "Push event" do
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index b238949ce47..30399866db6 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -45,10 +45,11 @@ RSpec.describe API::ProjectMilestones do
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
it 'creates an activity event when a milestone is closed' do
- expect(Event).to receive(:create!)
+ path = "/projects/#{project.id}/milestones/#{milestone.id}"
- put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
- params: { state_event: 'close' }
+ expect do
+ put api(path, user), params: { state_event: 'close' }
+ end.to change(Event, :count).by(1)
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index d10ed7d6640..57382b7e5a0 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -171,45 +171,53 @@ RSpec.describe EventCreateService do
let_it_be(:wiki_page) { create(:wiki_page) }
let_it_be(:meta) { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
- Event::WIKI_ACTIONS.each do |action|
- context "The action is #{action}" do
- let(:event) { service.wiki_event(meta, user, action) }
+ let(:fingerprint) { generate(:sha) }
- it 'creates the event', :aggregate_failures do
- expect(event).to have_attributes(
- wiki_page?: true,
- valid?: true,
- persisted?: true,
- action: action.to_s,
- wiki_page: wiki_page,
- author: user
- )
- end
+ def create_event
+ service.wiki_event(meta, user, action, fingerprint)
+ end
- it 'records the event in the event counter' do
- stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
- counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
- tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
+ where(:action) { Event::WIKI_ACTIONS.map { |action| [action] } }
- expect { event }
- .to change { counter_class.count_unique_events(tracking_params) }
- .from(0).to(1)
- end
+ with_them do
+ it 'creates the event' do
+ expect(create_event).to have_attributes(
+ wiki_page?: true,
+ valid?: true,
+ persisted?: true,
+ action: action.to_s,
+ wiki_page: wiki_page,
+ author: user,
+ fingerprint: fingerprint
+ )
+ end
- it 'is idempotent', :aggregate_failures do
- expect { event }.to change(Event, :count).by(1)
- duplicate = nil
- expect { duplicate = service.wiki_event(meta, user, action) }.not_to change(Event, :count)
+ it 'is idempotent', :aggregate_failures do
+ event = nil
+ expect { event = create_event }.to change(Event, :count).by(1)
+ duplicate = nil
+ expect { duplicate = create_event }.not_to change(Event, :count)
- expect(duplicate).to eq(event)
- end
+ expect(duplicate).to eq(event)
+ end
+
+ it 'records the event in the event counter' do
+ stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
+ counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
+ tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
+
+ expect { create_event }
+ .to change { counter_class.count_unique_events(tracking_params) }
+ .by(1)
end
end
(Event.actions.keys - Event::WIKI_ACTIONS).each do |bad_action|
context "The action is #{bad_action}" do
+ let(:action) { bad_action }
+
it 'raises an error' do
- expect { service.wiki_event(meta, user, bad_action) }.to raise_error(described_class::IllegalActionError)
+ expect { create_event }.to raise_error(described_class::IllegalActionError)
end
end
end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index f338b7a5709..7f709be8593 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -218,7 +218,7 @@ RSpec.describe Git::WikiPushService, services: true do
message = 'something went very very wrong'
allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
allow(service).to receive(:execute)
- .with(String, WikiPage, Symbol)
+ .with(String, WikiPage, Symbol, String)
.and_return(ServiceResponse.error(message: message))
end
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
index abf3bcb4c4d..974f2591763 100644
--- a/spec/services/wiki_pages/event_create_service_spec.rb
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -12,7 +12,8 @@ RSpec.describe WikiPages::EventCreateService do
let_it_be(:page) { create(:wiki_page, project: project) }
let(:slug) { generate(:sluggified_title) }
let(:action) { :created }
- let(:response) { subject.execute(slug, page, action) }
+ let(:fingerprint) { page.sha }
+ let(:response) { subject.execute(slug, page, action, fingerprint) }
context 'the user is nil' do
subject { described_class.new(nil) }
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index a34c48a5ba4..07d38d472cc 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -158,9 +158,11 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it "creates an activity event when a note is created", :sidekiq_might_not_need_inline do
- expect(Event).to receive(:create!)
+ uri = "/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes"
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
+ expect do
+ post api(uri, user), params: { body: 'hi!' }
+ end.to change(Event, :count).by(1)
end
context 'setting created_at' do