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 ...@@ -78,6 +78,11 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
# EE-specific start # EE-specific start
namespace :security do namespace :security do
resource :dashboard, only: [:show], controller: :dashboard resource :dashboard, only: [:show], controller: :dashboard
resources :vulnerabilities, only: [:index], controller: :vulnerabilities do
collection do
get :summary
end
end
end end
# EE-specific 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 Projects
module Security module Security
class DashboardController < Projects::ApplicationController class DashboardController < Projects::ApplicationController
before_action :ensure_security_features_enabled before_action :ensure_security_dashboard_feature_enabled
before_action :authorize_read_project_security_dashboard! before_action :authorize_read_project_security_dashboard!
def show def show
...@@ -10,8 +10,8 @@ module Projects ...@@ -10,8 +10,8 @@ module Projects
private private
def ensure_security_features_enabled def ensure_security_dashboard_feature_enabled
render_404 unless @project.security_reports_feature_available? render_404 unless @project.feature_available?(:security_dashboard)
end end
end end
end end
......
...@@ -68,6 +68,10 @@ module EE ...@@ -68,6 +68,10 @@ module EE
end end
end end
def all_vulnerabilities
Vulnerabilities::Occurrence.where(project: all_projects)
end
def human_ldap_access def human_ldap_access
::Gitlab::Access.options_with_owner.key(ldap_access) ::Gitlab::Access.options_with_owner.key(ldap_access)
end end
......
...@@ -104,13 +104,6 @@ module EE ...@@ -104,13 +104,6 @@ module EE
end end
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 def latest_pipeline_with_security_reports
pipelines.newest_first(default_branch).with_security_reports.first pipelines.newest_first(default_branch).with_security_reports.first
end end
......
...@@ -72,6 +72,7 @@ class License < ActiveRecord::Base ...@@ -72,6 +72,7 @@ class License < ActiveRecord::Base
].freeze ].freeze
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
security_dashboard
dependency_scanning dependency_scanning
license_management license_management
sast sast
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Vulnerabilities module Vulnerabilities
class Occurrence < ActiveRecord::Base class Occurrence < ActiveRecord::Base
include ShaAttribute include ShaAttribute
include ::Gitlab::Utils::StrongMemoize
self.table_name = "vulnerability_occurrences" self.table_name = "vulnerability_occurrences"
...@@ -29,12 +30,14 @@ module Vulnerabilities ...@@ -29,12 +30,14 @@ module Vulnerabilities
has_many :occurrence_identifiers, class_name: 'Vulnerabilities::OccurrenceIdentifier' has_many :occurrence_identifiers, class_name: 'Vulnerabilities::OccurrenceIdentifier'
has_many :identifiers, through: :occurrence_identifiers, class_name: 'Vulnerabilities::Identifier' has_many :identifiers, through: :occurrence_identifiers, class_name: 'Vulnerabilities::Identifier'
enum report_type: { REPORT_TYPES = {
sast: 0, sast: 0,
dependency_scanning: 1, dependency_scanning: 1,
container_scanning: 2, container_scanning: 2,
dast: 3 dast: 3
} }.with_indifferent_access.freeze
enum report_type: REPORT_TYPES
validates :scanner, presence: true validates :scanner, presence: true
validates :project, presence: true validates :project, presence: true
...@@ -56,6 +59,45 @@ module Vulnerabilities ...@@ -56,6 +59,45 @@ module Vulnerabilities
validates :metadata_version, presence: true validates :metadata_version, presence: true
validates :raw_metadata, 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) # 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 # To be replaced with enum using _prefix when migrating to rails 5
def severity def severity
...@@ -75,5 +117,31 @@ module Vulnerabilities ...@@ -75,5 +117,31 @@ module Vulnerabilities
def confidence=(confidence) def confidence=(confidence)
write_attribute(:confidence, LEVELS[confidence]) write_attribute(:confidence, LEVELS[confidence])
end 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
end end
...@@ -25,6 +25,10 @@ module EE ...@@ -25,6 +25,10 @@ module EE
.allow_group_owners_to_manage_ldap .allow_group_owners_to_manage_ldap
end end
condition(:security_dashboard_feature_disabled) do
!@subject.feature_available?(:security_dashboard)
end
rule { reporter }.policy do rule { reporter }.policy do
enable :admin_list enable :admin_list
enable :admin_board enable :admin_board
...@@ -65,6 +69,14 @@ module EE ...@@ -65,6 +69,14 @@ module EE
rule { project_creation_level_enabled & developer & developer_maintainer_access }.enable :create_projects rule { project_creation_level_enabled & developer & developer_maintainer_access }.enable :create_projects
rule { project_creation_level_enabled & create_projects_disabled }.prevent :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 end
end end
...@@ -55,7 +55,9 @@ module EE ...@@ -55,7 +55,9 @@ module EE
end end
with_scope :subject 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 condition(:prometheus_alerts_enabled) do
@subject.feature_available?(:prometheus_alerts, @user) @subject.feature_available?(:prometheus_alerts, @user)
...@@ -114,7 +116,13 @@ module EE ...@@ -114,7 +116,13 @@ module EE
rule { can?(:public_access) }.enable :read_package 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 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 ...@@ -34,10 +34,12 @@ describe Projects::Security::DashboardController do
get :show, namespace_id: project.namespace, project_id: project get :show, namespace_id: project.namespace, project_id: project
end end
context 'when security reports features are enabled' do context 'when security dashboard feature is enabled' do
it 'returns the latest pipeline with security reports for project' do before do
stub_licensed_features(sast: true) stub_licensed_features(security_dashboard: true)
end
it 'returns the latest pipeline with security reports for project' do
show_security_dashboard show_security_dashboard
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
...@@ -45,10 +47,12 @@ describe Projects::Security::DashboardController do ...@@ -45,10 +47,12 @@ describe Projects::Security::DashboardController do
end end
end end
context 'when security reports features are disabled' do context 'when security dashboard feature is disabled' do
it 'returns the latest pipeline with security reports for project' do before do
stub_licensed_features(sast: false, dependency_scanning: false, sast_container: false, dast: false) stub_licensed_features(security_dashboard: false)
end
it 'returns 404' do
show_security_dashboard show_security_dashboard
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
...@@ -59,9 +63,11 @@ describe Projects::Security::DashboardController do ...@@ -59,9 +63,11 @@ describe Projects::Security::DashboardController do
context 'with unauthorized user for security dashboard' do context 'with unauthorized user for security dashboard' do
let(:guest) { create(:user) } let(:guest) { create(:user) }
it 'returns a not found 404 response' do before do
stub_licensed_features(sast: true) stub_licensed_features(security_dashboard: true)
end
it 'returns a not found 404 response' do
group.add_guest(guest) group.add_guest(guest)
show_security_dashboard guest show_security_dashboard guest
......
# frozen_string_literal: true # frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
sequence :vulnerability_occurrence_uuid do |n|
Digest::SHA1.hexdigest("uuid-#{n}")[0..35]
end
factory :vulnerabilities_occurrence, class: Vulnerabilities::Occurrence do factory :vulnerabilities_occurrence, class: Vulnerabilities::Occurrence do
name 'Cipher with no integrity' name 'Cipher with no integrity'
project project
pipeline factory: :ci_pipeline pipeline factory: :ci_pipeline
ref 'master' ref 'master'
uuid 'a7342ca9-494e-457f-88e7-e65e145cc392' sequence(:uuid) { generate(:vulnerability_occurrence_uuid) }
project_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4' project_fingerprint { generate(:project_fingerprint) }
primary_identifier_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4' primary_identifier_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
location_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4' location_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
report_type :sast report_type :sast
...@@ -15,6 +19,24 @@ FactoryBot.define do ...@@ -15,6 +19,24 @@ FactoryBot.define do
confidence :medium confidence :medium
scanner factory: :vulnerabilities_scanner scanner factory: :vulnerabilities_scanner
metadata_version 'sast:1.0' 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
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 ...@@ -1526,29 +1526,6 @@ describe Project do
end end
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 describe '#latest_pipeline_with_security_reports' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:pipeline_1) { create(:ci_pipeline_without_jobs, project: project) } let(:pipeline_1) { create(:ci_pipeline_without_jobs, project: project) }
......
...@@ -400,4 +400,68 @@ describe GroupPolicy do ...@@ -400,4 +400,68 @@ describe GroupPolicy do
end end
end 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 end
...@@ -306,7 +306,7 @@ describe ProjectPolicy do ...@@ -306,7 +306,7 @@ describe ProjectPolicy do
describe 'read_project_security_dashboard' do describe 'read_project_security_dashboard' do
before do before do
allow(project).to receive(:security_reports_feature_available?).and_return(true) stub_licensed_features(security_dashboard: true)
end end
subject { described_class.new(current_user, project) } subject { described_class.new(current_user, project) }
...@@ -334,9 +334,9 @@ describe ProjectPolicy do ...@@ -334,9 +334,9 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:read_project_security_dashboard) } 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 before do
allow(project).to receive(:security_reports_feature_available?).and_return(false) stub_licensed_features(security_dashboard: false)
end end
it { is_expected.to be_disallowed(:read_project_security_dashboard) } 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