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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8bad2356d2ad2625aa0180faf0ab79f8a8acf40ce59afcdcd7843e1df6eaf358
4
- data.tar.gz: 00bf582bd667765e48311382f06b4f10587cfbe59424802dca5c96a1b9237b8d
3
+ metadata.gz: f66806e2230745ce4ab608c4930c575d92e35f577c7a86ff9f888327c313cda9
4
+ data.tar.gz: 2510f5a2d96208375f1a339976dc16c03490cdbabb297728095e178c635f2a20
5
5
  SHA512:
6
- metadata.gz: 768891ef2bdf14268a94b4607300f8aca8ded2020335b51077efc74d0e5dad2aa36709968a2f7e09119519599c57e34610f4b4034ee0fa69a9462ad547fe9fab
7
- data.tar.gz: 0021f7ee9aa94f498a2c469b4c11f3da43b85476c7bf4018e3970fa2252f93a6d02c5bc60163c81baaf802da67d39b3f3ed6bac69556587e834d19c326bfcd4e
6
+ metadata.gz: 5a6e489369e1d0a13bb3c60a02b02f1352680a0fe85c39448828891aac46b85a18227d97f62bc7486385dba3345bbb3027edf5fec786485bb54f2956ec5c720e
7
+ data.tar.gz: 90f1df72e003a07b155c3f445905332c63b902d49d753b0b167a33ad6158cfdc5a0408e22565ff36f42868a52f290d97aded1336d4354854d074fd294e45e645
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ v5.4.0 (September 2026)
2
+ - No changes
3
+
4
+ v5.3.0 (August 2026)
5
+ - Add support for managing Mappings through the Mappings Manager
6
+ - Add support for saving column assignments from the upload mapper
7
+
1
8
  v5.2.0 (June 2026)
2
9
  - No changes
3
10
 
@@ -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
- @default_columns = ['Column Header', 'Entity', 'Dradis Field']
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
- importer.import_csv(file: file, mappings: mappings)
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
@@ -8,7 +8,7 @@ module Dradis
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 5
11
- MINOR = 2
11
+ MINOR = 4
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -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
- def import(params={})
8
- logger.info { 'Uploading CSV file...' }
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
- logger.info { 'Done' }
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
- def import_csv(params)
14
- logger.info { 'Worker process starting background task.' }
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
- mappings_groups = params[:mappings].group_by { |index, mapping| mapping['type'] }
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
- filename = File.basename(params[:file])
19
- id_index = Integer(mappings_groups['identifier']&.first&.first, exception: false)
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(params[:file], headers: true).with_index do |row, index|
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 :evidence_mappings, :issue_lookup, :issue_mappings, :node_index
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: @evidence_mappings, row: row)
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
@@ -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.2.0
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/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- .DS_Store
2
-
3
- pkg/
4
-
5
- Gemfile.lock
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
@@ -1,3 +0,0 @@
1
- # Plugin contribution guidelines
2
-
3
- See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradisframework/blob/master/CONTRIBUTING.md)
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,2 +0,0 @@
1
- "Id","Title","Description","Host","Location","Port","Vulnerability Category"
2
- "1","SQL Injection","Test CSV","10.0.0.1","10.0.0.1","443","High"
@@ -1,2 +0,0 @@
1
- "Id","Title","Description","Host","Location","Port","Vulnerability Category"
2
- "1","SQL Injection","Test CSV","10.0.0.1","10.0.0.1","443","High"
@@ -1,2 +0,0 @@
1
- "Id";"Title";"Description";"Host";"Location";"Port"
2
- "1";"SQL Injection";"Test CSV";"10.0.0.1";"10.0.0.1";"443"
@@ -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