Commit c5e1c3c8 authored by Patrick Bair's avatar Patrick Bair

Merge branch '345905-cluster-image-scanning-vulnerability-resolution-api-endpoint' into 'master'

Add internal Starboard vulnerability resolution API endpoint

See merge request gitlab-org/gitlab!79406
parents b249087d f0a5f300
......@@ -561,6 +561,29 @@ Example response:
}
```
### Resolve Starboard vulnerabilities
Called from the GitLab Agent Server (`kas`) to resolve Starboard security vulnerabilities.
Accepts a list of finding UUIDs and marks all Starboard vulnerabilities not identified by
the list as resolved.
| Attribute | Type | Required | Description |
|:----------|:-------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------|
| `uuids` | string array | yes | UUIDs of detected vulnerabilities, as collected from [Create Starboard vulnerability](#create-starboard-vulnerability) responses. |
```plaintext
POST internal/kubernetes/modules/starboard_vulnerability/scan_result
```
Example Request:
```shell
curl --request POST --header "Gitlab-Kas-Api-Request: <JWT token>" \
--header "Authorization: Bearer <agent token>" --header "Content-Type: application/json" \
--url "http://localhost:3000/api/v4/internal/kubernetes/modules/starboard_vulnerability/scan_result" \
--data '{ "uuids": ["102e8a0a-fe29-59bd-b46c-57c3e9bc6411", "5eb12985-0ed5-51f4-b545-fd8871dc2870"] }'
```
## Subscriptions
The subscriptions endpoint is used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
......
......@@ -74,6 +74,7 @@ module EE
scope :with_findings, -> { includes(:findings) }
scope :with_findings_by_uuid, -> (uuid) { with_findings.where(findings: { uuid: uuid }) }
scope :with_findings_by_uuid_and_state, -> (uuid, state) { with_findings.where(findings: { uuid: uuid }, state: state) }
scope :with_findings_excluding_uuid, -> (uuid) { with_findings.where.not(findings: { uuid: uuid }) }
scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) }
scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) }
......
# frozen_string_literal: true
module Vulnerabilities
class StarboardVulnerabilityResolveService
include Gitlab::Allowable
REPORT_TYPE = ::Enums::Vulnerability.report_types[:cluster_image_scanning]
STATES = Vulnerability::ACTIVE_STATES
BATCH_SIZE = 250
attr_reader :project,
:author,
:uuids
def initialize(agent, uuids)
@project = agent.project
@author = agent.created_by_user
@uuids = uuids
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
undetected.each_batch(of: BATCH_SIZE) do |batch|
batch.update_all(resolved_on_default_branch: true, state: :resolved)
end
ServiceResponse.success
end
private
def undetected
project.vulnerabilities
.with_states(STATES)
.with_report_types(REPORT_TYPE)
.with_findings_excluding_uuid(uuids)
end
def authorized?
can?(author, :admin_vulnerability, project)
end
end
end
......@@ -125,6 +125,23 @@ module EE
render_api_error!(result.message, result.http_status)
end
end
desc 'POST scan_result' do
detail 'Resolves all active Cluster Image Scanning vulnerabilities with finding UUIDs not present in the payload'
end
params do
requires :uuids, type: Array[String], desc: 'Finding UUIDs collected from a scan'
end
route_setting :authentication, cluster_agent_token_allowed: true
post "/scan_result" do
not_found! if agent.project.nil?
service = ::Vulnerabilities::StarboardVulnerabilityResolveService.new(agent, params[:uuids])
result = service.execute
status result.http_status
end
end
end
end
......
......@@ -857,7 +857,7 @@ RSpec.describe Vulnerability do
describe '.with_findings_by_uuid_and_state scope' do
let_it_be(:vulnerability) { create(:vulnerability, state: :detected) }
let(:uuid) { [SecureRandom.uuid] }
let(:uuid) { ["592d0922-232a-470b-84e9-5ce1c7aa9477"] }
subject { described_class.with_findings_by_uuid_and_state(uuid, ["detected"]) }
......@@ -875,4 +875,20 @@ RSpec.describe Vulnerability do
end
end
end
describe '.with_findings_excluding_uuid scope' do
let_it_be(:vulnerability) { create(:vulnerability, :with_finding) }
let(:uuid) { vulnerability.finding.uuid }
subject { described_class.with_findings_excluding_uuid(uuid) }
it { is_expected.not_to include(vulnerability) }
context 'with mismatching uuid' do
let(:uuid) { [SecureRandom.uuid] }
it { is_expected.to include(vulnerability) }
end
end
end
......@@ -326,4 +326,85 @@ RSpec.describe API::Internal::Kubernetes do
end
end
end
describe 'POST /internal/kubernetes/modules/starboard_vulnerability/scan_result' do
let(:method) { :post }
let(:api_url) { '/internal/kubernetes/modules/starboard_vulnerability/scan_result' }
let_it_be(:agent_token) { create(:cluster_agent_token) }
let_it_be(:agent) { agent_token.agent }
let_it_be(:project) { agent.project }
let_it_be(:existing_vulnerabilities) { create_list(:vulnerability, 4, :detected, :with_finding, project: project, report_type: :cluster_image_scanning) }
let_it_be(:detected_vulnerabilities) { existing_vulnerabilities.first(2) }
let_it_be(:undetected_vulnerabilities) { existing_vulnerabilities - detected_vulnerabilities }
let_it_be(:payload) { { uuids: detected_vulnerabilities.map { |vuln| vuln.finding.uuid } } }
include_examples 'authorization'
include_examples 'agent authentication'
subject { send_request(params: payload) }
context 'is authenticated for an agent' do
before do
stub_licensed_features(security_dashboard: true)
end
before_all do
project.add_maintainer(agent.created_by_user)
end
it 'returns ok' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'resolves undetected vulnerabilities' do
subject
expect(Vulnerability.resolved).to match_array(undetected_vulnerabilities)
end
it 'marks undetected vulnerabilities as resolved on default branch' do
subject
expect(Vulnerability.with_resolution).to match_array(undetected_vulnerabilities)
end
it 'does not resolve vulnerabilities with other report types' do
Vulnerability.where(id: undetected_vulnerabilities).update_all(report_type: :container_scanning)
expect { subject }.not_to change { Vulnerability.resolved.count }
end
it "does not resolve other projects' vulnerabilities" do
Vulnerability.where(id: undetected_vulnerabilities).update_all(project_id: create(:project).id)
expect { subject }.not_to change { Vulnerability.resolved.count }
end
context 'when payload is invalid' do
let(:payload) { { uuids: -1 } }
it 'returns bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns forbidden for non licensed project' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::StarboardVulnerabilityResolveService do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:project) { agent.project }
let_it_be(:user) { agent.created_by_user }
let_it_be(:existing_vulnerabilities) { create_list(:vulnerability, 4, :detected, :with_finding, project: project, report_type: :cluster_image_scanning) }
let_it_be(:detected_vulnerabilities) { existing_vulnerabilities.first(2) }
let_it_be(:undetected_vulnerabilities) { existing_vulnerabilities - detected_vulnerabilities }
let_it_be(:uuids) { detected_vulnerabilities.map(&:finding).map(&:uuid) }
subject(:service) { described_class.new(agent, uuids) }
describe "#new" do
specify { expect(service.author).to be(user) }
specify { expect(service.project).to be(project) }
specify { expect(service.uuids).to eq(uuids) }
end
describe "#execute" do
subject { service.execute }
context 'with authorized user' do
before_all do
project.add_developer(user)
end
context 'with feature enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'resolves vulnerabilities' do
subject
expect(Vulnerability.resolved).to match_array(undetected_vulnerabilities)
end
it 'marks vulnerabilities as resolved on default branch' do
subject
expect(Vulnerability.with_resolution).to match_array(undetected_vulnerabilities)
end
it 'does not resolve vulnerabilities with other report types' do
Vulnerability.where(id: undetected_vulnerabilities).update_all(report_type: :container_scanning)
expect { subject }.not_to change { Vulnerability.resolved.count }
end
it "does not resolve other projects' vulnerabilities" do
Vulnerability.where(id: undetected_vulnerabilities).update_all(project_id: create(:project).id)
expect { subject }.not_to change { Vulnerability.resolved.count }
end
it 'does not resolve vulnerabilities in passive states' do
EE::Vulnerability::PASSIVE_STATES.each do |state|
Vulnerability.where(id: undetected_vulnerabilities).update_all(state: state)
expect { subject }.not_to change { Vulnerability.resolved.count }
end
end
end
context 'with feature disabled' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'raises AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'with unauthorized user' do
before_all do
project.add_reporter(user)
end
it 'raises AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment