Commit 5c7226dc authored by Craig Smith's avatar Craig Smith

Display CSV scanned resources for the pipeline

To allow users to view all the URLs scanned by
DAST this commit adds an endpoint to allow them
to download them as a CSV
parent 362fa7b9
# 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
end
end
resources :scanned_resources, only: [:index]
resources :vulnerabilities, only: [:show] do
member do
get :discussions, format: :json
......
......@@ -13,6 +13,21 @@ module Gitlab
scanned_resources_sum
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
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 @@
require 'spec_helper'
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) }
context 'there are scanned resources' do
......@@ -32,4 +32,61 @@ RSpec.describe Gitlab::Ci::Parsers::Security::ScannedResources do
it { is_expected.to be(0) }
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
# 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