Commit 87d63f10 authored by Imre Farkas's avatar Imre Farkas

Merge branch 'add-history-to-vulnerability' into 'master'

Query for vulnerability history

See merge request gitlab-org/gitlab!27052
parents 5407ed81 5825c12f
...@@ -7,6 +7,10 @@ class Vulnerability < ApplicationRecord ...@@ -7,6 +7,10 @@ class Vulnerability < ApplicationRecord
include Noteable include Noteable
include Awardable include Awardable
TooManyDaysError = Class.new(StandardError)
MAX_DAYS_IN_PAST = 10
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true cache_markdown_field :description, issuable_state_filter_enabled: true
...@@ -63,6 +67,28 @@ class Vulnerability < ApplicationRecord ...@@ -63,6 +67,28 @@ class Vulnerability < ApplicationRecord
delegate :default_branch, to: :project, prefix: :project delegate :default_branch, to: :project, prefix: :project
def self.counts_by_day_and_severity(num_days_in_past, end_date = Date.current)
return [] unless Feature.enabled?(:vulnerability_history, default_enabled: true)
# this clause guards against query timeouts
raise TooManyDaysError, "Cannot fetch counts for more than #{MAX_DAYS_IN_PAST} days" if num_days_in_past > MAX_DAYS_IN_PAST
quoted_num_days_in_past = connection.quote(num_days_in_past)
quoted_end_date = connection.quote(end_date)
select(
'DATE(calendar.entry) AS day, severity, COUNT(*)'
).from(
"generate_series(DATE #{quoted_end_date} - INTERVAL '#{quoted_num_days_in_past} days', DATE #{quoted_end_date}, INTERVAL '1 day') as calendar(entry)"
).joins(
'INNER JOIN vulnerabilities ON vulnerabilities.created_at <= calendar.entry'
).where(
'(vulnerabilities.dismissed_at IS NULL OR vulnerabilities.dismissed_at > calendar.entry) AND (vulnerabilities.resolved_at IS NULL OR vulnerabilities.resolved_at > calendar.entry)'
).group(
:day, :severity
)
end
# There will only be one finding associated with a vulnerability for the foreseeable future # There will only be one finding associated with a vulnerability for the foreseeable future
def finding def finding
findings.first findings.first
......
...@@ -167,6 +167,82 @@ describe Vulnerability do ...@@ -167,6 +167,82 @@ describe Vulnerability do
end end
end end
describe '.counts_by_day_and_severity' do
context 'when the vulnerability_history feature flag is disabled' do
before do
stub_feature_flags(vulnerability_history: false)
end
it 'returns an empty array' do
create(:vulnerability, created_at: 1.day.ago)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6)
expect(counts_by_day_and_severity).to be_empty
end
end
context 'when the vulnerability_history feature flag is enabled' do
before do
stub_feature_flags(vulnerability_history: true)
end
context 'when not given an end date' do
it 'returns the count of unresolved, undismissed vulnerabilities for each severity from the current day to the given number of days in the past' do
Timecop.freeze(Time.zone.parse('2019-10-31')) do
create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical)
create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high)
create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6)
expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-27', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-27', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-28', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-28', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-29', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-29', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-30', 'count' => 1 }
].to_json)
end
end
end
context 'when given an end date' do
it 'returns the count of unresolved, undismissed vulnerabilities for each severity for each day from the given end date to the given number of days in the past' do
Timecop.freeze(Time.zone.parse('2019-10-31')) do
create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical)
create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high)
create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6, Date.parse('2019-10-28'))
expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-27', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-27', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-28', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-28', 'count' => 2 }
].to_json)
end
end
end
context 'when given a number of past days greater than 10' do
it 'raises a TooManyDaysError' do
expect { Vulnerability.counts_by_day_and_severity(11) }.to raise_error(
Vulnerability::TooManyDaysError,
'Cannot fetch counts for more than 10 days'
)
end
end
end
end
describe '#finding' do describe '#finding' do
let_it_be(:project) { create(:project, :with_vulnerabilities) } let_it_be(:project) { create(:project, :with_vulnerabilities) }
let_it_be(:vulnerability) { project.vulnerabilities.first } let_it_be(:vulnerability) { project.vulnerabilities.first }
......
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