dradis-csv 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/app/assets/javascripts/dradis/plugins/csv/upload.js +46 -2
- data/app/controllers/dradis/plugins/csv/upload_controller.rb +39 -3
- data/app/jobs/dradis/plugins/csv/mapping_import_job.rb +3 -2
- data/app/views/dradis/plugins/csv/upload/new.html.erb +11 -2
- data/lib/dradis/plugins/csv/field_processor.rb +10 -0
- data/lib/dradis/plugins/csv/gem_version.rb +1 -1
- data/lib/dradis/plugins/csv/importer.rb +78 -17
- data/lib/dradis/plugins/csv/mapping.rb +77 -0
- data/lib/dradis/plugins/csv/mapping_form.rb +99 -0
- data/lib/dradis/plugins/csv.rb +3 -0
- metadata +5 -20
- data/.github/pull_request_template.md +0 -45
- data/.gitignore +0 -5
- data/CHANGELOG.template +0 -12
- data/CONTRIBUTING.md +0 -3
- data/Gemfile +0 -23
- data/dradis-csv.gemspec +0 -25
- data/spec/features/upload_spec.rb +0 -315
- data/spec/fixtures/files/simple (copy).csv +0 -2
- data/spec/fixtures/files/simple.csv +0 -2
- data/spec/fixtures/files/simple_malformed.csv +0 -2
- data/spec/jobs/dradis/plugins/csv/mapping_import_job_spec.rb +0 -30
- data/spec/lib/dradis/plugins/csv/importer_spec.rb +0 -140
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f66806e2230745ce4ab608c4930c575d92e35f577c7a86ff9f888327c313cda9
|
|
4
|
+
data.tar.gz: 2510f5a2d96208375f1a339976dc16c03490cdbabb297728095e178c635f2a20
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a6e489369e1d0a13bb3c60a02b02f1352680a0fe85c39448828891aac46b85a18227d97f62bc7486385dba3345bbb3027edf5fec786485bb54f2956ec5c720e
|
|
7
|
+
data.tar.gz: 90f1df72e003a07b155c3f445905332c63b902d49d753b0b167a33ad6158cfdc5a0408e22565ff36f42868a52f290d97aded1336d4354854d074fd294e45e645
|
data/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,11 @@ window.addEventListener('job-done', function () {
|
|
|
14
14
|
Turbo.visit(redirectPath);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
if ($('body.dradis-plugins-csv-upload.new').length) {
|
|
19
|
+
$('[data-behavior~=mapping-form] input[type="submit"]')
|
|
20
|
+
.val('Done!');
|
|
21
|
+
}
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
document.addEventListener('turbo:load', function() {
|
|
@@ -60,6 +65,16 @@ document.addEventListener('turbo:load', function() {
|
|
|
60
65
|
_setDradisFieldSelect($(this));
|
|
61
66
|
});
|
|
62
67
|
|
|
68
|
+
$('[data-behavior~=dradis-field-select]').on('change', function () {
|
|
69
|
+
console.log('change!');
|
|
70
|
+
_toggleCustomFieldInput($(this));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
$('[data-behavior~=dradis-field-select]').not(':disabled').each(function () {
|
|
74
|
+
console.log('initializing!');
|
|
75
|
+
_toggleCustomFieldInput($(this));
|
|
76
|
+
});
|
|
77
|
+
|
|
63
78
|
$('[data-behavior~=mapping-form]').submit(function () {
|
|
64
79
|
var valid = _validateIdentifierSelected() && _validateNodeSelected();
|
|
65
80
|
|
|
@@ -87,13 +102,21 @@ document.addEventListener('turbo:load', function() {
|
|
|
87
102
|
.attr('disabled', 'disabled')
|
|
88
103
|
.addClass('d-none');
|
|
89
104
|
|
|
105
|
+
$row
|
|
106
|
+
.find('[data-behavior~=custom-destination-input]')
|
|
107
|
+
.prop('disabled', true)
|
|
108
|
+
.addClass('d-none')
|
|
109
|
+
.attr('required', false);
|
|
110
|
+
|
|
111
|
+
var $activeSelect;
|
|
112
|
+
|
|
90
113
|
if ($select.val() == 'issue') {
|
|
91
|
-
$row
|
|
114
|
+
$activeSelect = $row
|
|
92
115
|
.find('[data-behavior~=issue-field-select]')
|
|
93
116
|
.removeAttr('disabled')
|
|
94
117
|
.removeClass('d-none');
|
|
95
118
|
} else if ($select.val() == 'evidence') {
|
|
96
|
-
$row
|
|
119
|
+
$activeSelect = $row
|
|
97
120
|
.find('[data-behavior~=evidence-field-select]')
|
|
98
121
|
.removeAttr('disabled')
|
|
99
122
|
.removeClass('d-none');
|
|
@@ -103,6 +126,27 @@ document.addEventListener('turbo:load', function() {
|
|
|
103
126
|
.attr('disabled', 'disabled')
|
|
104
127
|
.removeClass('d-none');
|
|
105
128
|
}
|
|
129
|
+
|
|
130
|
+
if ($activeSelect) {
|
|
131
|
+
_toggleCustomFieldInput($activeSelect);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _toggleCustomFieldInput($select) {
|
|
136
|
+
var $customInput = $select.next('[data-behavior~=custom-destination-input]');
|
|
137
|
+
|
|
138
|
+
if (!$customInput.length) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var isCustom = $select
|
|
143
|
+
.find('option:selected')
|
|
144
|
+
.is('[data-behavior~=custom-destination-field]');
|
|
145
|
+
|
|
146
|
+
$customInput
|
|
147
|
+
.toggleClass('d-none', !isCustom)
|
|
148
|
+
.prop('disabled', !isCustom)
|
|
149
|
+
.attr('required', isCustom);
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
function _validateNodeSelected() {
|
|
@@ -3,21 +3,31 @@ module Dradis::Plugins::CSV
|
|
|
3
3
|
include ProjectScoped
|
|
4
4
|
|
|
5
5
|
before_action :load_attachment, only: [:new, :create]
|
|
6
|
-
before_action :load_rtp_fields, only: [:new]
|
|
6
|
+
before_action :load_rtp_fields, only: [:new, :create]
|
|
7
7
|
before_action :load_csv_headers, only: [:new]
|
|
8
8
|
|
|
9
|
+
# Reached after the standard upload flow has already run the file
|
|
10
|
+
# through Importer#import. If a saved mapping matched, that import
|
|
11
|
+
# already happened (see importer.rb) and there's nothing left to map.
|
|
9
12
|
def new
|
|
10
|
-
|
|
13
|
+
if saved_mapping?
|
|
14
|
+
return redirect_to main_app.project_issues_path(current_project),
|
|
15
|
+
notice: 'CSV imported using its saved mapping.'
|
|
16
|
+
end
|
|
11
17
|
|
|
18
|
+
@default_columns = ['Column Header', 'Entity', 'Dradis Field']
|
|
12
19
|
@log_uid = Log.new.uid
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def create
|
|
23
|
+
save_mapping
|
|
24
|
+
|
|
16
25
|
job_logger.write 'Enqueueing job to start in the background.'
|
|
17
26
|
|
|
18
27
|
MappingImportJob.perform_later(
|
|
19
28
|
default_user_id: current_user.id,
|
|
20
29
|
file: @attachment.fullpath.to_s,
|
|
30
|
+
headers: csv_headers,
|
|
21
31
|
mappings: mappings_params[:field_attributes].to_h,
|
|
22
32
|
project_id: current_project.id,
|
|
23
33
|
state: state,
|
|
@@ -31,6 +41,10 @@ module Dradis::Plugins::CSV
|
|
|
31
41
|
@job_logger ||= Log.new(uid: params[:log_uid].to_i)
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
def csv_headers
|
|
45
|
+
@csv_headers ||= ::CSV.open(@attachment.fullpath, &:readline)
|
|
46
|
+
end
|
|
47
|
+
|
|
34
48
|
def load_attachment
|
|
35
49
|
filename = CGI::escape params[:attachment]
|
|
36
50
|
@attachment = Attachment.find(filename, conditions: { node_id: current_project.plugin_uploads_node.id })
|
|
@@ -62,7 +76,29 @@ module Dradis::Plugins::CSV
|
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
def mappings_params
|
|
65
|
-
params.require(:mappings).permit(field_attributes: [:field, :type])
|
|
79
|
+
params.require(:mappings).permit(field_attributes: [:field, :type, :custom_field])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rtp_destination
|
|
83
|
+
rtp = current_project.report_template_properties
|
|
84
|
+
rtp && rtp.as_mapping_destination
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Persist the submitted column assignments so future uploads of this CSV
|
|
88
|
+
# format can reuse them. Mappings are scoped to a report template, so
|
|
89
|
+
# projects without one keep the upload-time mapper only.
|
|
90
|
+
def save_mapping
|
|
91
|
+
return unless rtp_destination
|
|
92
|
+
|
|
93
|
+
MappingForm.new(
|
|
94
|
+
destination: rtp_destination,
|
|
95
|
+
headers: csv_headers,
|
|
96
|
+
rtp_fields: @rtp_fields
|
|
97
|
+
).save(column_mappings: mappings_params[:field_attributes].to_h)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def saved_mapping?
|
|
101
|
+
rtp_destination && Dradis::Plugins::CSV.mapping_exists?(headers: @headers, destination: rtp_destination)
|
|
66
102
|
end
|
|
67
103
|
|
|
68
104
|
def state
|
|
@@ -13,7 +13,7 @@ module Dradis::Plugins::CSV
|
|
|
13
13
|
# '2' => { 'type' => 'identifier' },
|
|
14
14
|
# '3' => { 'type' => 'evidence', 'field' => 'Port' }
|
|
15
15
|
# }
|
|
16
|
-
def perform(default_user_id:, file:, mappings:, project_id:, state:, uid:)
|
|
16
|
+
def perform(default_user_id:, file:, headers:, mappings:, project_id:, state:, uid:)
|
|
17
17
|
logger = Log.new(uid: uid)
|
|
18
18
|
logger.write { "Job id is #{job_id}." }
|
|
19
19
|
|
|
@@ -25,7 +25,8 @@ module Dradis::Plugins::CSV
|
|
|
25
25
|
state: state
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
logger.write { 'Worker process starting background task.' }
|
|
29
|
+
importer.import_rows(file: file, headers: headers, mappings: mappings)
|
|
29
30
|
|
|
30
31
|
logger.write { 'Worker process completed.' }
|
|
31
32
|
end
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
<div class="alert alert-danger d-none" data-behavior="node-type-validation-message">
|
|
21
21
|
<p>A Node Label must be selected to import evidence records.</p>
|
|
22
22
|
</div>
|
|
23
|
+
|
|
24
|
+
<% if @rtp_fields && (@rtp_fields[:issue].present? || @rtp_fields[:evidence].present?) %>
|
|
25
|
+
<div class="alert alert-info">
|
|
26
|
+
<p class="m-0">This mapping will be saved and applied automatically the next time you upload a CSV file with these same column headers.</p>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
23
29
|
</div>
|
|
24
30
|
|
|
25
31
|
<%= form_with url: project_upload_index_path(current_project, format: :js), method: :post, local: false, data: { behavior: 'mapping-form' } do |f| %>
|
|
@@ -37,6 +43,7 @@
|
|
|
37
43
|
</tr>
|
|
38
44
|
</thead>
|
|
39
45
|
<tbody>
|
|
46
|
+
<% custom_field_option = ['Custom Field', 'Custom Field', { data: { behavior: 'custom-destination-field' } }] %>
|
|
40
47
|
<% @headers.each_with_index do |header, index| %>
|
|
41
48
|
<tr class="issue-type">
|
|
42
49
|
<td><%= header %></td>
|
|
@@ -48,11 +55,13 @@
|
|
|
48
55
|
<td>
|
|
49
56
|
<% if @rtp_fields %>
|
|
50
57
|
<div>
|
|
51
|
-
<% issue_options = @rtp_fields[:issue].any? ? options_for_select(@rtp_fields[:issue]) : options_for_select([[header, header]], disabled: header, selected: header) %>
|
|
58
|
+
<% issue_options = @rtp_fields[:issue].any? ? options_for_select(@rtp_fields[:issue] + [custom_field_option]) : options_for_select([[header, header]], disabled: header, selected: header) %>
|
|
52
59
|
<%= f.select "mappings[field_attributes][#{index}][field]", issue_options, {}, class: 'form-select w-75 field-select', data: { behavior: 'dradis-field-select issue-field-select', header: header, 'combobox-config': 'no-combobox' } %>
|
|
60
|
+
<%= f.text_field "mappings[field_attributes][#{index}][custom_field]", disabled: true, class: 'form-control w-75 mt-1 custom-field-input d-none', style: 'min-width: 0;', placeholder: 'Enter field name', data: { behavior: 'custom-destination-input' } %>
|
|
53
61
|
|
|
54
|
-
<% evidence_options = @rtp_fields[:evidence].any? ? options_for_select(@rtp_fields[:evidence]) : options_for_select([[header, header]], disabled: header, selected: header) %>
|
|
62
|
+
<% evidence_options = @rtp_fields[:evidence].any? ? options_for_select(@rtp_fields[:evidence] + [custom_field_option]) : options_for_select([[header, header]], disabled: header, selected: header) %>
|
|
55
63
|
<%= f.select "mappings[field_attributes][#{index}][field]", evidence_options, {}, disabled: true, class: 'form-select w-75 field-select d-none', data: { behavior: 'dradis-field-select evidence-field-select', header: header, 'combobox-config': 'no-combobox' } %>
|
|
64
|
+
<%= f.text_field "mappings[field_attributes][#{index}][custom_field]", disabled: true, class: 'form-control w-75 mt-1 custom-field-input d-none', style: 'min-width: 0;', placeholder: 'Enter field name', data: { behavior: 'custom-destination-input' } %>
|
|
56
65
|
|
|
57
66
|
<%= f.select "mappings[field_attributes][#{index}][field]", [['N/A', '']], {}, disabled: true, class: 'form-select w-75 field-select d-none', data: { behavior: 'dradis-field-select empty-field-select', header: header, 'combobox-config': 'no-combobox' } %>
|
|
58
67
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Dradis::Plugins::CSV
|
|
2
|
+
class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
|
|
3
|
+
# data is a Hash of the current row, keyed by normalized header (see
|
|
4
|
+
# Importer#normalized_row), matching how MappingForm normalizes
|
|
5
|
+
# headers when it stores each field's source_field/content.
|
|
6
|
+
def value(args = {})
|
|
7
|
+
data[args[:field]]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
module Dradis::Plugins::CSV
|
|
2
2
|
class Importer < Dradis::Plugins::Upload::Importer
|
|
3
|
+
# Sources are dynamic (one per mapped CSV format), so report the
|
|
4
|
+
# currently known sources grouped by entity for the Mappings Manager.
|
|
3
5
|
def self.templates
|
|
4
|
-
|
|
6
|
+
sources = Dradis::Plugins::CSV.mapping_sources.map(&:to_s)
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
evidence: sources.grep(/\Aevidence_/),
|
|
10
|
+
issue: sources.grep(/\Aissue_/)
|
|
11
|
+
}
|
|
5
12
|
end
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
# Runs as part of the standard upload flow, before the user would reach
|
|
15
|
+
# the column mapper. If this project already has a saved mapping for the
|
|
16
|
+
# file's headers, import immediately using it, so a recognized format
|
|
17
|
+
# never needs re-mapping and gets the same treatment (Rules Engine
|
|
18
|
+
# included) as any other plugin's upload. Unrecognized formats no-op
|
|
19
|
+
# here; UploadController sends the user to the mapper instead.
|
|
20
|
+
def import(params = {})
|
|
21
|
+
return false if mapping_service.destination.blank?
|
|
22
|
+
|
|
23
|
+
headers = CSV.open(params[:file], &:readline)
|
|
9
24
|
|
|
10
|
-
|
|
25
|
+
unless Dradis::Plugins::CSV.mapping_exists?(headers: headers, destination: mapping_service.destination)
|
|
26
|
+
logger.info { 'No saved mapping found for this CSV format.' }
|
|
27
|
+
return false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
import_rows(file: params[:file], headers: headers)
|
|
11
31
|
end
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
# Entry point for both the column mapper form submission (see
|
|
34
|
+
# MappingImportJob) and the auto-recognized import above. mappings is the
|
|
35
|
+
# raw form submission and is only consulted as a fallback, for whichever
|
|
36
|
+
# of identifier/node/entity fields has no saved Mapping to read from
|
|
37
|
+
# instead (e.g. no RTP is set, so MappingForm never persisted one).
|
|
38
|
+
def import_rows(file:, headers:, mappings: nil)
|
|
39
|
+
filename = File.basename(file)
|
|
40
|
+
@issue_lookup = {}
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
@issue_source, @issue_fields, id_index = mapped_fields(entity: :issue, headers: headers)
|
|
43
|
+
@evidence_source, @evidence_fields, node_index = mapped_fields(entity: :evidence, headers: headers)
|
|
17
44
|
|
|
18
|
-
|
|
19
|
-
id_index
|
|
45
|
+
mappings_groups = (mappings || {}).group_by { |index, mapping| mapping['type'] }
|
|
46
|
+
id_index ||= Integer(mappings_groups['identifier']&.first&.first, exception: false)
|
|
47
|
+
@node_index = node_index || Integer(mappings_groups['node']&.first&.first, exception: false)
|
|
20
48
|
@evidence_mappings = mappings_groups['evidence'] || []
|
|
21
|
-
@issue_lookup = {}
|
|
22
49
|
@issue_mappings = mappings_groups['issue'] || []
|
|
23
|
-
@node_index = Integer(mappings_groups['node']&.first&.first, exception: false)
|
|
24
|
-
|
|
25
50
|
|
|
26
|
-
CSV.foreach(
|
|
51
|
+
CSV.foreach(file, headers: true).with_index do |row, index|
|
|
27
52
|
csv_id = row[id_index] || "#{filename}-#{index}"
|
|
28
53
|
process_issue(csv_id: csv_id, row: row)
|
|
29
54
|
process_node(csv_id: csv_id, row: row)
|
|
@@ -34,9 +59,39 @@ module Dradis::Plugins::CSV
|
|
|
34
59
|
|
|
35
60
|
private
|
|
36
61
|
|
|
37
|
-
attr_accessor :
|
|
62
|
+
attr_accessor :evidence_fields, :evidence_mappings, :evidence_source,
|
|
63
|
+
:issue_fields, :issue_lookup, :issue_mappings, :issue_source, :node_index
|
|
64
|
+
|
|
65
|
+
# The saved Mapping's fields (with their content templates) for this
|
|
66
|
+
# entity, excluding the reserved identifier/node field (that drives
|
|
67
|
+
# csv_id/node_label directly in import_rows, not entity content) but
|
|
68
|
+
# returning its column index instead, so import_rows can read it
|
|
69
|
+
# straight off the Mapping rather than the column mapper form. Returns
|
|
70
|
+
# all nils when there's no RTP or no saved mapping, so build_text falls
|
|
71
|
+
# back to reading the CSV directly instead of applying a template, and
|
|
72
|
+
# import_rows falls back to the form for the reserved index too.
|
|
73
|
+
def mapped_fields(entity:, headers:)
|
|
74
|
+
return [nil, nil, nil] unless mapping_service.destination.present?
|
|
75
|
+
|
|
76
|
+
source = Dradis::Plugins::CSV.mapping_source(headers: headers, entity: entity)
|
|
77
|
+
mapping = Dradis::Plugins::CSV.get_mapping(source: source, destination: mapping_service.destination)
|
|
78
|
+
|
|
79
|
+
return [nil, nil, nil] unless mapping
|
|
80
|
+
|
|
81
|
+
reserved_field = entity == :issue ? Mapping::IDENTIFIER_FIELD : Mapping::NODE_LABEL_FIELD
|
|
82
|
+
reserved, fields = mapping.mapping_fields.partition { |field| field.destination_field == reserved_field }
|
|
83
|
+
reserved = reserved.first
|
|
84
|
+
|
|
85
|
+
reserved_index = reserved && headers.index do |header|
|
|
86
|
+
Dradis::Plugins::CSV.normalize_header(header) == reserved.source_field
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
[source, fields, reserved_index]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_text(mappings:, source:, fields:, row:)
|
|
93
|
+
return apply_template(source: source, fields: fields, row: row) if fields
|
|
38
94
|
|
|
39
|
-
def build_text(mappings:, row:)
|
|
40
95
|
mappings.map do |index, mapping|
|
|
41
96
|
next if project.report_template_properties && mapping['field'].blank?
|
|
42
97
|
|
|
@@ -46,17 +101,23 @@ module Dradis::Plugins::CSV
|
|
|
46
101
|
end.compact.join("\n\n")
|
|
47
102
|
end
|
|
48
103
|
|
|
104
|
+
def apply_template(source:, fields:, row:)
|
|
105
|
+
data = row.to_h.transform_keys { |header| Dradis::Plugins::CSV.normalize_header(header) }
|
|
106
|
+
|
|
107
|
+
mapping_service.apply_mapping(source: source, data: data, mapping_fields: fields)
|
|
108
|
+
end
|
|
109
|
+
|
|
49
110
|
def process_evidence(csv_id:, node:, row:)
|
|
50
111
|
logger.info{ "\t\t => Creating evidence: (node: #{node.label}, plugin_id: #{csv_id})" }
|
|
51
112
|
|
|
52
113
|
issue = issue_lookup[csv_id]
|
|
53
|
-
evidence_content = build_text(mappings:
|
|
114
|
+
evidence_content = build_text(mappings: evidence_mappings, source: evidence_source, fields: evidence_fields, row: row)
|
|
54
115
|
content_service.create_evidence(issue: issue, node: node, content: evidence_content)
|
|
55
116
|
end
|
|
56
117
|
|
|
57
118
|
def process_issue(csv_id:, row:)
|
|
58
119
|
logger.info { "\t => Creating new issue (plugin_id: #{csv_id})" }
|
|
59
|
-
issue_text = build_text(mappings: issue_mappings, row: row)
|
|
120
|
+
issue_text = build_text(mappings: issue_mappings, source: issue_source, fields: issue_fields, row: row)
|
|
60
121
|
issue = content_service.create_issue(text: issue_text, id: csv_id)
|
|
61
122
|
|
|
62
123
|
issue_lookup[csv_id] = issue
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Dradis::Plugins::CSV
|
|
2
|
+
# Unlike other integrations, CSV files don't have a fixed structure, so the
|
|
3
|
+
# list of sources can't be defined upfront. Instead, a new source is
|
|
4
|
+
# registered every time a user maps a new CSV format (i.e. a new set of
|
|
5
|
+
# column headers) through the column mapper (see MappingForm). The class
|
|
6
|
+
# methods below give Dradis::Plugins::Mappings::Base's default,
|
|
7
|
+
# constant-backed implementations (mapping_sources, source_fields,
|
|
8
|
+
# default_mapping) database-backed overrides instead, so there's no
|
|
9
|
+
# DEFAULT_MAPPING/SOURCE_FIELDS constant here for them to fall back to.
|
|
10
|
+
module Mapping
|
|
11
|
+
# Reserved destination fields that carry row metadata instead of entity
|
|
12
|
+
# content. They're stored alongside the content fields but are excluded
|
|
13
|
+
# from the entity text during import.
|
|
14
|
+
IDENTIFIER_FIELD = 'plugin_id'.freeze
|
|
15
|
+
NODE_LABEL_FIELD = 'node_label'.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_mapping(_source)
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Excludes the reserved identifier/node fields from the destination field
|
|
23
|
+
# list: they carry row metadata, not entity content (see Importer#mapped_fields,
|
|
24
|
+
# which excludes them the same way when building the import template).
|
|
25
|
+
def self.field_names(source:, destination: nil, field_type: 'destination')
|
|
26
|
+
super.reject do |field|
|
|
27
|
+
field_type == 'destination' &&
|
|
28
|
+
[Mapping::IDENTIFIER_FIELD, Mapping::NODE_LABEL_FIELD].include?(field)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether a format matching these headers has already been mapped for this
|
|
33
|
+
# destination, i.e. whether either its issue or evidence Mapping (or both)
|
|
34
|
+
# was saved. Shared by Importer#import (to recognize a format on upload)
|
|
35
|
+
# and UploadController (to skip the mapper for a recognized format).
|
|
36
|
+
def self.mapping_exists?(headers:, destination:)
|
|
37
|
+
sources = %i[issue evidence].map { |entity| mapping_source(headers: headers, entity: entity) }
|
|
38
|
+
|
|
39
|
+
::Mapping.exists?(component: component, source: sources, destination: destination)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.mapping_sources
|
|
43
|
+
::Mapping.where(component: component).distinct.pluck(:source).map(&:to_sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The source name for a CSV format: the entity (issue/evidence) the mapping
|
|
47
|
+
# populates, plus its normalized headers joined with '/', so the Mappings
|
|
48
|
+
# Manager shows something recognizable (e.g. issue_severity/title) instead
|
|
49
|
+
# of an opaque digest. Headers are sorted so reordering columns doesn't
|
|
50
|
+
# produce a new source; any '/' inside a header name is replaced so it
|
|
51
|
+
# can't be mistaken for the join separator. The entity prefix also lets
|
|
52
|
+
# Importer.templates group sources by entity with a simple regex.
|
|
53
|
+
def self.mapping_source(headers:, entity:)
|
|
54
|
+
normalized = headers.map { |header| normalize_header(header).gsub('/', '-') }.sort
|
|
55
|
+
|
|
56
|
+
"#{entity}_#{normalized.join('/')}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.normalize_header(header)
|
|
60
|
+
header.to_s.delete(" \t\r\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# We're building a dynamic source here since we can't rely on a fixed set of
|
|
64
|
+
# sources unlike the other integrations.
|
|
65
|
+
def self.sample(source)
|
|
66
|
+
source_fields(source).index_with { |field| field }.to_json
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.source_fields(source)
|
|
70
|
+
::MappingField.
|
|
71
|
+
joins(:mapping).
|
|
72
|
+
where(mappings: { component: component, source: source.to_s }).
|
|
73
|
+
where.not(source_field: 'Custom Text').
|
|
74
|
+
distinct.
|
|
75
|
+
pluck(:source_field)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Dradis::Plugins::CSV
|
|
2
|
+
# Converts the column mapper's form structure into persisted Mapping
|
|
3
|
+
# records, one per entity (issue/evidence), so a CSV format only needs to
|
|
4
|
+
# be mapped once: #save is the forward direction (form -> records), run
|
|
5
|
+
# the first time a format is mapped. Later uploads of a recognized format
|
|
6
|
+
# read straight from those records instead (see Importer#mapped_fields),
|
|
7
|
+
# rather than reconstructing the form.
|
|
8
|
+
class MappingForm
|
|
9
|
+
# Mapper column types that produce fields for each entity: identifier
|
|
10
|
+
# columns are stored with the issue mapping, node columns with evidence.
|
|
11
|
+
ENTITY_TYPES = {
|
|
12
|
+
issue: %w[issue identifier],
|
|
13
|
+
evidence: %w[evidence node]
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :column_mappings, :destination, :headers, :rtp_fields
|
|
17
|
+
|
|
18
|
+
# rtp_fields is { issue: [...field names...], evidence: [...] }: the
|
|
19
|
+
# destination fields actually defined on the RTP, used to gate #save (see
|
|
20
|
+
# below).
|
|
21
|
+
def initialize(destination:, headers:, rtp_fields:)
|
|
22
|
+
@destination = destination
|
|
23
|
+
@headers = headers
|
|
24
|
+
@rtp_fields = rtp_fields
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Persists one Mapping per entity that has assigned columns, so a
|
|
28
|
+
# newly-mapped CSV format is recognized automatically on future uploads
|
|
29
|
+
# (see Importer#import). Only ever called for formats with no existing
|
|
30
|
+
# mapping: Importer#import and UploadController#new short-circuit before
|
|
31
|
+
# the mapper is reached once a format already has a saved mapping.
|
|
32
|
+
#
|
|
33
|
+
# column_mappings is the mapper form submission, keyed by column index:
|
|
34
|
+
# {
|
|
35
|
+
# '0' => { 'type' => 'node' },
|
|
36
|
+
# '1' => { 'type' => 'issue', 'field' => 'Title' },
|
|
37
|
+
# '2' => { 'type' => 'identifier' },
|
|
38
|
+
# '3' => { 'type' => 'evidence', 'field' => 'Port' }
|
|
39
|
+
# }
|
|
40
|
+
#
|
|
41
|
+
# An entity with no RTP fields defined has nowhere valid to point a
|
|
42
|
+
# mapping, so it's skipped entirely (including its identifier/node
|
|
43
|
+
# column, if any) rather than saving a mapping with a bogus destination.
|
|
44
|
+
def save(column_mappings:)
|
|
45
|
+
@column_mappings = column_mappings
|
|
46
|
+
|
|
47
|
+
return if destination.blank?
|
|
48
|
+
|
|
49
|
+
::Mapping.transaction do
|
|
50
|
+
ENTITY_TYPES.keys.each do |entity|
|
|
51
|
+
next if rtp_fields[entity].blank?
|
|
52
|
+
|
|
53
|
+
fields = fields_for(entity)
|
|
54
|
+
next if fields.empty?
|
|
55
|
+
|
|
56
|
+
source = Dradis::Plugins::CSV.mapping_source(headers: headers, entity: entity)
|
|
57
|
+
mapping = ::Mapping.create!(component: component, source: source, destination: destination)
|
|
58
|
+
mapping.mapping_fields.create!(fields)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def component
|
|
66
|
+
Dradis::Plugins::CSV.component
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fields_for(entity)
|
|
70
|
+
types = ENTITY_TYPES.fetch(entity)
|
|
71
|
+
|
|
72
|
+
column_mappings.filter_map do |index, assignment|
|
|
73
|
+
next unless types.include?(assignment['type'])
|
|
74
|
+
|
|
75
|
+
header = Dradis::Plugins::CSV.normalize_header(headers[index.to_i])
|
|
76
|
+
next if header.blank?
|
|
77
|
+
|
|
78
|
+
destination_field =
|
|
79
|
+
case assignment['type']
|
|
80
|
+
when 'identifier'
|
|
81
|
+
Mapping::IDENTIFIER_FIELD
|
|
82
|
+
when 'node'
|
|
83
|
+
Mapping::NODE_LABEL_FIELD
|
|
84
|
+
else
|
|
85
|
+
field = assignment['field'] == 'Custom Field' ? assignment['custom_field'] : assignment['field']
|
|
86
|
+
next if field.blank?
|
|
87
|
+
|
|
88
|
+
field
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
source_field: header,
|
|
93
|
+
content: "{{ csv[#{header}] }}",
|
|
94
|
+
destination_field: destination_field
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/dradis/plugins/csv.rb
CHANGED
|
@@ -7,5 +7,8 @@ module Dradis
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
require 'dradis/plugins/csv/engine'
|
|
10
|
+
require 'dradis/plugins/csv/field_processor'
|
|
10
11
|
require 'dradis/plugins/csv/importer'
|
|
12
|
+
require 'dradis/plugins/csv/mapping'
|
|
13
|
+
require 'dradis/plugins/csv/mapping_form'
|
|
11
14
|
require 'dradis/plugins/csv/version'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dradis-csv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Martin
|
|
@@ -56,12 +56,7 @@ executables: []
|
|
|
56
56
|
extensions: []
|
|
57
57
|
extra_rdoc_files: []
|
|
58
58
|
files:
|
|
59
|
-
- ".github/pull_request_template.md"
|
|
60
|
-
- ".gitignore"
|
|
61
59
|
- CHANGELOG.md
|
|
62
|
-
- CHANGELOG.template
|
|
63
|
-
- CONTRIBUTING.md
|
|
64
|
-
- Gemfile
|
|
65
60
|
- LICENSE
|
|
66
61
|
- README.md
|
|
67
62
|
- Rakefile
|
|
@@ -75,19 +70,15 @@ files:
|
|
|
75
70
|
- app/views/dradis/plugins/csv/upload/new.html.erb
|
|
76
71
|
- config/initializers/inflections.rb
|
|
77
72
|
- config/routes.rb
|
|
78
|
-
- dradis-csv.gemspec
|
|
79
73
|
- lib/dradis-csv.rb
|
|
80
74
|
- lib/dradis/plugins/csv.rb
|
|
81
75
|
- lib/dradis/plugins/csv/engine.rb
|
|
76
|
+
- lib/dradis/plugins/csv/field_processor.rb
|
|
82
77
|
- lib/dradis/plugins/csv/gem_version.rb
|
|
83
78
|
- lib/dradis/plugins/csv/importer.rb
|
|
79
|
+
- lib/dradis/plugins/csv/mapping.rb
|
|
80
|
+
- lib/dradis/plugins/csv/mapping_form.rb
|
|
84
81
|
- lib/dradis/plugins/csv/version.rb
|
|
85
|
-
- spec/features/upload_spec.rb
|
|
86
|
-
- spec/fixtures/files/simple (copy).csv
|
|
87
|
-
- spec/fixtures/files/simple.csv
|
|
88
|
-
- spec/fixtures/files/simple_malformed.csv
|
|
89
|
-
- spec/jobs/dradis/plugins/csv/mapping_import_job_spec.rb
|
|
90
|
-
- spec/lib/dradis/plugins/csv/importer_spec.rb
|
|
91
82
|
homepage: http://dradis.com
|
|
92
83
|
licenses:
|
|
93
84
|
- GPL-2
|
|
@@ -109,10 +100,4 @@ requirements: []
|
|
|
109
100
|
rubygems_version: 3.6.9
|
|
110
101
|
specification_version: 4
|
|
111
102
|
summary: CSV add-on for the Dradis Framework.
|
|
112
|
-
test_files:
|
|
113
|
-
- spec/features/upload_spec.rb
|
|
114
|
-
- spec/fixtures/files/simple (copy).csv
|
|
115
|
-
- spec/fixtures/files/simple.csv
|
|
116
|
-
- spec/fixtures/files/simple_malformed.csv
|
|
117
|
-
- spec/jobs/dradis/plugins/csv/mapping_import_job_spec.rb
|
|
118
|
-
- spec/lib/dradis/plugins/csv/importer_spec.rb
|
|
103
|
+
test_files: []
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
Please review [CONTRIBUTING.md](https://github.com/dradis/dradis-ce/blob/develop/CONTRIBUTING.md) and remove this line.
|
|
2
|
-
|
|
3
|
-
### Summary
|
|
4
|
-
|
|
5
|
-
Provide a general description of the code changes in your pull
|
|
6
|
-
request... were there any bugs you had fixed? If so, mention them. If
|
|
7
|
-
these bugs have open GitHub issues, be sure to tag them here as well,
|
|
8
|
-
to keep the conversation linked together.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Testing Steps
|
|
12
|
-
|
|
13
|
-
Provide steps to test functionality, described in detail for someone not familiar with this part of the application / code base
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
### Other Information
|
|
17
|
-
|
|
18
|
-
If there's anything else that's important and relevant to your pull
|
|
19
|
-
request, mention that information here. This could include
|
|
20
|
-
benchmarks, or other information.
|
|
21
|
-
|
|
22
|
-
Thanks for contributing to Dradis!
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Copyright assignment
|
|
26
|
-
|
|
27
|
-
Collaboration is difficult with commercial closed source but we want
|
|
28
|
-
to keep as much of the OSS ethos as possible available to users
|
|
29
|
-
who want to fix it themselves.
|
|
30
|
-
|
|
31
|
-
In order to unambiguously own and sell Dradis Framework commercial
|
|
32
|
-
products, we must have the copyright associated with the entire
|
|
33
|
-
codebase. Any code you create which is merged must be owned by us.
|
|
34
|
-
That's not us trying to be a jerks, that's just the way it works.
|
|
35
|
-
|
|
36
|
-
You can delete this section, but the following sentence needs to
|
|
37
|
-
remain in the PR's description:
|
|
38
|
-
|
|
39
|
-
> I assign all rights, including copyright, to any future Dradis
|
|
40
|
-
> work by myself to Security Roots.
|
|
41
|
-
|
|
42
|
-
### Check List
|
|
43
|
-
|
|
44
|
-
- [ ] Added a CHANGELOG entry
|
|
45
|
-
- [ ] Added specs
|
data/CHANGELOG.template
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
[v#.#.#] ([month] [YYYY])
|
|
2
|
-
- [future tense verb] [feature]
|
|
3
|
-
- Upgraded gems:
|
|
4
|
-
- [gem]
|
|
5
|
-
- Bugs fixes:
|
|
6
|
-
- [future tense verb] [bug fix]
|
|
7
|
-
- Bug tracker items:
|
|
8
|
-
- [item]
|
|
9
|
-
- Security Fixes:
|
|
10
|
-
- High: (Authenticated|Unauthenticated) (admin|author|contributor) [vulnerability description]
|
|
11
|
-
- Medium: (Authenticated|Unauthenticated) (admin|author|contributor) [vulnerability description]
|
|
12
|
-
- Low: (Authenticated|Unauthenticated) (admin|author|contributor) [vulnerability description]
|
data/CONTRIBUTING.md
DELETED
data/Gemfile
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
source 'https://rubygems.org'
|
|
2
|
-
|
|
3
|
-
# Declare your gem's dependencies in dradispro-duoweb.gemspec.
|
|
4
|
-
# Bundler will treat runtime dependencies like base dependencies, and
|
|
5
|
-
# development dependencies will be added by default to the :development group.
|
|
6
|
-
gemspec
|
|
7
|
-
|
|
8
|
-
# jquery-rails is used by the dummy application
|
|
9
|
-
# gem "jquery-rails"
|
|
10
|
-
|
|
11
|
-
# Declare any dependencies that are still in development here instead of in
|
|
12
|
-
# your gemspec. These might include edge Rails or gems from your path or
|
|
13
|
-
# Git. Remember to move these dependencies to your gemspec before releasing
|
|
14
|
-
# your gem to rubygems.org.
|
|
15
|
-
|
|
16
|
-
# To use debugger
|
|
17
|
-
# gem 'debugger'
|
|
18
|
-
|
|
19
|
-
if Dir.exist?('../dradis-plugins')
|
|
20
|
-
gem 'dradis-plugins', path: '../dradis-plugins'
|
|
21
|
-
else
|
|
22
|
-
gem 'dradis-plugins', github: 'dradis/dradis-plugins'
|
|
23
|
-
end
|
data/dradis-csv.gemspec
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
$:.push File.expand_path('../lib', __FILE__)
|
|
2
|
-
require 'dradis/plugins/csv/version'
|
|
3
|
-
version = Dradis::Plugins::CSV::VERSION::STRING
|
|
4
|
-
|
|
5
|
-
# Describe your gem and declare its dependencies:
|
|
6
|
-
Gem::Specification.new do |spec|
|
|
7
|
-
spec.platform = Gem::Platform::RUBY
|
|
8
|
-
spec.name = 'dradis-csv'
|
|
9
|
-
spec.version = version
|
|
10
|
-
spec.summary = 'CSV add-on for the Dradis Framework.'
|
|
11
|
-
spec.description = 'This add-on allows you to upload and parse CSV output into Dradis.'
|
|
12
|
-
|
|
13
|
-
spec.license = 'GPL-2'
|
|
14
|
-
|
|
15
|
-
spec.authors = ['Daniel Martin']
|
|
16
|
-
spec.homepage = 'http://dradis.com'
|
|
17
|
-
|
|
18
|
-
spec.files = `git ls-files`.split("\n")
|
|
19
|
-
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
20
|
-
spec.test_files = spec.files.grep(%r{^(spec|features)/})
|
|
21
|
-
|
|
22
|
-
spec.add_dependency 'dradis-plugins', '>= 4.0'
|
|
23
|
-
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
24
|
-
spec.add_development_dependency 'rake'
|
|
25
|
-
end
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
require 'rails_helper'
|
|
2
|
-
|
|
3
|
-
# To run, execute from Dradis main app folder:
|
|
4
|
-
# bin/rspec [dradis-plugins path]/spec/features/upload_spec.rb
|
|
5
|
-
|
|
6
|
-
describe 'upload feature', js: true do
|
|
7
|
-
before do
|
|
8
|
-
login_to_project_as_user
|
|
9
|
-
visit project_upload_path(@project)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
context 'uploading a CSV file' do
|
|
13
|
-
let(:file_path) { File.expand_path('../fixtures/files/simple.csv', __dir__) }
|
|
14
|
-
before do
|
|
15
|
-
@headers = CSV.open(file_path, &:readline)
|
|
16
|
-
|
|
17
|
-
find('#state + .combobox').click
|
|
18
|
-
find('#state ~ .combobox-menu .combobox-option', text: 'Published').click
|
|
19
|
-
|
|
20
|
-
find('#uploader + .combobox').click
|
|
21
|
-
find('#uploader ~ .combobox-menu .combobox-option', text: 'Dradis::Plugins::CSV').click
|
|
22
|
-
|
|
23
|
-
attach_file 'file', file_path, visible: false, disabled: false
|
|
24
|
-
|
|
25
|
-
expect(page).to have_text('CSV Upload Mapping', wait: 30)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it 'redirects to the mapping page' do
|
|
29
|
-
expect(current_path).to eq(csv.new_project_upload_path(@project))
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it 'lists the fields in the table' do
|
|
33
|
-
within('tbody') do
|
|
34
|
-
@headers.each do |header|
|
|
35
|
-
expect(page).to have_selector('td', text: header)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
context 'mapping CSV columns' do
|
|
41
|
-
context 'when identifier not selected' do
|
|
42
|
-
it 'shows a validation message on the page' do
|
|
43
|
-
within all('tbody tr')[3] do
|
|
44
|
-
select 'Evidence Field'
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
click_button 'Import CSV'
|
|
48
|
-
expect(page).to have_text('An Issue ID must be selected.')
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
context 'when there are evidence type but no node type selected' do
|
|
53
|
-
it 'shows a validation message on the page' do
|
|
54
|
-
within all('tbody tr')[2] do
|
|
55
|
-
select 'Issue ID'
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
within all('tbody tr')[3] do
|
|
59
|
-
select 'Evidence Field'
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
click_button 'Import CSV'
|
|
63
|
-
expect(page).to have_text('A Node Label must be selected to import evidence records.')
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
context 'valid states' do
|
|
68
|
-
it 'imports the issues based on the selected state' do
|
|
69
|
-
select 'Issue ID', from: 'mappings[field_attributes][0][type]'
|
|
70
|
-
select 'Node', from: 'mappings[field_attributes][3][type]'
|
|
71
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
72
|
-
select 'Evidence Field', from: 'mappings[field_attributes][5][type]'
|
|
73
|
-
|
|
74
|
-
perform_enqueued_jobs do
|
|
75
|
-
click_button 'Import CSV'
|
|
76
|
-
|
|
77
|
-
find('#console .log', wait: 30, match: :first)
|
|
78
|
-
|
|
79
|
-
expect(page).to have_text('Worker process completed.')
|
|
80
|
-
|
|
81
|
-
expect(Issue.published.count).to eq(1)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
context 'invalid states' do
|
|
87
|
-
it 'imports the issues as draft' do
|
|
88
|
-
select 'Issue ID', from: 'mappings[field_attributes][0][type]'
|
|
89
|
-
select 'Node', from: 'mappings[field_attributes][3][type]'
|
|
90
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
91
|
-
select 'Evidence Field', from: 'mappings[field_attributes][5][type]'
|
|
92
|
-
|
|
93
|
-
page.execute_script(<<~JS)
|
|
94
|
-
const select = document.querySelector('#state');
|
|
95
|
-
select.value = 'tampered_value';
|
|
96
|
-
JS
|
|
97
|
-
|
|
98
|
-
perform_enqueued_jobs do
|
|
99
|
-
click_button 'Import CSV'
|
|
100
|
-
|
|
101
|
-
find('#console .log', wait: 30, match: :first)
|
|
102
|
-
|
|
103
|
-
expect(page).to have_text('Worker process completed.')
|
|
104
|
-
|
|
105
|
-
expect(Issue.published.count).to eq(0)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
context 'when project does not have RTP' do
|
|
111
|
-
it 'imports all columns as fields' do
|
|
112
|
-
select 'Issue ID', from: 'mappings[field_attributes][0][type]'
|
|
113
|
-
select 'Node', from: 'mappings[field_attributes][3][type]'
|
|
114
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
115
|
-
select 'Evidence Field', from: 'mappings[field_attributes][5][type]'
|
|
116
|
-
|
|
117
|
-
perform_enqueued_jobs do
|
|
118
|
-
click_button 'Import CSV'
|
|
119
|
-
|
|
120
|
-
find('#console .log', wait: 30, match: :first)
|
|
121
|
-
|
|
122
|
-
expect(page).to have_text('Worker process completed.')
|
|
123
|
-
|
|
124
|
-
issue = Issue.last
|
|
125
|
-
expect(issue.fields).to eq({ 'Description' => 'Test CSV', 'Title' => 'SQL Injection', 'VulnerabilityCategory' =>'High', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
126
|
-
|
|
127
|
-
node = issue.affected.first
|
|
128
|
-
expect(node.label).to eq('10.0.0.1')
|
|
129
|
-
|
|
130
|
-
evidence = node.evidence.first
|
|
131
|
-
expect(evidence.fields).to eq({ 'Label' => '10.0.0.1', 'Title' => 'SQL Injection', 'Location' => '10.0.0.1', 'Port' => '443' })
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
context 'when project have RTP' do
|
|
137
|
-
before do
|
|
138
|
-
rtp = create(:report_template_properties, evidence_fields: evidence_fields, issue_fields: issue_fields)
|
|
139
|
-
@project.update(report_template_properties: rtp)
|
|
140
|
-
|
|
141
|
-
page.refresh
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
context 'without fields' do
|
|
145
|
-
let (:evidence_fields) { [] }
|
|
146
|
-
let (:issue_fields) { [] }
|
|
147
|
-
|
|
148
|
-
it 'creates records with fields from the headers' do
|
|
149
|
-
select 'Issue ID', from: 'mappings[field_attributes][0][type]'
|
|
150
|
-
select 'Node', from: 'mappings[field_attributes][3][type]'
|
|
151
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
152
|
-
select 'Evidence Field', from: 'mappings[field_attributes][5][type]'
|
|
153
|
-
|
|
154
|
-
perform_enqueued_jobs do
|
|
155
|
-
click_button 'Import CSV'
|
|
156
|
-
|
|
157
|
-
find('#console .log', wait: 30, match: :first)
|
|
158
|
-
|
|
159
|
-
expect(page).to have_text('Worker process completed.')
|
|
160
|
-
|
|
161
|
-
issue = Issue.last
|
|
162
|
-
expect(issue.fields).to eq({ 'Description' => 'Test CSV', 'Title' => 'SQL Injection', 'Vulnerability Category' =>'High', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
163
|
-
|
|
164
|
-
node = issue.affected.first
|
|
165
|
-
expect(node.label).to eq('10.0.0.1')
|
|
166
|
-
|
|
167
|
-
evidence = node.evidence.first
|
|
168
|
-
expect(evidence.fields).to eq({ 'Label' => '10.0.0.1', 'Location' => '10.0.0.1', 'Title' => 'SQL Injection', 'Port' => '443' })
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
context 'with fields' do
|
|
174
|
-
let (:evidence_fields) {
|
|
175
|
-
[
|
|
176
|
-
{ name: 'Location', type: :string, default: true },
|
|
177
|
-
{ name: 'Port', type: :string, default: true}
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let (:issue_fields) {
|
|
182
|
-
[
|
|
183
|
-
{ name: 'Title', type: :string, default: true },
|
|
184
|
-
{ name: 'Description', type: :string, default: true},
|
|
185
|
-
{ name: 'Severity', type: :string, default: true}
|
|
186
|
-
]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
it 'shows the available fields for the selected type' do
|
|
190
|
-
select 'Issue Field', from: 'mappings[field_attributes][1][type]'
|
|
191
|
-
|
|
192
|
-
issue_fields.each do |field|
|
|
193
|
-
expect(page).to have_selector('option', text: field[:name])
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
197
|
-
|
|
198
|
-
evidence_fields.each do |field|
|
|
199
|
-
expect(page).to have_selector('option', text: field[:name])
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it 'can select which columns to import' do
|
|
204
|
-
select 'Issue ID', from: 'mappings[field_attributes][0][type]'
|
|
205
|
-
|
|
206
|
-
select 'Issue Field', from: 'mappings[field_attributes][1][type]'
|
|
207
|
-
select 'Title', from: 'mappings[field_attributes][1][field]'
|
|
208
|
-
|
|
209
|
-
select 'Issue Field', from: 'mappings[field_attributes][2][type]'
|
|
210
|
-
select 'Description', from: 'mappings[field_attributes][2][field]'
|
|
211
|
-
|
|
212
|
-
select 'Node', from: 'mappings[field_attributes][3][type]'
|
|
213
|
-
|
|
214
|
-
select 'Evidence Field', from: 'mappings[field_attributes][4][type]'
|
|
215
|
-
select 'Location', from: 'mappings[field_attributes][4][field]'
|
|
216
|
-
|
|
217
|
-
select 'Evidence Field', from: 'mappings[field_attributes][5][type]'
|
|
218
|
-
select 'Port', from: 'mappings[field_attributes][5][field]'
|
|
219
|
-
|
|
220
|
-
select 'Issue Field', from: 'mappings[field_attributes][6][type]'
|
|
221
|
-
select 'Severity', from: 'mappings[field_attributes][6][field]'
|
|
222
|
-
|
|
223
|
-
perform_enqueued_jobs do
|
|
224
|
-
click_button 'Import CSV'
|
|
225
|
-
|
|
226
|
-
find('#console .log', wait: 30, match: :first)
|
|
227
|
-
|
|
228
|
-
expect(page).to have_text('Worker process completed.')
|
|
229
|
-
|
|
230
|
-
issue = Issue.last
|
|
231
|
-
expect(issue.fields).to eq({ 'Description' => 'Test CSV', 'Title' => 'SQL Injection', 'Severity' => 'High', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
232
|
-
|
|
233
|
-
node = issue.affected.first
|
|
234
|
-
expect(node.label).to eq('10.0.0.1')
|
|
235
|
-
|
|
236
|
-
evidence = node.evidence.first
|
|
237
|
-
expect(evidence.fields).to eq({ 'Label' => '10.0.0.1', 'Location' => '10.0.0.1', 'Title' => 'SQL Injection', 'Port' => '443' })
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
context 'when no evidence fields' do
|
|
243
|
-
let (:evidence_fields) { [] }
|
|
244
|
-
let (:issue_fields) { [] }
|
|
245
|
-
|
|
246
|
-
it 'still creates evidence record' do
|
|
247
|
-
within all('tbody tr')[0] do
|
|
248
|
-
select 'Issue ID'
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
within all('tbody tr')[1] do
|
|
252
|
-
select 'Issue Field'
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
within all('tbody tr')[3] do
|
|
256
|
-
select 'Node'
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
within all('tbody tr')[5] do
|
|
260
|
-
select 'Issue Field'
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
perform_enqueued_jobs do
|
|
264
|
-
click_button 'Import CSV'
|
|
265
|
-
|
|
266
|
-
find('#console .log', wait: 30, match: :first)
|
|
267
|
-
|
|
268
|
-
expect(page).to have_text('Worker process completed.')
|
|
269
|
-
|
|
270
|
-
issue = Issue.last
|
|
271
|
-
expect(issue.fields).to include({ 'Title' => 'SQL Injection', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
272
|
-
|
|
273
|
-
node = issue.affected.first
|
|
274
|
-
expect(node.label).to eq('10.0.0.1')
|
|
275
|
-
|
|
276
|
-
evidence = node.evidence.first
|
|
277
|
-
expect(evidence.content).to eq('')
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
describe 'CSV file samples' do
|
|
286
|
-
before do
|
|
287
|
-
find('#uploader + .combobox').click
|
|
288
|
-
find('#uploader ~ .combobox-menu .combobox-option', text: 'Dradis::Plugins::CSV').click
|
|
289
|
-
|
|
290
|
-
attach_file 'file', file_path, visible: false, disabled: false
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
context 'uploading a malformed CSV file' do
|
|
294
|
-
let(:file_path) { File.expand_path('../fixtures/files/simple_malformed.csv', __dir__) }
|
|
295
|
-
|
|
296
|
-
it 'redirects to upload manager with error' do
|
|
297
|
-
find('.alert.alert-danger', wait: 30)
|
|
298
|
-
|
|
299
|
-
expect(page).to have_text('The uploaded file is not a valid CSV file')
|
|
300
|
-
expect(current_path).to eq(main_app.project_upload_manager_path(@project))
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
context 'uploading any file other than CSV' do
|
|
305
|
-
let(:file_path) { Rails.root.join('spec/fixtures/files/rails.png') }
|
|
306
|
-
|
|
307
|
-
it 'redirects to upload manager with error' do
|
|
308
|
-
find('.alert.alert-danger', wait: 30)
|
|
309
|
-
|
|
310
|
-
expect(page).to have_text('The uploaded file is not a CSV file.')
|
|
311
|
-
expect(current_path).to eq(main_app.project_upload_manager_path(@project))
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
require 'rails_helper'
|
|
2
|
-
|
|
3
|
-
RSpec.describe Dradis::Plugins::CSV::MappingImportJob do
|
|
4
|
-
let(:file) { File.expand_path('../../../.../../../fixtures/files/simple.csv', __dir__) }
|
|
5
|
-
|
|
6
|
-
let(:perform_job) do
|
|
7
|
-
described_class.new.perform(
|
|
8
|
-
default_user_id: create(:user).id,
|
|
9
|
-
file: file,
|
|
10
|
-
mappings: {},
|
|
11
|
-
project_id: create(:project).id,
|
|
12
|
-
uid: 1
|
|
13
|
-
)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
describe '#perform' do
|
|
17
|
-
it 'calls Importer#import_csv' do
|
|
18
|
-
dbl = double('Importer')
|
|
19
|
-
allow(Dradis::Plugins::CSV::Importer).to receive(:new).and_return(dbl)
|
|
20
|
-
expect(dbl).to receive(:import_csv).and_return(true)
|
|
21
|
-
|
|
22
|
-
perform_job
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
it 'writes a known final line in the log' do
|
|
26
|
-
perform_job
|
|
27
|
-
expect(Log.last.text).to eq 'Worker process completed.'
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
require 'rails_helper'
|
|
2
|
-
|
|
3
|
-
RSpec.describe Dradis::Plugins::CSV::Importer do
|
|
4
|
-
let(:file) { File.expand_path('../../../.../../../fixtures/files/simple.csv', __dir__) }
|
|
5
|
-
let(:project) { create(:project) }
|
|
6
|
-
|
|
7
|
-
let(:instance) do
|
|
8
|
-
described_class.new(
|
|
9
|
-
default_user_id: create(:user).id,
|
|
10
|
-
logger: Log.new(uid: 1),
|
|
11
|
-
plugin: Dradis::Plugins::CSV,
|
|
12
|
-
project_id: project.id
|
|
13
|
-
)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
let(:import_csv) do
|
|
17
|
-
instance.import_csv(file: file, mappings: mappings)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
describe '#import_csv' do
|
|
21
|
-
context 'when project has RTP' do
|
|
22
|
-
let(:mappings) do
|
|
23
|
-
{
|
|
24
|
-
'0' => { 'type' => 'identifier' },
|
|
25
|
-
'1' => { 'type' => 'issue', 'field' => 'MyTitle' },
|
|
26
|
-
'3' => { 'type' => 'node', 'field' => '' },
|
|
27
|
-
'4' => { 'type' => 'evidence', 'field' => 'MyLocation' },
|
|
28
|
-
'5' => { 'type' => 'evidence', 'field' => '' }
|
|
29
|
-
}
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
before do
|
|
33
|
-
project.update(report_template_properties: create(:report_template_properties))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it 'uses the field as Dradis Field' do
|
|
37
|
-
import_csv
|
|
38
|
-
|
|
39
|
-
issue = Issue.first
|
|
40
|
-
expect(issue.fields).to eq({ 'MyTitle' => 'SQL Injection', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
41
|
-
|
|
42
|
-
node = issue.affected.first
|
|
43
|
-
expect(node.label).to eq('10.0.0.1')
|
|
44
|
-
|
|
45
|
-
evidence = node.evidence.first
|
|
46
|
-
expect(evidence.fields).to eq({ 'Label' => '10.0.0.1', 'Title' => '(No #[Title]# field)', 'MyLocation' => '10.0.0.1' })
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
context 'when project does not have RTP' do
|
|
51
|
-
let(:mappings) do
|
|
52
|
-
{
|
|
53
|
-
'0' => { 'type' => 'identifier' },
|
|
54
|
-
'1' => { 'type' => 'issue', 'field' => 'MyTitle' },
|
|
55
|
-
'3' => { 'type' => 'node', 'field' => '' },
|
|
56
|
-
'4' => { 'type' => 'evidence', 'field' => 'MyLocation' },
|
|
57
|
-
'5' => { 'type' => 'evidence', 'field' => '' },
|
|
58
|
-
'6' => { 'type' => 'issue', 'field' => '' }
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'uses the column name as Dradis Field' do
|
|
63
|
-
import_csv
|
|
64
|
-
|
|
65
|
-
issue = Issue.first
|
|
66
|
-
expect(issue.fields).to eq({ 'Title' => 'SQL Injection', 'VulnerabilityCategory' => 'High', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
67
|
-
|
|
68
|
-
node = issue.affected.first
|
|
69
|
-
expect(node.label).to eq('10.0.0.1')
|
|
70
|
-
|
|
71
|
-
evidence = node.evidence.first
|
|
72
|
-
expect(evidence.fields).to eq({ 'Label' => '10.0.0.1', 'Location' => '10.0.0.1', 'Port' => '443', 'Title' => 'SQL Injection' })
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
it 'strips out whitespace from column header' do
|
|
76
|
-
import_csv
|
|
77
|
-
|
|
78
|
-
issue = Issue.first
|
|
79
|
-
expect(issue.fields.keys).to include('VulnerabilityCategory')
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
context 'when mapping does not have a node type' do
|
|
84
|
-
let(:mappings) do
|
|
85
|
-
{
|
|
86
|
-
'0' => { 'type' => 'identifier' },
|
|
87
|
-
'1' => { 'type' => 'issue' },
|
|
88
|
-
'4' => { 'type' => 'evidence' }
|
|
89
|
-
}
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
it 'does not create node and evidence' do
|
|
93
|
-
import_csv
|
|
94
|
-
|
|
95
|
-
issue = Issue.last
|
|
96
|
-
expect(issue.affected.length).to eq(0)
|
|
97
|
-
expect(issue.evidence.length).to eq(0)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
context 'when no identifier is passed in' do
|
|
102
|
-
let(:mappings) do
|
|
103
|
-
{
|
|
104
|
-
'1' => { 'type' => 'issue' },
|
|
105
|
-
'4' => { 'type' => 'evidence' }
|
|
106
|
-
}
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
it 'uses filename and row index as csv_id' do
|
|
110
|
-
import_csv
|
|
111
|
-
|
|
112
|
-
issue = Issue.last
|
|
113
|
-
expect(issue.fields).to eq({ 'Title' => 'SQL Injection', 'plugin' => 'csv', 'plugin_id' => 'simple.csv-0' })
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
context 'when no evidence fields' do
|
|
118
|
-
let(:mappings) do
|
|
119
|
-
{
|
|
120
|
-
'0' => { 'type' => 'identifier' },
|
|
121
|
-
'1' => { 'type' => 'issue', 'field' => 'MyTitle' },
|
|
122
|
-
'3' => { 'type' => 'node', 'field' => '' }
|
|
123
|
-
}
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it 'still creates evidence record' do
|
|
127
|
-
import_csv
|
|
128
|
-
|
|
129
|
-
issue = Issue.first
|
|
130
|
-
expect(issue.fields).to eq({ 'Title' => 'SQL Injection', 'plugin' => 'csv', 'plugin_id' => '1' })
|
|
131
|
-
|
|
132
|
-
node = issue.affected.first
|
|
133
|
-
expect(node.label).to eq('10.0.0.1')
|
|
134
|
-
|
|
135
|
-
evidence = node.evidence.first
|
|
136
|
-
expect(evidence.content).to eq('')
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|