Commit 4b35f2ac authored by Fabien Catteau's avatar Fabien Catteau Committed by Kamil Trzciński

API endpoints for Group-level Security Dashboard

parent f2447c88
......@@ -78,6 +78,11 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
# EE-specific start
namespace :security do
resource :dashboard, only: [:show], controller: :dashboard
resources :vulnerabilities, only: [:index], controller: :vulnerabilities do
collection do
get :summary
end
end
end
# EE-specific end
......
require './spec/support/sidekiq'
class Gitlab::Seeder::Vulnerabilities
attr_reader :project
def initialize(project)
@project = project
end
def seed!
return unless pipeline
10.times do |rank|
occurrence = create_occurrence(rank)
create_occurrence_identifier(occurrence, rank, primary: true)
create_occurrence_identifier(occurrence, rank)
if author
case rank % 3
when 0
create_feedback(occurrence, 'dismissal')
when 1
create_feedback(occurrence, 'issue')
else
# no feedback
end
end
end
end
private
def create_occurrence(rank)
project.vulnerabilities.create!(
uuid: random_uuid,
name: 'Cipher with no integrity',
pipeline: pipeline,
ref: project.default_branch,
report_type: :sast,
severity: random_level,
confidence: random_level,
project_fingerprint: random_fingerprint,
primary_identifier_fingerprint: random_fingerprint,
location_fingerprint: random_fingerprint,
raw_metadata: metadata(rank).to_json,
metadata_version: 'sast:1.0',
scanner: scanner)
end
def create_occurrence_identifier(occurrence, key, primary: false)
type = primary ? 'primary' : 'secondary'
fingerprint = if primary
occurrence.primary_identifier_fingerprint
else
Digest::SHA1.hexdigest("sid_fingerprint-#{project.id}-#{key}")
end
project.vulnerability_identifiers.create!(
external_type: "#{type.upcase}_SECURITY_ID",
external_id: "#{type.upcase}_SECURITY_#{key}",
fingerprint: fingerprint,
name: "#{type.capitalize} #{key}",
url: "https://security.example.com/#{type.downcase}/#{key}"
)
end
def create_feedback(occurrence, type)
issue = create_issue("Dismiss #{occurrence.name}") if type == 'issue'
project.vulnerability_feedback.create!(
feedback_type: type,
category: 'sast',
author: author,
issue: issue,
pipeline: pipeline,
project_fingerprint: occurrence.project_fingerprint,
vulnerability_data: { category: 'sast' })
end
def scanner
@scanner ||= project.vulnerability_scanners.create!(
project: project,
external_id: 'security-scanner',
name: 'Security Scanner')
end
def create_issue(title)
project.issues.create!(author: author, title: title)
end
def random_level
::Vulnerabilities::Occurrence::LEVELS.keys.sample
end
def metadata(line)
{
description: "The cipher does not provide data integrity update 1",
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
location: {
file: "maven/src/main/java//App.java",
start_line: line,
end_line: line,
class: "com.gitlab..App",
method: "insecureCypher"
},
links: [
{
name: "Cipher does not check for integrity first?",
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
}
]
}
end
def random_uuid
SecureRandom.hex(18)
end
def random_fingerprint
SecureRandom.hex(20)
end
def pipeline
@pipeline ||= project.pipelines.where(ref: project.default_branch).last
end
def author
@author ||= project.users.first
end
end
Gitlab::Seeder.quiet do
Project.joins(:pipelines).uniq.all.sample(5).each do |project|
seeder = Gitlab::Seeder::Vulnerabilities.new(project)
seeder.seed!
end
end
# frozen_string_literal: true
class Groups::Security::VulnerabilitiesController < Groups::ApplicationController
before_action :ensure_security_dashboard_feature_enabled
before_action :authorize_read_group_security_dashboard!
def index
@vulnerabilities = group.all_vulnerabilities.ordered
.page(params[:page])
.per(10)
.to_a
::Gitlab::Vulnerabilities::OccurrencesPreloader.new.preload(@vulnerabilities) # rubocop:disable CodeReuse/ActiveRecord
respond_to do |format|
format.json do
render json: Vulnerabilities::OccurrenceSerializer
.new(current_user: @current_user)
.with_pagination(request, response)
.represent(@vulnerabilities)
end
end
end
def summary
respond_to do |format|
format.json do
render json: VulnerabilitySummarySerializer.new.represent(group)
end
end
end
private
def ensure_security_dashboard_feature_enabled
render_404 unless @group.feature_available?(:security_dashboard)
end
def authorize_read_group_security_dashboard!
render_403 unless can?(current_user, :read_group_security_dashboard, group)
end
end
module Projects
module Security
class DashboardController < Projects::ApplicationController
before_action :ensure_security_features_enabled
before_action :ensure_security_dashboard_feature_enabled
before_action :authorize_read_project_security_dashboard!
def show
......@@ -10,8 +10,8 @@ module Projects
private
def ensure_security_features_enabled
render_404 unless @project.security_reports_feature_available?
def ensure_security_dashboard_feature_enabled
render_404 unless @project.feature_available?(:security_dashboard)
end
end
end
......
......@@ -68,6 +68,10 @@ module EE
end
end
def all_vulnerabilities
Vulnerabilities::Occurrence.where(project: all_projects)
end
def human_ldap_access
::Gitlab::Access.options_with_owner.key(ldap_access)
end
......
......@@ -104,13 +104,6 @@ module EE
end
end
def security_reports_feature_available?
feature_available?(:sast) ||
feature_available?(:dependency_scanning) ||
feature_available?(:sast_container) ||
feature_available?(:dast)
end
def latest_pipeline_with_security_reports
pipelines.newest_first(default_branch).with_security_reports.first
end
......
......@@ -72,6 +72,7 @@ class License < ActiveRecord::Base
].freeze
EEU_FEATURES = EEP_FEATURES + %i[
security_dashboard
dependency_scanning
license_management
sast
......
......@@ -3,6 +3,7 @@
module Vulnerabilities
class Occurrence < ActiveRecord::Base
include ShaAttribute
include ::Gitlab::Utils::StrongMemoize
self.table_name = "vulnerability_occurrences"
......@@ -29,12 +30,14 @@ module Vulnerabilities
has_many :occurrence_identifiers, class_name: 'Vulnerabilities::OccurrenceIdentifier'
has_many :identifiers, through: :occurrence_identifiers, class_name: 'Vulnerabilities::Identifier'
enum report_type: {
REPORT_TYPES = {
sast: 0,
dependency_scanning: 1,
container_scanning: 2,
dast: 3
}
}.with_indifferent_access.freeze
enum report_type: REPORT_TYPES
validates :scanner, presence: true
validates :project, presence: true
......@@ -56,6 +59,45 @@ module Vulnerabilities
validates :metadata_version, presence: true
validates :raw_metadata, presence: true
scope :ordered, -> { order("severity desc", :id) }
scope :counted_by_report_and_severity, -> { group(:report_type, :severity).count }
def feedback(feedback_type:)
params = {
project_id: project_id,
category: report_type,
project_fingerprint: project_fingerprint,
feedback_type: feedback_type
}
BatchLoader.for(params).batch do |items, loader|
project_ids = items.group_by { |i| i[:project_id] }
categories = items.group_by { |i| i[:category] }
fingerprints = items.group_by { |i| i[:project_fingerprint] }
VulnerabilityFeedback.where(
project_id: project_ids.keys,
category: categories.keys,
project_fingerprint: fingerprints.keys).find_each do |feedback|
loaded_params = {
project_id: feedback.project_id,
category: feedback.category,
project_fingerprint: feedback.project_fingerprint,
feedback_type: feedback.feedback_type
}
loader.call(loaded_params, feedback)
end
end
end
def dismissal_feedback
feedback(feedback_type: 'dismissal')
end
def issue_feedback
feedback(feedback_type: 'issue')
end
# Override getter and setter for :severity as we can't use enum (it conflicts with :confidence)
# To be replaced with enum using _prefix when migrating to rails 5
def severity
......@@ -75,5 +117,31 @@ module Vulnerabilities
def confidence=(confidence)
write_attribute(:confidence, LEVELS[confidence])
end
def metadata
strong_memoize(:metadata) do
begin
JSON.parse(raw_metadata)
rescue JSON::ParserError
{}
end
end
end
def description
metadata.dig('description')
end
def solution
metadata.dig('solution')
end
def location
metadata.fetch('location', {})
end
def links
metadata.fetch('links', [])
end
end
end
......@@ -25,6 +25,10 @@ module EE
.allow_group_owners_to_manage_ldap
end
condition(:security_dashboard_feature_disabled) do
!@subject.feature_available?(:security_dashboard)
end
rule { reporter }.policy do
enable :admin_list
enable :admin_board
......@@ -65,6 +69,14 @@ module EE
rule { project_creation_level_enabled & developer & developer_maintainer_access }.enable :create_projects
rule { project_creation_level_enabled & create_projects_disabled }.prevent :create_projects
rule { developer }.policy do
enable :read_group_security_dashboard
end
rule { security_dashboard_feature_disabled }.policy do
prevent :read_group_security_dashboard
end
end
end
end
......@@ -55,7 +55,9 @@ module EE
end
with_scope :subject
condition(:security_reports_feature_available) { @subject.security_reports_feature_available? }
condition(:security_dashboard_feature_disabled) do
!@subject.feature_available?(:security_dashboard)
end
condition(:prometheus_alerts_enabled) do
@subject.feature_available?(:prometheus_alerts, @user)
......@@ -114,7 +116,13 @@ module EE
rule { can?(:public_access) }.enable :read_package
rule { can?(:developer_access) & security_reports_feature_available }.enable :read_project_security_dashboard
rule { can?(:developer_access) }.policy do
enable :read_project_security_dashboard
end
rule { security_dashboard_feature_disabled }.policy do
prevent :read_project_security_dashboard
end
rule { can?(:read_project) }.enable :read_vulnerability_feedback
......
# frozen_string_literal: true
class Vulnerabilities::IdentifierEntity < Grape::Entity
expose :external_type
expose :external_id
expose :name
expose :url
end
# frozen_string_literal: true
class Vulnerabilities::OccurrenceEntity < Grape::Entity
include RequestAwareEntity
expose :id, :report_type, :name, :severity, :confidence
expose :scanner, using: Vulnerabilities::ScannerEntity
expose :identifiers, using: Vulnerabilities::IdentifierEntity
expose :project_fingerprint
expose :vulnerability_feedback_url, if: ->(*) { can_admin_vulnerability_feedback? }
expose :project, using: ::ProjectEntity
expose :dismissal_feedback, using: VulnerabilityFeedbackEntity
expose :issue_feedback, using: VulnerabilityFeedbackEntity
expose :metadata, merge: true, if: ->(occurrence, _) { occurrence.raw_metadata } do
expose :description
expose :solution
expose :location
expose :links
end
alias_method :occurrence, :object
private
def vulnerability_feedback_url
project_vulnerability_feedback_index_url(occurrence.project)
end
def can_admin_vulnerability_feedback?
can?(request.current_user, :admin_vulnerability_feedback, occurrence.project)
end
end
class Vulnerabilities::OccurrenceSerializer < BaseSerializer
include WithPagination
entity Vulnerabilities::OccurrenceEntity
end
# frozen_string_literal: true
class Vulnerabilities::ScannerEntity < Grape::Entity
expose :external_id
expose :name
end
# frozen_string_literal: true
class VulnerabilitySummaryEntity < Grape::Entity
Vulnerabilities::Occurrence::REPORT_TYPES.each do |report_type_name, report_type|
expose report_type_name do
Vulnerabilities::Occurrence::LEVELS.each do |severity_name, severity|
expose severity_name do |group|
grouped_vulnerabilities[[report_type, severity]] || 0
end
end
end
end
private
def grouped_vulnerabilities
@grouped_by_report_and_severity ||= object.all_vulnerabilities.counted_by_report_and_severity
end
end
class VulnerabilitySummarySerializer < BaseSerializer
entity VulnerabilitySummaryEntity
end
# frozen_string_literal: true
module Gitlab
# Preloading of Vulnerabilities Occurrences.
#
# This class can be used to efficiently preload the feedback of a given list of
# vulnerabilities (occurrences).
module Vulnerabilities
class OccurrencesPreloader
def preload(occurrences)
occurrences.each(&:issue_feedback)
occurrences.each(&:dismissal_feedback)
end
end
end
end
require 'spec_helper'
describe Groups::Security::VulnerabilitiesController do
include ApiHelpers
set(:group) { create(:group) }
set(:group_other) { create(:group) }
set(:user) { create(:user) }
set(:project_dev) { create(:project, :private, :repository, group: group) }
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
sign_in(user)
end
describe 'GET index.json' do
subject { get :index, group_id: group, format: :json }
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)
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_occurrence, project: 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_list(:vulnerabilities_occurrence, 11, project: project)
end
end
it "returns a list of vulnerabilities" do
get :index, group_id: group, page: 3, 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
def get_summary
get :index, group_id: group, format: :json
end
it "avoids N+1 queries" do
control_count = ActiveRecord::QueryRecorder.new { get_summary }
# Create feedback
project_dev.vulnerabilities.each do |occ|
create(:vulnerability_feedback, :sast, :dismissal,
project: project_dev, project_fingerprint: occ.project_fingerprint)
create(:vulnerability_feedback, :sast, :issue,
issue: create(:issue, project: project),
project: project_dev, project_fingerprint: occ.project_fingerprint)
end
expect { get_summary }.not_to exceed_query_limit(control_count)
end
end
end
end
end
describe 'GET summary.json' do
subject { get :summary, group_id: group, format: :json }
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)
create_list(:vulnerabilities_occurrence, 3,
project: project_dev, report_type: :sast, severity: :high)
create_list(:vulnerabilities_occurrence, 1,
project: project_dev, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 2,
project: project_guest, report_type: :dependency_scanning, severity: :low)
create_list(:vulnerabilities_occurrence, 1,
project: project_guest, report_type: :dast, severity: :medium)
create_list(:vulnerabilities_occurrence, 1,
project: project_other, report_type: :dast, severity: :low)
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
it 'returns vulnerabilities counts' do
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Hash)
expect(json_response.dig('sast', 'high')).to eq(3)
expect(json_response.dig('dependency_scanning', 'low')).to eq(3)
expect(json_response.dig('dast', 'medium')).to eq(1)
expect(response).to match_response_schema('vulnerabilities/summary', dir: 'ee')
end
end
end
end
end
......@@ -34,10 +34,12 @@ describe Projects::Security::DashboardController do
get :show, namespace_id: project.namespace, project_id: project
end
context 'when security reports features are enabled' do
it 'returns the latest pipeline with security reports for project' do
stub_licensed_features(sast: true)
context 'when security dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns the latest pipeline with security reports for project' do
show_security_dashboard
expect(response).to have_gitlab_http_status(200)
......@@ -45,10 +47,12 @@ describe Projects::Security::DashboardController do
end
end
context 'when security reports features are disabled' do
it 'returns the latest pipeline with security reports for project' do
stub_licensed_features(sast: false, dependency_scanning: false, sast_container: false, dast: false)
context 'when security dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns 404' do
show_security_dashboard
expect(response).to have_gitlab_http_status(404)
......@@ -59,9 +63,11 @@ describe Projects::Security::DashboardController do
context 'with unauthorized user for security dashboard' do
let(:guest) { create(:user) }
it 'returns a not found 404 response' do
stub_licensed_features(sast: true)
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns a not found 404 response' do
group.add_guest(guest)
show_security_dashboard guest
......
# frozen_string_literal: true
FactoryBot.define do
sequence :vulnerability_occurrence_uuid do |n|
Digest::SHA1.hexdigest("uuid-#{n}")[0..35]
end
factory :vulnerabilities_occurrence, class: Vulnerabilities::Occurrence do
name 'Cipher with no integrity'
project
pipeline factory: :ci_pipeline
ref 'master'
uuid 'a7342ca9-494e-457f-88e7-e65e145cc392'
project_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
sequence(:uuid) { generate(:vulnerability_occurrence_uuid) }
project_fingerprint { generate(:project_fingerprint) }
primary_identifier_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
location_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
report_type :sast
......@@ -15,6 +19,24 @@ FactoryBot.define do
confidence :medium
scanner factory: :vulnerabilities_scanner
metadata_version 'sast:1.0'
raw_metadata 'raw_metadata'
raw_metadata do
{
description: "The cipher does not provide data integrity update 1",
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
location: {
file: "maven/src/main/java/com/gitlab/security_products/tests/App.java",
start_line: 29,
end_line: 29,
class: "com.gitlab.security_products.tests.App",
method: "insecureCypher"
},
links: [
{
name: "Cipher does not check for integrity first?",
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
}
]
}.to_json
end
end
end
{
"type" : "object",
"required" : [
"name",
"project_fingerprint",
"confidence",
"severity",
"report_type",
"scanner",
"project"
],
"properties" : {
"name" : { "type": "string" },
"project_fingerprint": { "type": "string" },
"vulnerability_feedback_url": { "type": "string" },
"confidence" : {
"type": "string",
"enum": ["undefined", "ignore", "unknown", "experimental", "low", "medium", "high", "critical"]
},
"severity" : {
"type": "string",
"enum": ["undefined", "ignore", "unknown", "experimental", "low", "medium", "high", "critical"]
},
"report_type": {
"type": "string",
"enum": ["sast", "dependency_scanning", "container_scanning", "dast"]
},
"scanner" : {
"external_id" : { "type": "string" },
"name" : { "type": "string" }
},
"project" : {
"required" : [
"id",
"name",
"full_path",
"full_name"
],
"id" : { "type": "integer" },
"name" : { "type": "string" },
"full_path" : { "type": "string" },
"full_name" : { "type": "string" }
},
"issue_feedback" : { "oneOf": [
{ "type": "null" },
{ "$ref": "../vulnerability_feedback.json" }
]},
"dismissal_feedback" : { "oneOf": [
{ "type": "null" },
{ "$ref": "../vulnerability_feedback.json" }
]},
"description": { "type": "string" },
"solution": { "type": "string" },
"location" : {
"class" : { "type": "string" },
"method" : { "type": "string" },
"file" : { "type": "string" },
"start_line" : { "type": "integer" },
"end_line" : { "type": "integer" }
},
"links" : {
"type": "array",
"items": {
"name": { "type": ["string", "null"] },
"url": { "type": ["string", "null"] }
}
},
"identifiers" : {
"type": "array",
"items": {
"primary" : { "type": ["boolean"] },
"name": { "type": ["string"] },
"url": { "type": ["string", "null"] },
"external_id": { "type": ["string"] },
"external_type": { "type": ["string"] }
}
}
}
}
{
"type": "array",
"items": { "$ref": "occurrence.json" }
}
{
"type" : "object",
"required" : [
"dast",
"sast",
"container_scanning",
"dependency_scanning"
],
"properties" : {
"dast" : { "$ref": "summary_for_report_type.json" },
"sast" : { "$ref": "summary_for_report_type.json" },
"container_scanning" : { "$ref": "summary_for_report_type.json" },
"dependency_scanning" : { "$ref": "summary_for_report_type.json" }
},
"additional_properties" : false
}
{
"type" : "object",
"properties" : {
"undefined" : { "type": "integer" },
"ignore" : { "type": "integer" },
"unknown" : { "type": "integer" },
"experimental" : { "type": "integer" },
"low" : { "type": "integer" },
"medium" : { "type": "integer" },
"high" : { "type": "integer" },
"critical" : { "type": "integer" }
},
"additional_properties" : false
}
......@@ -1526,29 +1526,6 @@ describe Project do
end
end
describe '#security_reports_feature_available?' do
security_features = %i[sast dependency_scanning sast_container dast]
let(:project) { create(:project) }
security_features.each do |feature|
it "returns true when at least #{feature} is enabled" do
allow(project).to receive(:feature_available?) { false }
allow(project).to receive(:feature_available?).with(feature) { true }
expect(project.security_reports_feature_available?).to eq(true)
end
end
it "returns false when all security features are disabled" do
security_features.each do |feature|
allow(project).to receive(:feature_available?).with(feature) { false }
end
expect(project.security_reports_feature_available?).to eq(false)
end
end
describe '#latest_pipeline_with_security_reports' do
let(:project) { create(:project) }
let(:pipeline_1) { create(:ci_pipeline_without_jobs, project: project) }
......
......@@ -400,4 +400,68 @@ describe GroupPolicy do
end
end
end
describe 'read_group_security_dashboard' do
before do
stub_licensed_features(security_dashboard: true)
end
subject { described_class.new(current_user, group) }
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:read_group_security_dashboard) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group_security_dashboard) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:read_group_security_dashboard) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_allowed(:read_group_security_dashboard) }
context 'when security dashboard features is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it { is_expected.to be_disallowed(:read_group_security_dashboard) }
end
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:read_group_security_dashboard) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_group_security_dashboard) }
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:read_group_security_dashboard) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:read_group_security_dashboard) }
end
end
end
......@@ -306,7 +306,7 @@ describe ProjectPolicy do
describe 'read_project_security_dashboard' do
before do
allow(project).to receive(:security_reports_feature_available?).and_return(true)
stub_licensed_features(security_dashboard: true)
end
subject { described_class.new(current_user, project) }
......@@ -334,9 +334,9 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:read_project_security_dashboard) }
context 'when security reports features are not available' do
context 'when security dashboard features is not available' do
before do
allow(project).to receive(:security_reports_feature_available?).and_return(false)
stub_licensed_features(security_dashboard: false)
end
it { is_expected.to be_disallowed(:read_project_security_dashboard) }
......
require 'spec_helper'
describe Vulnerabilities::IdentifierEntity do
let(:identifier) { create(:vulnerabilities_identifier) }
let(:entity) do
described_class.represent(identifier)
end
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:external_type, :external_id, :name, :url)
end
end
end
require 'spec_helper'
describe Vulnerabilities::OccurrenceEntity do
set(:user) { create(:user) }
set(:project) { create(:project) }
let(:scanner) do
create(:vulnerabilities_scanner, project: project)
end
let(:identifiers) do
[
create(:vulnerabilities_identifier),
create(:vulnerabilities_identifier)
]
end
let(:occurrence) do
create(
:vulnerabilities_occurrence,
scanner: scanner,
project: project,
identifiers: identifiers
)
end
let!(:dismiss_feedback) do
create(:vulnerability_feedback, :sast, :dismissal,
project: project, project_fingerprint: occurrence.project_fingerprint)
end
let!(:issue_feedback) do
create(:vulnerability_feedback, :sast, :issue,
issue: create(:issue, project: project),
project: project, project_fingerprint: occurrence.project_fingerprint)
end
let(:request) { double('request') }
let(:entity) do
described_class.represent(occurrence, request: request)
end
describe '#as_json' do
subject { entity.as_json }
before do
allow(request).to receive(:current_user).and_return(user)
end
it 'contains required fields' do
expect(subject).to include(:id)
expect(subject).to include(:name, :report_type, :severity, :confidence, :project_fingerprint)
expect(subject).to include(:scanner, :project, :identifiers)
expect(subject).to include(:dismissal_feedback, :issue_feedback)
expect(subject).to include(:description, :solution, :location, :links)
end
context 'when not allowed to admin vulnerability feedback' do
before do
project.add_guest(user)
end
it 'does not contain vulnerability feedback URL' do
expect(subject).not_to include(:vulnerability_feedback_url)
end
end
context 'when allowed to admin vulnerability feedback' do
before do
project.add_developer(user)
end
it 'contains vulnerability feedback URL' do
expect(subject).to include(:vulnerability_feedback_url)
end
end
end
end
require 'spec_helper'
describe Vulnerabilities::ScannerEntity do
let(:scanner) { create(:vulnerabilities_scanner) }
let(:entity) do
described_class.represent(scanner)
end
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:name, :external_id)
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