Commit 3c77db60 authored by charlie ablett's avatar charlie ablett

Merge branch 'scanned_resources_csv_endpoint_223673' into 'master'

Get the scanned resources for a Pipeline as a CSV

See merge request gitlab-org/gitlab!36019
parents 095f0970 5c7226dc
# frozen_string_literal: true
module Projects
module Security
class ScannedResourcesController < ::Projects::ApplicationController
before_action :authorize_read_vulnerability!
before_action :scanned_resources
def index
respond_to do |format|
format.csv do
send_data(
render_csv,
type: 'text/csv; charset=utf-8'
)
end
end
end
private
def scanned_resources
pipeline = project.ci_pipelines.find(pipeline_id)
@scanned_resources = pipeline&.security_reports&.reports&.fetch('dast', nil)&.scanned_resources
return if @scanned_resources
render_404
end
def render_csv
CsvBuilders::SingleBatch.new(
::Gitlab::Ci::Parsers::Security::ScannedResources.new.scanned_resources_for_csv(@scanned_resources),
{
'Method': 'request_method',
'Scheme': 'scheme',
'Host': 'host',
'Port': 'port',
'Path': 'path',
'Query String': 'query_string'
}
).render
end
def pipeline_id
params.require(:pipeline_id)
end
def authorize_read_vulnerability!
return if can?(current_user, :read_vulnerability, project)
render_404
end
end
end
end
---
title: Allow users to download the DAST scanned resources for a pipeline as a CSV
merge_request: 36019
author:
type: added
...@@ -78,6 +78,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -78,6 +78,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
resources :scanned_resources, only: [:index]
resources :vulnerabilities, only: [:show] do resources :vulnerabilities, only: [:show] do
member do member do
get :discussions, format: :json get :discussions, format: :json
......
...@@ -13,6 +13,21 @@ module Gitlab ...@@ -13,6 +13,21 @@ module Gitlab
scanned_resources_sum scanned_resources_sum
end end
def scanned_resources_for_csv(scanned_resources)
scanned_resources.map do |sr|
uri = URI.parse(sr['url'] || '')
OpenStruct.new({
request_method: sr['method'],
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: uri.path,
query_string: uri.query,
raw_url: sr['url']
})
end
end
private private
def parse_report_json(blob) def parse_report_json(blob)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Security::ScannedResourcesController do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
let_it_be(:action_params) { { project_id: project, namespace_id: project.namespace, pipeline_id: pipeline } }
before do
stub_licensed_features(dast: true, security_dashboard: true)
sign_in(user)
end
describe 'GET index' do
let(:subject) { get :index, params: action_params, format: :csv }
let(:parsed_csv_data) { CSV.parse(subject.body, headers: true) }
before do
project.add_developer(user)
end
context 'when DAST security scan is found' do
before do
create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) do |job|
create(:ee_ci_job_artifact, :dast, job: job, project: project)
end
end
it 'returns a CSV representation of the scanned resources' do
expect_next_instance_of(::Gitlab::Ci::Parsers::Security::ScannedResources) do |instance|
expect(instance).to receive(:scanned_resources_for_csv).and_return([])
end
expect(subject).to have_gitlab_http_status(:ok)
end
context 'when the pipeline id is missing' do
let_it_be(:action_params) { { project_id: project, namespace_id: project.namespace } }
before do
project.add_developer(user)
end
it 'raises an error when pipeline_id param is missing' do
expect { subject }.to raise_error(ActionController::ParameterMissing)
end
end
end
end
end
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Security::ScannedResources do RSpec.describe Gitlab::Ci::Parsers::Security::ScannedResources do
describe '#scanned_resources_count' do let(:parser) { described_class.new }
let(:parser) { described_class.new }
describe '#scanned_resources_count' do
subject { parser.scanned_resources_count(artifact) } subject { parser.scanned_resources_count(artifact) }
context 'there are scanned resources' do context 'there are scanned resources' do
...@@ -32,4 +32,61 @@ RSpec.describe Gitlab::Ci::Parsers::Security::ScannedResources do ...@@ -32,4 +32,61 @@ RSpec.describe Gitlab::Ci::Parsers::Security::ScannedResources do
it { is_expected.to be(0) } it { is_expected.to be(0) }
end end
end end
describe '#scanned_resources_for_csv' do
subject { parser.scanned_resources_for_csv(scanned_resources) }
context 'when there are scanned resources' do
let(:scanned_resources) do
[
{ "method" => "GET", "type" => "url", "url" => "http://railsgoat:3001" },
{ "method" => "GET", "type" => "url", "url" => "http://railsgoat:3001/" },
{ "method" => "GET", "type" => "url", "url" => "http://railsgoat:3001/login?foo=bar" },
{ "method" => "POST", "type" => "url", "url" => "http://railsgoat:3001/users" }
]
end
it 'converts the hash to OpenStructs', :aggregate_failures do
expect(subject.length).to eq(4)
resource = subject[2]
expect(resource.request_method).to eq('GET')
expect(resource.scheme).to eq('http')
expect(resource.host).to eq('railsgoat')
expect(resource.port).to eq(3001)
expect(resource.path).to eq('/login')
expect(resource.query_string).to eq('foo=bar')
end
end
context 'when there is an invalid URL' do
let(:scanned_resources) do
[
{ "method" => "GET", "type" => "url", "url" => "http://railsgoat:3001" },
{ "method" => "GET", "type" => "url", "url" => "notaURL" },
{ "method" => "GET", "type" => "url", "url" => "" },
{ "method" => "GET", "type" => "url", "url" => nil },
{ "method" => "GET", "type" => "url", "url" => "http://railsgoat:3001/login?foo=bar" }
]
end
it 'returns a blank object with full URL string', :aggregate_failures do
expect(subject.length).to eq(5)
invalid_url = subject[1]
expect(invalid_url.request_method).to eq('GET')
expect(invalid_url.scheme).to be_nil
expect(invalid_url.raw_url).to eq('notaURL')
blank_url = subject[2]
expect(blank_url.request_method).to eq('GET')
expect(blank_url.scheme).to be_nil
expect(blank_url.raw_url).to eq('')
nil_url = subject[3]
expect(nil_url.request_method).to eq('GET')
expect(nil_url.scheme).to be_nil
expect(nil_url.raw_url).to be_nil
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Security::ScannedResourcesController, type: :request do
describe 'GET #index' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
let_it_be(:pipeline_without_scan) { create(:ci_pipeline, project: project) }
let_it_be(:pipeline_on_another_project) { create(:ci_pipeline) }
let_it_be(:pipeline_id) { pipeline.id }
let(:parsed_csv_data) { CSV.parse(response.body, headers: true) }
subject { get project_security_scanned_resources_path(project, :csv, pipeline_id: pipeline_id) }
before do
stub_licensed_features(dast: true, security_dashboard: true)
login_as(user)
end
shared_examples 'returns a 404' do
it 'will return a 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'user has access to view vulnerabilities' do
before do
project.add_developer(user)
end
context 'when DAST security scan is found' do
before do
create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) do |job|
create(:ee_ci_job_artifact, :dast, job: job, project: project)
end
end
it 'returns a CSV representation of the scanned resources' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(parsed_csv_data.length).to eq(6)
expect(parsed_csv_data.first.to_h).to include(
'Method' => 'GET',
'Scheme' => 'http',
'Host' => 'api-server',
'Port' => '80',
'Path' => '/',
'Query String' => nil
)
end
end
context 'when DAST licensed feature is unavailable' do
before do
stub_licensed_features(dast: false)
end
include_examples 'returns a 404'
end
context 'when security_dashboard licensed feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
include_examples 'returns a 404'
end
context 'when DAST security scan is not found' do
let(:pipeline_id) { pipeline_without_scan.id }
include_examples 'returns a 404'
end
context 'when the pipeline id exists under another project' do
let(:pipeline_id) { pipeline_on_another_project.id }
before do
create(:ci_build, :success, name: 'dast_job', pipeline: pipeline_on_another_project, project: pipeline_on_another_project.project) do |job|
create(:ee_ci_job_artifact, :dast, job: job, project: pipeline_on_another_project.project)
end
end
include_examples 'returns a 404'
end
context 'when the pipeline does not exist' do
let(:pipeline_id) { 'not_a_valid_id' }
include_examples 'returns a 404'
end
end
context 'user does not have access to view vulnerabilities' do
before do
project.add_guest(user)
create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) do |job|
create(:ee_ci_job_artifact, :dast, job: job, project: project)
end
end
include_examples 'returns a 404'
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