Commit 59bf77cc authored by Stan Hu's avatar Stan Hu

Merge branch...

Merge branch 'extract-vulnerabilities-actions-from-group-security-vulnerabilities-controller' into 'master'

Extract vulnerabilities actions from group security vulnerabilities controller

See merge request gitlab-org/gitlab-ee!14476
parents d94e028b b64b3886
# frozen_string_literal: true
# The VulnerabilitiesActions concern contains actions that are used to populate vulnerabilities
# on security dashboards.
#
# Note: Consumers of this module will need to define a `def vulnerable` method, which must return
# an object with an interface that matches the one provided by the Vulnerable model concern.
module VulnerabilitiesActions
extend ActiveSupport::Concern
HISTORY_RANGE = 3.months
def index
vulnerabilities = found_vulnerabilities(:with_sha).ordered.page(params[:page])
respond_to do |format|
format.json do
render json: Vulnerabilities::OccurrenceSerializer
.new(current_user: current_user)
.with_pagination(request, response)
.represent(vulnerabilities, preload: true)
end
end
end
def summary
vulnerabilities_summary = found_vulnerabilities.counted_by_severity
respond_to do |format|
format.json do
render json: VulnerabilitySummarySerializer.new.represent(vulnerabilities_summary)
end
end
end
def history
vulnerabilities_counter = found_vulnerabilities(:all).count_by_day_and_severity(HISTORY_RANGE)
respond_to do |format|
format.json do
render json: Vulnerabilities::HistorySerializer.new.represent(vulnerabilities_counter)
end
end
end
private
def filter_params
params.permit(report_type: [], confidence: [], project_id: [], severity: [])
.merge(hide_dismissed: ::Gitlab::Utils.to_boolean(params[:hide_dismissed]))
end
def found_vulnerabilities(collection = :latest)
::Security::VulnerabilitiesFinder.new(vulnerable, params: filter_params).execute(collection)
end
end
# frozen_string_literal: true # frozen_string_literal: true
class Groups::Security::VulnerabilitiesController < Groups::Security::ApplicationController class Groups::Security::VulnerabilitiesController < Groups::Security::ApplicationController
HISTORY_RANGE = 3.months include VulnerabilitiesActions
def index
vulnerabilities = found_vulnerabilities(:with_sha).ordered.page(params[:page])
respond_to do |format|
format.json do
render json: Vulnerabilities::OccurrenceSerializer
.new(current_user: @current_user)
.with_pagination(request, response)
.represent(vulnerabilities, preload: true)
end
end
end
def summary
vulnerabilities_summary = found_vulnerabilities.counted_by_severity
respond_to do |format|
format.json do
render json: VulnerabilitySummarySerializer.new.represent(vulnerabilities_summary)
end
end
end
def history
vulnerabilities_counter = found_vulnerabilities(:all).count_by_day_and_severity(HISTORY_RANGE)
respond_to do |format|
format.json do
render json: Vulnerabilities::HistorySerializer.new.represent(vulnerabilities_counter)
end
end
end
private private
def filter_params def vulnerable
params.permit(report_type: [], confidence: [], project_id: [], severity: []) group
.merge(hide_dismissed: Gitlab::Utils.to_boolean(params[:hide_dismissed]))
end
def found_vulnerabilities(collection = :latest)
::Security::VulnerabilitiesFinder.new(group, params: filter_params).execute(collection)
end end
end end
...@@ -8,7 +8,9 @@ module Vulnerabilities ...@@ -8,7 +8,9 @@ module Vulnerabilities
self.table_name = "vulnerability_occurrences" self.table_name = "vulnerability_occurrences"
paginates_per 20 OCCURRENCES_PER_PAGE = 20
paginates_per OCCURRENCES_PER_PAGE
sha_attribute :project_fingerprint sha_attribute :project_fingerprint
sha_attribute :location_fingerprint sha_attribute :location_fingerprint
......
...@@ -3,177 +3,24 @@ ...@@ -3,177 +3,24 @@
require 'spec_helper' require 'spec_helper'
describe Groups::Security::VulnerabilitiesController do describe Groups::Security::VulnerabilitiesController do
include ApiHelpers let(:group) { create(:group) }
let(:user) { create(:user) }
set(:group) { create(:group) } it_behaves_like VulnerabilitiesActions do
set(:group_other) { create(:group) } let(:vulnerable) { group }
set(:user) { create(:user) } let(:vulnerable_params) { { group_id: group } }
set(:project_dev) { create(:project, :private, :repository, group: group) } end
set(:project_guest) { create(:project, :private, :repository, group: group) }
set(:project_other) { create(:project, :public, :repository, group: group_other) }
let(:projects) { [project_dev, project_guest, project_other] }
before do before do
sign_in(user) sign_in(user)
end end
describe 'GET index.json' do describe 'access for all actions' do
subject { get :index, params: { group_id: group }, format: :json }
context 'when security dashboard feature is disabled' do context 'when security dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns 404' do it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when security dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'when user has guest access' do
before do
group.add_guest(user)
end
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
context 'when user has developer access' do
before do
group.add_developer(user)
end
context 'when no page request' do
before do
projects.each do |project|
create_vulnerabilities(1, project)
end
end
it "returns a list of vulnerabilities" do
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq 2
expect(response).to match_response_schema('vulnerabilities/occurrence_list', dir: 'ee')
end
end
context 'when page requested' do
before do
projects.each do |project|
create_vulnerabilities(11, project)
end
end
it "returns a list of vulnerabilities" do
get :index, params: { group_id: group, page: 2 }, format: :json
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq 2
end
end
context 'with vulnerability feedback' do
it "avoids N+1 queries", :with_request_store do
create_vulnerabilities(2, project_dev, with_feedback: true)
control_count = ActiveRecord::QueryRecorder.new { get_index }
create_vulnerabilities(2, project_guest, with_feedback: true)
expect { get_index }.not_to exceed_all_query_limit(control_count)
end
private
def get_index
get :index, params: { group_id: group }, format: :json
end
end
context 'with multiple report types' do
before do
projects.each do |project|
create_vulnerabilities(2, project_guest, { report_type: :sast })
create_vulnerabilities(1, project_dev, { report_type: :dependency_scanning })
end
end
it "returns a list of vulnerabilities for all report types without filter" do
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq 3
expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast', 'dependency_scanning')
expect(response).to match_response_schema('vulnerabilities/occurrence_list', dir: 'ee')
end
it "returns a list of vulnerabilities for sast only if filter is enabled" do
get :index, params: { group_id: group, report_type: ['sast'] }, format: :json
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq 2
expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast')
expect(response).to match_response_schema('vulnerabilities/occurrence_list', dir: 'ee')
end
it "returns a list of vulnerabilities of all types with multi filter" do
get :index, params: { group_id: group, report_type: %w[sast dependency_scanning] }, format: :json
expect(json_response.length).to eq 3
expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast', 'dependency_scanning')
end
end
def create_vulnerabilities(count, project, options = {})
report_type = options[:report_type] || :sast
pipeline = create(:ci_pipeline, :success, project: project)
vulnerabilities = create_list(:vulnerabilities_occurrence, count, report_type: report_type, pipelines: [pipeline], project: project)
return vulnerabilities unless options[:with_feedback]
vulnerabilities.each do |occurrence|
create(:vulnerability_feedback, report_type, :dismissal,
pipeline: pipeline,
project: project_dev,
project_fingerprint: occurrence.project_fingerprint)
create(:vulnerability_feedback, report_type, :issue,
pipeline: pipeline,
issue: create(:issue, project: project),
project: project_dev,
project_fingerprint: occurrence.project_fingerprint)
end
end
end
end
end
describe 'GET summary.json' do
subject { get :summary, params: { group_id: group }, format: :json }
context 'when security dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false) stub_licensed_features(security_dashboard: false)
end
it 'returns 404' do get :index, params: { group_id: group }, format: :json
subject
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
...@@ -182,189 +29,51 @@ describe Groups::Security::VulnerabilitiesController do ...@@ -182,189 +29,51 @@ describe Groups::Security::VulnerabilitiesController do
context 'when security dashboard feature is enabled' do context 'when security dashboard feature is enabled' do
before do before do
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
pipeline = create(:ci_pipeline, :success, project: project_dev)
create_list(:vulnerabilities_occurrence, 3,
pipelines: [pipeline], project: project_dev, report_type: :sast, severity: :high)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_dev, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 2,
pipelines: [pipeline], project: project_guest, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_guest, report_type: :dast, severity: :medium)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_other, report_type: :dast, severity: :low)
end end
context 'when user has guest access' do context 'when user has guest access' do
before do it 'denies access' do
group.add_guest(user) group.add_guest(user)
end
it 'returns 403' do get :index, params: { group_id: group }, format: :json
subject
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(403)
end end
end end
context 'when user has developer access' do context 'when user has developer access' do
before do it 'grants access' do
group.add_developer(user) group.add_developer(user)
end
it 'returns vulnerabilities counts for all report types' do
subject
expect(response).to have_gitlab_http_status(200) get :index, params: { group_id: group }, format: :json
expect(json_response).to be_an(Hash)
expect(json_response['high']).to eq(3)
expect(json_response['low']).to eq(4)
expect(json_response['medium']).to eq(1)
expect(response).to match_response_schema('vulnerabilities/summary', dir: 'ee')
end
context 'with enabled filters' do
it 'returns counts for filtered vulnerabilities' do
get :summary, params: { group_id: group, report_type: %w[sast dast], severity: %[high low] }, format: :json
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Hash)
expect(json_response['high']).to eq(3)
expect(json_response['low']).to eq(1)
expect(json_response['medium']).to eq(1)
expect(response).to match_response_schema('vulnerabilities/summary', dir: 'ee')
end
end end
end end
end end
end end
describe 'GET history.json' do describe 'GET index.json' do
subject { get :history, params: { group_id: group }, format: :json } it 'returns vulnerabilities for all projects in the group' do
context 'when security dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when security dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
travel_to(Time.zone.parse('2018-11-10')) do
pipeline_1 = create(:ci_pipeline, :success, project: project_dev)
pipeline_2 = create(:ci_pipeline, :success, project: project_dev)
create_list(:vulnerabilities_occurrence, 2,
pipelines: [pipeline_1], project: project_dev, report_type: :sast, severity: :high)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline_1], project: project_dev, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline_1, pipeline_2], project: project_dev, report_type: :sast, severity: :critical)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline_1, pipeline_2], project: project_dev, report_type: :dependency_scanning, severity: :low)
end
travel_to(Time.zone.parse('2018-11-12')) do
pipeline = create(:ci_pipeline, :success, project: project_dev)
create_list(:vulnerabilities_occurrence, 2,
pipelines: [pipeline], project: project_dev, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_dev, report_type: :dast, severity: :medium)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_dev, report_type: :dast, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project_dev, report_type: :container_scanning, severity: :high)
end
end
context 'when user has guest access' do
before do
group.add_guest(user)
end
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
context 'when user has developer access' do
before do
group.add_developer(user) group.add_developer(user)
end
it 'returns vulnerability history within last 90 days' do # create projects for the group
travel_to(Time.zone.parse('2019-02-10')) do 2.times do
subject project = create(:project, namespace: group)
end pipeline = create(:ci_pipeline, :success, project: project)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Hash)
expect(json_response['total']).to eq({ '2018-11-10' => 5, '2018-11-12' => 5 })
expect(json_response['critical']).to eq({ '2018-11-10' => 1 })
expect(json_response['high']).to eq({ '2018-11-10' => 2, '2018-11-12' => 1 })
expect(json_response['medium']).to eq({ '2018-11-12' => 1 })
expect(json_response['low']).to eq({ '2018-11-10' => 2, '2018-11-12' => 3 })
expect(response).to match_response_schema('vulnerabilities/history', dir: 'ee')
end
it 'returns empty history if there are no vulnerabilities within last 90 days' do create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, severity: :high)
travel_to(Time.zone.parse('2019-02-13')) do
subject
end end
expect(response).to have_gitlab_http_status(200) # create an ungrouped project to ensure we don't include it
expect(json_response).to be_an(Hash) project = create(:project)
expect(json_response).to eq({ pipeline = create(:ci_pipeline, :success, project: project)
"undefined" => {}, create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, severity: :high)
"info" => {},
"unknown" => {},
"low" => {},
"medium" => {},
"high" => {},
"critical" => {},
"total" => {}
})
expect(response).to match_response_schema('vulnerabilities/history', dir: 'ee')
end
it 'returns filtered history if filters are enabled' do get :index, params: { group_id: group }, format: :json
travel_to(Time.zone.parse('2019-02-10')) do
get :history, params: { group_id: group, report_type: %w[dependency_scanning sast dast container_scanning] }, format: :json
end
expect(response).to have_gitlab_http_status(200) expect(json_response.count).to be(2)
expect(json_response).to be_an(Hash)
expect(json_response['total']).to eq({ '2018-11-10' => 5, '2018-11-12' => 5 })
expect(json_response['critical']).to eq({ '2018-11-10' => 1 })
expect(json_response['high']).to eq({ '2018-11-10' => 2, '2018-11-12' => 1 })
expect(json_response['medium']).to eq({ '2018-11-12' => 1 })
expect(json_response['low']).to eq({ '2018-11-10' => 2, '2018-11-12' => 3 })
expect(response).to match_response_schema('vulnerabilities/history', dir: 'ee')
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
shared_examples VulnerabilitiesActions do
include ApiHelpers
let(:params) { vulnerable_params }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
def project
return vulnerable if vulnerable.is_a?(Project)
@project ||= create(:project, namespace: vulnerable)
end
before do
vulnerable.add_developer(user)
sign_in(user)
stub_licensed_features(security_dashboard: true)
end
describe 'GET index.json' do
subject { get :index, params: params, format: :json }
it 'returns an ordered list of vulnerabilities' do
critical_vulnerability = create(
:vulnerabilities_occurrence,
pipelines: [pipeline],
project: project,
severity: :critical
)
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, severity: :high)
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq 2
expect(json_response.first['id']).to be(critical_vulnerability.id)
expect(response).to match_response_schema('vulnerabilities/occurrence_list', dir: 'ee')
end
context 'when a specific page is requested' do
let(:params) { vulnerable_params.merge(page: 2) }
before do
Vulnerabilities::Occurrence.paginates_per 2
create_list(:vulnerabilities_occurrence, 3, pipelines: [pipeline], project: project)
subject
end
after do
Vulnerabilities::Occurrence.paginates_per Vulnerabilities::Occurrence::OCCURRENCES_PER_PAGE
end
it 'returns the list of vulnerabilities that are on the requested page' do
expect(json_response.length).to eq 1
end
end
context 'when the vulnerabilities have feedback' do
before do
vulnerability = create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
create(:vulnerability_feedback,
:sast,
:issue,
pipeline: pipeline,
issue: create(:issue, project: project),
project: project,
project_fingerprint: vulnerability.project_fingerprint)
end
it 'avoids N+1 queries', :with_request_store do
control_count = ActiveRecord::QueryRecorder.new { subject }
vulnerability = create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
create(:vulnerability_feedback,
:sast,
:issue,
pipeline: pipeline,
issue: create(:issue, project: project),
project: project,
project_fingerprint: vulnerability.project_fingerprint)
expect { subject }.not_to exceed_all_query_limit(control_count)
end
end
context 'with multiple report types' do
before do
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :sast)
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :dast)
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project, report_type: :dependency_scanning)
subject
end
context 'with a single report filter' do
let(:params) { vulnerable_params.merge(report_type: ['sast']) }
it 'returns a list of vulnerabilities for that reporty type only' do
expect(json_response.length).to eq 1
expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast')
end
end
context 'with multiple report filters' do
let(:params) { vulnerable_params.merge(report_type: %w[sast dependency_scanning]) }
it 'returns a list of vulnerabilities for all filtered upon types' do
expect(json_response.length).to eq 2
expect(json_response.map { |v| v['report_type'] }.uniq).to contain_exactly('sast', 'dependency_scanning')
end
end
end
end
describe 'GET summary.json' do
subject { get :summary, params: params, format: :json }
before do
create_list(:vulnerabilities_occurrence, 3,
pipelines: [pipeline], project: project, report_type: :sast, severity: :high)
create_list(:vulnerabilities_occurrence, 2,
pipelines: [pipeline], project: project, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project, report_type: :dast, severity: :medium)
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project, report_type: :sast, severity: :medium)
subject
end
it 'returns vulnerabilities counts for all report types' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['high']).to eq(3)
expect(json_response['low']).to eq(2)
expect(json_response['medium']).to eq(2)
expect(response).to match_response_schema('vulnerabilities/summary', dir: 'ee')
end
context 'with enabled filters' do
let(:params) { vulnerable_params.merge(report_type: %w[sast dast], severity: %[high low]) }
it 'returns counts for filtered vulnerabilities' do
expect(json_response['high']).to eq(3)
expect(json_response['low']).to eq(0)
expect(json_response['medium']).to eq(2)
end
end
end
describe 'GET history.json' do
subject { get :history, params: params, format: :json }
before do
travel_to(Time.zone.parse('2018-11-10')) do
create(:vulnerabilities_occurrence,
pipelines: [pipeline],
project: project,
report_type: :sast,
severity: :critical)
create(:vulnerabilities_occurrence,
pipelines: [pipeline],
project: project,
report_type: :dependency_scanning,
severity: :low)
end
travel_to(Time.zone.parse('2018-11-12')) do
create(:vulnerabilities_occurrence,
pipelines: [pipeline],
project: project,
report_type: :sast,
severity: :critical)
create(:vulnerabilities_occurrence,
pipelines: [pipeline],
project: project,
report_type: :dependency_scanning,
severity: :low)
end
end
it 'returns vulnerability history within last 90 days' do
travel_to(Time.zone.parse('2019-02-11')) do
subject
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['total']).to eq({ '2018-11-12' => 2 })
expect(json_response['critical']).to eq({ '2018-11-12' => 1 })
expect(json_response['low']).to eq({ '2018-11-12' => 1 })
expect(response).to match_response_schema('vulnerabilities/history', dir: 'ee')
end
it 'returns empty history if there are no vulnerabilities within last 90 days' do
travel_to(Time.zone.parse('2019-02-13')) do
subject
end
expect(json_response).to eq({
"undefined" => {},
"info" => {},
"unknown" => {},
"low" => {},
"medium" => {},
"high" => {},
"critical" => {},
"total" => {}
})
end
context 'with a report type filter' do
let(:params) { vulnerable_params.merge(report_type: %w[sast]) }
before do
travel_to(Time.zone.parse('2019-02-11')) do
subject
end
end
it 'returns filtered history if filters are enabled' do
expect(json_response['total']).to eq({ '2018-11-12' => 1 })
expect(json_response['critical']).to eq({ '2018-11-12' => 1 })
expect(json_response['low']).to eq({})
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