Commit bc5eaa39 authored by Robert Speicher's avatar Robert Speicher

Merge branch '33899-instance-security-dashboard-vulnerabilities' into 'master'

Add vulnerability findings data to instance security dashboard

Closes #33899

See merge request gitlab-org/gitlab!21691
parents d420b566 537c0479
...@@ -6,7 +6,7 @@ module ProjectCollectionVulnerabilityFindingsActions ...@@ -6,7 +6,7 @@ module ProjectCollectionVulnerabilityFindingsActions
included do included do
def history def history
history_count = Gitlab::Vulnerabilities::History.new(vulnerable, filter_params).findings_counter history_count = Gitlab::Vulnerabilities::History.new(vulnerable, params: filter_params).findings_counter
respond_to do |format| respond_to do |format|
format.json do format.json do
......
# frozen_string_literal: true
module Security
class VulnerabilityFindingsController < ::Security::ApplicationController
include ProjectCollectionVulnerabilityFindingsActions
before_action :remove_invalid_project_ids
private
def remove_invalid_project_ids
render_empty_response if valid_project_ids.empty?
params[:project_id] = valid_project_ids
end
def render_empty_response
respond_to do |format|
format.json do
render json: {}
end
end
end
def vulnerable
@vulnerable ||= ApplicationInstance.new
end
def valid_project_ids
return security_dashboard_project_ids if request_project_ids.empty?
security_dashboard_project_ids & request_project_ids
end
def request_project_ids
params.fetch(:project_id, []).map(&:to_i)
end
def security_dashboard_project_ids
current_user.security_dashboard_project_ids
end
end
end
# frozen_string_literal: true
module SecurityHelper
def instance_security_dashboard_data
{
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
project_add_endpoint: security_projects_path,
project_list_endpoint: security_projects_path,
vulnerabilities_count_endpoint: summary_security_vulnerability_findings_path,
vulnerabilities_endpoint: security_vulnerability_findings_path,
vulnerabilities_history_endpoint: history_security_vulnerability_findings_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities')
}
end
end
# frozen_string_literal: true
class ApplicationInstance
extend ActiveModel::Naming
include ::Vulnerable
def all_pipelines
::Ci::Pipeline.all
end
end
...@@ -332,6 +332,14 @@ module EE ...@@ -332,6 +332,14 @@ module EE
read_attribute(:support_bot) read_attribute(:support_bot)
end end
def security_dashboard_project_ids
if self.can?(:read_all_resources)
security_dashboard_projects.ids
else
security_dashboard_projects.visible_to_user(self).ids
end
end
protected protected
override :password_required? override :password_required?
......
- page_title _('Security Dashboard') - page_title _('Security Dashboard')
- @hide_breadcrumbs = true - @hide_breadcrumbs = true
#js-security{ data: { vulnerabilities_endpoint: '/groups/gitlab-org/-/security/vulnerabilities', #js-security{ data: instance_security_dashboard_data }
vulnerabilities_count_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/summary',
vulnerabilities_history_endpoint: '/groups/gitlab-org/-/security/vulnerabilities/history',
project_add_endpoint: security_projects_path,
project_list_endpoint: security_projects_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard') } }
...@@ -4,4 +4,11 @@ namespace :security do ...@@ -4,4 +4,11 @@ namespace :security do
root to: 'dashboard#show' root to: 'dashboard#show'
resources :projects, only: [:index, :create, :destroy] resources :projects, only: [:index, :create, :destroy]
resources :vulnerability_findings, only: [:index] do
collection do
get :summary
get :history
end
end
end end
...@@ -8,10 +8,11 @@ module Gitlab ...@@ -8,10 +8,11 @@ module Gitlab
attr_reader :vulnerable, :filters attr_reader :vulnerable, :filters
HISTORY_RANGE = 3.months HISTORY_RANGE = 3.months
NoProjectIDsError = Class.new(StandardError)
def initialize(vulnerable, filters) def initialize(vulnerable, params:)
@vulnerable = vulnerable @vulnerable = vulnerable
@filters = filters @filters = params
end end
def findings_counter def findings_counter
...@@ -57,6 +58,10 @@ module Gitlab ...@@ -57,6 +58,10 @@ module Gitlab
return filters[:project_id] if filters.key?('project_id') return filters[:project_id] if filters.key?('project_id')
vulnerable.project_ids_with_security_reports vulnerable.project_ids_with_security_reports
rescue NoMethodError
vulnerable_name = vulnerable.model_name.human.downcase
raise NoProjectIDsError, "A project_id filter must be given with this #{vulnerable_name}"
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe SecurityHelper do
describe '#instance_security_dashboard_data' do
before do
stub_feature_flags(first_class_vulnerabilities: true)
end
subject { instance_security_dashboard_data }
it 'returns vulnerability, project, feedback, asset, and docs paths for the instance security dashboard' do
is_expected.to eq({
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
project_add_endpoint: security_projects_path,
project_list_endpoint: security_projects_path,
vulnerabilities_count_endpoint: summary_security_vulnerability_findings_path,
vulnerabilities_endpoint: security_vulnerability_findings_path,
vulnerabilities_history_endpoint: history_security_vulnerability_findings_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities')
})
end
end
end
...@@ -3,48 +3,63 @@ ...@@ -3,48 +3,63 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Vulnerabilities::HistoryCache do describe Gitlab::Vulnerabilities::HistoryCache do
let(:group) { create(:group) } describe '#fetch', :use_clean_rails_memory_store_caching do
let(:project) { create(:project, :public, namespace: group) } shared_examples 'the history cache when given an expected Vulnerable' do
let(:project_cache_key) { described_class.new(group, project.id).send(:cache_key) } let(:project) { create(:project, :public, namespace: group) }
let(:project_cache_key) { described_class.new(vulnerable, project.id).send(:cache_key) }
before do before do
create_vulnerabilities(1, project) create_vulnerabilities(1, project)
end end
describe '#fetch', :use_clean_rails_memory_store_caching do it 'reads from cache when records are cached' do
it 'reads from cache when records are cached' do history_cache = described_class.new(vulnerable, project.id)
history_cache = described_class.new(group, project.id)
expect(Rails.cache.fetch(project_cache_key, raw: true)).to be_nil expect(Rails.cache.fetch(project_cache_key, raw: true)).to be_nil
control_count = ActiveRecord::QueryRecorder.new { history_cache.fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) } control_count = ActiveRecord::QueryRecorder.new { history_cache.fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) }
expect { 2.times { history_cache.fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) } }.not_to exceed_query_limit(control_count) expect { 2.times { history_cache.fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) } }.not_to exceed_query_limit(control_count)
end end
it 'returns the proper format for uncached history' do it 'returns the proper format for uncached history' do
Timecop.freeze do Timecop.freeze do
fetched_history = described_class.new(group, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) fetched_history = described_class.new(vulnerable, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE)
expect(fetched_history[:total]).to eq( Date.today => 1 ) expect(fetched_history[:total]).to eq( Date.today => 1 )
expect(fetched_history[:high]).to eq( Date.today => 1 ) expect(fetched_history[:high]).to eq( Date.today => 1 )
end
end end
end
it 'returns the proper format for cached history' do it 'returns the proper format for cached history' do
Timecop.freeze do Timecop.freeze do
described_class.new(group, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) described_class.new(vulnerable, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE)
fetched_history = described_class.new(group, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE) fetched_history = described_class.new(vulnerable, project.id).fetch(Gitlab::Vulnerabilities::History::HISTORY_RANGE)
expect(fetched_history[:total]).to eq( Date.today => 1 ) expect(fetched_history[:total]).to eq( Date.today => 1 )
expect(fetched_history[:high]).to eq( Date.today => 1 ) expect(fetched_history[:high]).to eq( Date.today => 1 )
end
end
def create_vulnerabilities(count, project, options = {})
report_type = options[:report_type] || :sast
pipeline = create(:ci_pipeline, :success, project: project)
create_list(:vulnerabilities_occurrence, count, report_type: report_type, pipelines: [pipeline], project: project)
end
end
context 'when given a Group' do
it_behaves_like 'the history cache when given an expected Vulnerable' do
let(:group) { create(:group) }
let(:vulnerable) { group }
end end
end end
def create_vulnerabilities(count, project, options = {}) context 'when given an ApplicationInstance' do
report_type = options[:report_type] || :sast it_behaves_like 'the history cache when given an expected Vulnerable' do
pipeline = create(:ci_pipeline, :success, project: project) let(:group) { create(:group) }
create_list(:vulnerabilities_occurrence, count, report_type: report_type, pipelines: [pipeline], project: project) let(:vulnerable) { ApplicationInstance.new }
end
end end
end end
end end
...@@ -3,65 +3,99 @@ ...@@ -3,65 +3,99 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Vulnerabilities::History do describe Gitlab::Vulnerabilities::History do
let(:group) { create(:group) }
let(:project1) { create(:project, :public, namespace: group) }
let(:project2) { create(:project, :public, namespace: group) }
let(:filters) { {} }
before do
create_vulnerabilities(1, project1, { severity: :medium, report_type: :sast })
create_vulnerabilities(2, project2, { severity: :high, report_type: :sast })
end
describe '#findings_counter', :use_clean_rails_memory_store_caching do describe '#findings_counter', :use_clean_rails_memory_store_caching do
subject(:counter) { described_class.new(group, filters).findings_counter } shared_examples 'the history cache when given an expected Vulnerable' do
let(:filters) { project_ids }
let(:today) { Date.parse('20191031') }
before do
Timecop.freeze(today) do
create_vulnerabilities(1, project1, { severity: :medium, report_type: :sast })
create_vulnerabilities(2, project2, { severity: :high, report_type: :sast })
end
end
subject(:counter) { described_class.new(vulnerable, params: filters).findings_counter }
context 'filters are passed' do context 'when filters are passed' do
let(:filters) { { report_type: :sast } } let(:filters) { project_ids.merge(report_type: :sast) }
it 'does not call Gitlab::Vulnerabilities::HistoryCache' do it 'does not call Gitlab::Vulnerabilities::HistoryCache' do
expect(Gitlab::Vulnerabilities::HistoryCache).not_to receive(:new) expect(Gitlab::Vulnerabilities::HistoryCache).not_to receive(:new)
counter
end
end
it 'calls Gitlab::Vulnerabilities::HistoryCache' do
expect(Gitlab::Vulnerabilities::HistoryCache).to receive(:new).twice.and_call_original
counter counter
end end
end
it 'calls Gitlab::Vulnerabilities::HistoryCache' do it 'returns the proper format for the history' do
expect(Gitlab::Vulnerabilities::HistoryCache).to receive(:new).twice.and_call_original Timecop.freeze(today) do
expect(counter[:total]).to eq({ today => 3 })
expect(counter[:high]).to eq({ today => 2 })
end
end
context 'when there are multiple projects with vulnerabilities' do
before do
Timecop.freeze(today - 1) do
create_vulnerabilities(1, project1, { severity: :high })
end
Timecop.freeze(today - 4) do
create_vulnerabilities(1, project2, { severity: :high })
end
end
it 'sorts by date for each key' do
Timecop.freeze(today) do
expect(counter[:high].keys).to eq([(today - 4), (today - 1), today])
end
end
end
counter def create_vulnerabilities(count, project, options = {})
report_type = options[:report_type] || :sast
severity = options[:severity] || :high
pipeline = create(:ci_pipeline, :success, project: project)
created_at = options[:created_at] || today
create_list(:vulnerabilities_occurrence, count, report_type: report_type, severity: severity, pipelines: [pipeline], project: project, created_at: created_at)
end
end end
it 'returns the proper format for the history' do context 'when the given vulnerable is a Group' do
Timecop.freeze do it_behaves_like 'the history cache when given an expected Vulnerable' do
expect(counter[:total]).to eq({ Date.today => 3 }) let(:group) { create(:group) }
expect(counter[:high]).to eq({ Date.today => 2 }) let(:project1) { create(:project, :public, namespace: group) }
let(:project2) { create(:project, :public, namespace: group) }
let(:project_ids) { {} }
let(:vulnerable) { group }
end end
end end
context 'multiple projects with vulnerabilities' do context 'when given an ApplicationInstance' do
before do let(:vulnerable) { ApplicationInstance.new }
Timecop.freeze(Date.today - 1) do
create_vulnerabilities(1, project1, { severity: :high }) context 'and a project_id filter' do
end it_behaves_like 'the history cache when given an expected Vulnerable' do
Timecop.freeze(Date.today - 4) do let(:group) { create(:group) }
create_vulnerabilities(1, project2, { severity: :high }) let(:project1) { create(:project, :public, namespace: group) }
let(:project2) { create(:project, :public, namespace: group) }
let(:project_ids) { ActionController::Parameters.new({ 'project_id' => [project1, project2] }) }
end end
end end
it 'sorts by date for each key' do context 'and no project_id filter' do
Timecop.freeze do it 'throws an error saying that the filter must be given' do
expect(counter[:high].keys).to eq([(Date.today - 4), (Date.today - 1), Date.today]) expect { described_class.new(vulnerable, params: {}).findings_counter }.to raise_error(
Gitlab::Vulnerabilities::History::NoProjectIDsError,
"A project_id filter must be given with this #{vulnerable.model_name.human.downcase}"
)
end end
end end
end end
def create_vulnerabilities(count, project, options = {})
report_type = options[:report_type] || :sast
severity = options[:severity] || :high
pipeline = create(:ci_pipeline, :success, project: project)
created_at = options[:created_at] || Date.today
create_list(:vulnerabilities_occurrence, count, report_type: report_type, severity: severity, pipelines: [pipeline], project: project, created_at: created_at)
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ApplicationInstance do
it_behaves_like Vulnerable do
let(:vulnerable) { described_class.new }
end
describe '#all_pipelines' do
it 'returns all CI pipelines for the instance' do
allow(::Ci::Pipeline).to receive(:all)
described_class.new.all_pipelines
expect(::Ci::Pipeline).to have_received(:all)
end
end
end
...@@ -700,4 +700,32 @@ describe User do ...@@ -700,4 +700,32 @@ describe User do
end end
end end
end end
describe '#security_dashboard_project_ids' do
let(:project) { create(:project) }
context 'when the user can read all resources' do
it "returns the ids for all of the user's security dashboard projects" do
admin = create(:admin)
auditor = create(:auditor)
admin.security_dashboard_projects << project
auditor.security_dashboard_projects << project
expect(admin.security_dashboard_project_ids).to eq([project.id])
expect(auditor.security_dashboard_project_ids).to eq([project.id])
end
end
context 'when the user cannot read all resources' do
it 'returns the ids for security dashboard projects visible to the user' do
user = create(:user)
member_project = create(:project)
member_project.add_developer(user)
user.security_dashboard_projects << [project, member_project]
expect(user.security_dashboard_project_ids).to eq([member_project.id])
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
shared_examples 'instance security dashboard vulnerability findings endpoint' do
let(:findings_headers) { { 'ACCEPT' => 'application/json' } }
let(:findings_project) { create(:project) }
let(:findings_user) { create(:user) }
let(:fetcher_double) do
double(
findings_fetcher,
execute: Vulnerabilities::Occurrence.none,
findings_counter: {}
)
end
before do
findings_project.add_developer(findings_user)
findings_user.security_dashboard_projects << findings_project
login_as(findings_user)
stub_licensed_features(security_dashboard: true)
end
context 'when no project ID param is given' do
it "defaults to the current user's security dashboard projects" do
_other_project = create(:project)
expect(findings_fetcher).to receive(:new).with(
anything,
params: hash_including('project_id' => [findings_project.id])
).and_return(fetcher_double)
get findings_endpoint, headers: findings_headers
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when the request includes IDs from projects the current user does not have on their dashboard' do
it "only fetches vulnerabilities from the current user's security dashboard projects" do
inaccessible_project = create(:project)
project_ids_param = { project_id: [findings_project.id, inaccessible_project.id] }
expect(findings_fetcher).to receive(:new).with(
anything,
params: hash_including('project_id' => [findings_project.id])
).and_return(fetcher_double)
get findings_endpoint, headers: findings_headers, params: project_ids_param
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when the request only includes invalid project IDs' do
it 'replies with an empty response' do
inaccessible_project = create(:project)
project_ids_param = { project_id: [inaccessible_project.id] }
expect(findings_fetcher).not_to receive(:new)
get findings_endpoint, headers: findings_headers, params: project_ids_param
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
context 'when the request includes project IDs the current user is not a member of' do
context 'and the user is not an auditor' do
it 'only fetches vulnerabilities from projects to which the user has access' do
inaccessible_project = create(:project)
findings_user.security_dashboard_projects << inaccessible_project
project_ids_param = { project_id: [findings_project.id, inaccessible_project.id] }
expect(findings_fetcher).to receive(:new).with(
anything,
params: hash_including('project_id' => [findings_project.id])
).and_return(fetcher_double)
get findings_endpoint, headers: findings_headers, params: project_ids_param
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'and the user is an auditor' do
let(:findings_user) { create(:auditor) }
it 'fetches vulnerabilities for all given projects on their dashboard' do
unmembered_project = create(:project)
findings_user.security_dashboard_projects << unmembered_project
project_ids_param = { project_id: [findings_project.id, unmembered_project.id] }
expect(findings_fetcher).to receive(:new).with(
anything,
params: hash_including('project_id' => array_including(findings_project.id, unmembered_project.id))
).and_return(fetcher_double)
get findings_endpoint, headers: findings_headers, params: project_ids_param
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
describe 'GET /-/security/vulnerability_findings' do
it_behaves_like 'instance security dashboard JSON endpoint' do
let(:security_dashboard_request) do
get security_vulnerability_findings_path, headers: { 'ACCEPT' => 'application/json' }
end
end
context 'when the current user is authenticated' do
let(:findings_request_params) { project_ids_param }
let(:headers) { { 'ACCEPT' => 'application/json' } }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:project) { create(:project) }
let(:project_ids_param) { { project_id: [project.id] } }
let(:user) { create(:user) }
it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
let(:findings_endpoint) { security_vulnerability_findings_path }
let(:findings_fetcher) { ::Security::VulnerabilityFindingsFinder }
end
before do
project.add_developer(user)
user.security_dashboard_projects << project
login_as(user)
stub_licensed_features(security_dashboard: true)
end
subject { get security_vulnerability_findings_path, headers: headers, params: findings_request_params }
it 'returns an ordered list of vulnerability findings for the given projects' 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(:findings_request_params) { project_ids_param.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 vulnerability findings that are on the requested page' do
expect(json_response.length).to eq 1
end
end
context 'when the vulnerability findings 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(:findings_request_params) { project_ids_param.merge(report_type: ['sast']) }
it 'returns a list of vulnerability findings for that report 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(:findings_request_params) { project_ids_param.merge(report_type: %w[sast dependency_scanning]) }
it 'returns a list of vulnerability findings 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
end
describe 'GET /-/security/vulnerability_findings/summary' do
it_behaves_like 'instance security dashboard JSON endpoint' do
let(:security_dashboard_request) do
get summary_security_vulnerability_findings_path, headers: { 'ACCEPT' => 'application/json' }
end
end
context 'when the current user is authenticated' do
let(:findings_request_params) { project_ids_param }
let(:headers) { { 'ACCEPT' => 'application/json' } }
let(:project_ids_param) { { project_id: [project.id] } }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:user) { create(:user) }
let(:project) { create(:project) }
it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
let(:findings_endpoint) { summary_security_vulnerability_findings_path }
let(:findings_fetcher) { ::Security::VulnerabilityFindingsFinder }
end
before do
project.add_developer(user)
user.security_dashboard_projects << project
login_as(user)
stub_licensed_features(security_dashboard: true)
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)
end
subject { get summary_security_vulnerability_findings_path, headers: headers, params: findings_request_params }
it 'returns vulnerability findings counts for all report types' do
subject
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(:findings_request_params) { project_ids_param.merge(report_type: %w[sast dast], severity: %w[high low]) }
it 'returns counts for filtered vulnerability findings' do
subject
expect(json_response['high']).to eq(3)
expect(json_response['medium']).to eq(0)
expect(json_response['low']).to eq(0)
end
end
end
end
describe 'GET /-/security/vulnerability_findings/history' do
it_behaves_like 'instance security dashboard JSON endpoint' do
let(:security_dashboard_request) do
get(
history_security_vulnerability_findings_path,
headers: { 'ACCEPT' => 'application/json' },
params: { project_id: [] }
)
end
end
context 'when the current user is authenticated' do
let(:findings_request_params) { project_ids_param }
let(:headers) { { 'ACCEPT' => 'application/json' } }
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:project) { create(:project) }
let(:project_ids_param) { { project_id: [project.id] } }
let(:user) { create(:user) }
it_behaves_like 'instance security dashboard vulnerability findings endpoint' do
let(:findings_endpoint) { history_security_vulnerability_findings_path }
let(:findings_fetcher) { ::Gitlab::Vulnerabilities::History }
end
before do
project.add_developer(user)
user.security_dashboard_projects << project
login_as(user)
stub_licensed_features(security_dashboard: true)
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
subject { get history_security_vulnerability_findings_path, headers: headers, params: findings_request_params }
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(:findings_request_params) { project_ids_param.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
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module VulnerableHelpers module VulnerableHelpers
class BadVulnerableError < StandardError class BadVulnerableError < StandardError
def message def message
'The given vulnerable must be either `Project` or `Namespace`' 'The given vulnerable must be either `Project`, `Namespace`, or `ApplicationInstance`'
end end
end end
...@@ -13,6 +13,21 @@ module VulnerableHelpers ...@@ -13,6 +13,21 @@ module VulnerableHelpers
vulnerable vulnerable
when Namespace when Namespace
create(:project, namespace: vulnerable) create(:project, namespace: vulnerable)
when ApplicationInstance
create(:project)
else
raise BadVulnerableError
end
end
def as_external_vulnerable_project(vulnerable)
case vulnerable
when Project
create(:project)
when Namespace
create(:project)
when ApplicationInstance
nil
else else
raise BadVulnerableError raise BadVulnerableError
end end
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
shared_examples_for Vulnerable do shared_examples_for Vulnerable do
include VulnerableHelpers include VulnerableHelpers
let(:external_project) { create(:project) } let(:external_project) { as_external_vulnerable_project(vulnerable) }
let(:failed_pipeline) { create(:ci_pipeline, :failed, project: vulnerable_project) } let(:failed_pipeline) { create(:ci_pipeline, :failed, project: vulnerable_project) }
let!(:old_vuln) { create_vulnerability(vulnerable_project) } let!(:old_vuln) { create_vulnerability(vulnerable_project) }
...@@ -20,8 +20,10 @@ shared_examples_for Vulnerable do ...@@ -20,8 +20,10 @@ shared_examples_for Vulnerable do
end end
def create_vulnerability(project, pipeline = nil) def create_vulnerability(project, pipeline = nil)
pipeline ||= create(:ci_pipeline, :success, project: project) if project
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project) pipeline ||= create(:ci_pipeline, :success, project: project)
create(:vulnerabilities_occurrence, pipelines: [pipeline], project: project)
end
end end
describe '#latest_vulnerabilities' do describe '#latest_vulnerabilities' do
......
# frozen_string_literal: true
require 'spec_helper'
shared_examples 'instance security dashboard JSON endpoint' do
context 'when the user is authenticated' do
let(:security_application_controller_user) { create(:user) }
before do
stub_licensed_features(security_dashboard: true)
login_as(security_application_controller_user)
end
it 'responds with success' do
security_dashboard_request
expect(response).to have_gitlab_http_status(:ok)
end
context 'and the instance does not have an Ultimate license' do
it '404s' do
stub_licensed_features(security_dashboard: false)
security_dashboard_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'and the security dashboard feature is disabled' do
it '404s' do
stub_feature_flags(security_dashboard: false)
security_dashboard_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when the user is not authenticated' do
it 'responds with a 401' do
security_dashboard_request
expect(response).to have_gitlab_http_status(:unauthorized)
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