diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3b103f47..a043068c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -117,12 +117,12 @@ def destroy end def export - # Using class variable @@components_to_export here to save params[:components_type] value in memory, + # Using class variable @@components_to_export here to save params[:component_ids] value in memory, # because format.html below triggers a redirect to this same action controller - # causing to lose the :components_type param. + # causing to lose the :component_ids param. # rubocop:disable Style/ClassVars - @@components_to_export = params[:components_type] || @@components_to_export + @@components_to_export = params[:component_ids] || @@components_to_export # rubocop:enable Style/ClassVars export_type = params[:type]&.to_sym diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index cca8d895..ed885d0d 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -32,10 +32,11 @@ module ExportHelper # rubocop:todo Metrics/ModuleLength artifact_description: 16 }.freeze - def export_excel(project, components_type, is_disa_export) + def export_excel(project, component_ids, is_disa_export) + components_to_export = project.components.where(id: component_ids.split(',')) # One file for all data types, each data type in a different tab workbook = FastExcel.open(constant_memory: true) - components_to_export = components_type == 'all' ? project.components : project.components.where(released: true) + # components_to_export = components_type == 'all' ? project.components : project.components.where(released: true) components_to_export.eager_load( rules: [:reviews, :disa_rule_descriptions, :rule_descriptions, :checks, :additional_answers, :satisfies, :satisfied_by, { diff --git a/app/javascript/components/project/Project.vue b/app/javascript/components/project/Project.vue index 1c5c4f6c..5b6390bb 100644 --- a/app/javascript/components/project/Project.vue +++ b/app/javascript/components/project/Project.vue @@ -74,10 +74,15 @@ @projectUpdated="refreshProject" /> - + DISA Excel Export - Excel Export + + Excel Export + InSpec Profile Xccdf Export @@ -85,39 +90,44 @@ -

- Would you like to export all components in this project or just the released - components? -

- -
+ + + + - -

- Would you like to export all components in this project or just the released - components? -

@@ -364,9 +374,20 @@ export default { project: this.initialProjectState, visible: this.initialProjectState.visibility === "discoverable", projectTabIndex: 0, + excelExportType: "", + selectedComponentsToExport: [], + allComponentsSelected: false, + releasedComponentsSelected: false, + indeterminate: false, }; }, computed: { + excelExportComponentOptions: function () { + return this.sortedComponents().map((c) => { + const versionRelease = c.version && c.release ? ` - V${c.version}R${c.release}` : ""; + return { text: `${c.name}${versionRelease}`, value: c.id }; + }); + }, sortedAvailableComponents: function () { return _.sortBy(this.project.available_components, ["child_project_name"], ["asc"]); }, @@ -399,6 +420,29 @@ export default { JSON.stringify(this.projectTabIndex) ); }, + selectedComponentsToExport: function (newValue, oldValue) { + // Handle changes in individual component checkboxes + if (newValue.length === 0) { + this.indeterminate = false; + this.allComponentsSelected = false; + this.releasedComponentsSelected = false; + } else if (newValue.length === this.project.components.length) { + this.indeterminate = false; + this.allComponentsSelected = true; + this.releasedComponentsSelected = true; + } else if ( + this.releasedComponents().lenght > 0 && + this.releasedComponents().every((element) => newValue.includes(element)) + ) { + this.indeterminate = true; + this.allComponentsSelected = false; + this.releasedComponentsSelected = true; + } else { + this.indeterminate = true; + this.allComponentsSelected = false; + this.releasedComponentsSelected = false; + } + }, }, mounted: function () { // Persist `currentTab` across page loads @@ -416,6 +460,18 @@ export default { } }, methods: { + toggleComponents: function () { + if (this.allComponentsSelected) { + this.selectedComponentsToExport = this.project.components.map((comp) => comp.id); + } else if (this.releasedComponentsSelected) { + this.selectedComponentsToExport = this.releasedComponents(); + } else { + this.selectedComponentsToExport = []; + } + }, + releasedComponents: function () { + return this.project.components.filter((comp) => comp.released).map((comp) => comp.id); + }, sortedComponents: function () { return _.orderBy( this.project.components, @@ -457,20 +513,23 @@ export default { }) .catch(this.alertOrNotifyResponse); }, - downloadExport: function (type, componentsToExport) { - if (type === "excel") { - this.$refs["excel-export-modal"].hide(); - } else if (type === "disa_excel") { - this.$refs["disa-excel-export-modal"].hide(); - } + downloadExport: function (type) { axios - .get(`/projects/${this.project.id}/export/${type}?components_type=${componentsToExport}`) + .get( + `/projects/${this.project.id}/export/${type}?component_ids=${this.selectedComponentsToExport}` + ) .then((_res) => { // Once it is validated that there is content to download, prompt // the user to save the file window.open(`/projects/${this.project.id}/export/${type}`); }) .catch(this.alertOrNotifyResponse); + + if (type === "excel" || type === "disa_excel") { + this.$refs["excel-export-modal"].hide(); + this.excelExportType = ""; + this.selectedComponentsToExport = []; + } }, }, }; diff --git a/spec/helpers/export_helper_spec.rb b/spec/helpers/export_helper_spec.rb index ea8d0483..521014e5 100644 --- a/spec/helpers/export_helper_spec.rb +++ b/spec/helpers/export_helper_spec.rb @@ -7,34 +7,25 @@ before(:all) do @component = FactoryBot.create(:component) - @released_component = FactoryBot.create(:released_component) @project = @component.project - @project_with_released_comp = @released_component.project + @component_ids = @project.components.pluck(:id).join(',') end describe '#export_excel' do before(:all) do - @workbook = export_excel(@project, 'released', false) - @workbook_release_only = export_excel(@project_with_released_comp, 'released', false) - @workbook_release_all = export_excel(@project_with_released_comp, 'all', false) + @workbook = export_excel(@project, @component_ids, false) + @workbook_disa_export = export_excel(@project, @component_ids, true) - @workbook_disa_export = export_excel(@project, 'released', true) - @workbook_disa_export_release_only = export_excel(@project_with_released_comp, 'released', true) - @workbook_disa_export_release_all = export_excel(@project_with_released_comp, 'all', true) - - [@workbook, @workbook_release_only, @workbook_release_all, - @workbook_disa_export, @workbook_disa_export_release_only, - @workbook_disa_export_release_all].each_with_index do |item, index| + [@workbook, @workbook_disa_export].each_with_index do |item, index| file_name = '' if index == 0 file_name = "./#{@project.name}.xlsx" File.binwrite(file_name, item.read_string) @xlsx = Roo::Spreadsheet.open(file_name) else - file_name = "./#{@project_with_released_comp.name}.xlsx" + file_name = "./#{@project.name}_DISA.xlsx" File.binwrite(file_name, item.read_string) - @xlsx_release_only = Roo::Spreadsheet.open(file_name) if index == 1 - @xlsx_release_all = Roo::Spreadsheet.open(file_name) if index == 2 + @xlsx_disa = Roo::Spreadsheet.open(file_name) end File.delete(file_name) end @@ -44,29 +35,67 @@ it 'creates an excel format of a given project' do expect(@workbook).to be_present expect(@workbook.filename).to end_with 'xlsx' + expect(@workbook_disa_export).to be_present + expect(@workbook_disa_export.filename).to end_with 'xlsx' + end + it 'creates an excel file with the # sheets == # of components that was requested for export' do + expect(@xlsx.sheets.size).to eq @component_ids.split(',').size + expect(@xlsx_disa.sheets.size).to eq @component_ids.split(',').size end end - context 'when project has released component(s) and user requested only released components' do - it 'creates an excel file with the # of sheets == # of released components' do - expect(@xlsx_release_only.sheets.size).to eq @project_with_released_comp.components.where(released: true).size + context 'When a user request a DISA excel export' do + it 'does not include a column for "InSpec Control Body"' do + parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1) + expect(parsed.first).not_to include 'InSpec Control Body' end + it 'returns empty values for "Status Justification", "Mitigation",and "Artifact Description" columns if + the rule status is "Applicable - Configurable"' do + parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1) + status_justifications = parsed.filter_map do |row| + next if row['Status'] != 'Applicable - Configurable' + + row['Status Justification'] + end + mitigations = parsed.filter_map do |row| + next if row['Status'] != 'Applicable - Configurable' - it 'creates an excel file with correct format for worksheet name' do - expect(@xlsx_release_only.sheets).to all(match(/\w+-V[0-9]+R[0-9]+-[0-9]+/)) + row['Mitigation'] + end + artifact_descriptions = parsed.filter_map do |row| + next if row['Status'] != 'Applicable - Configurable' + + row['Artifact Description'] + end + expect(status_justifications).to be_empty + expect(mitigations).to be_empty + expect(artifact_descriptions).to be_empty end - end - context 'When project has released component(s) and user requested to download all components' do - it 'creates an excel file with the # of sheets == total # of components' do - expect(@xlsx_release_all.sheets.size).to eq @project_with_released_comp.components.size + it 'returns emty values for "Mitigation" column if rule (row) status is "Applicable - Inherently Meets"' do + parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1) + mitigations = parsed.filter_map do |row| + next if row['Status'] != 'Applicable - Inherently Meets' + + row['Mitigation'] + end + expect(mitigations).to be_empty end - end - context 'when project has no released component and user requested only released components' do - it 'creates an empty spreadsheet' do - expect(@xlsx.sheets.size).to eq 1 - expect(@xlsx.sheets.first).to eq 'Sheet1' + it 'returns emty values for "Mitigation, Artifact Description" column if rule (row) status is "Not Applicable"' do + parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1) + mitigations = parsed.filter_map do |row| + next if row['Status'] != 'Not Applicable' + + row['Mitigation'] + end + artifact_descriptions = parsed.filter_map do |row| + next if row['Status'] != 'Not Applicable' + + row['Artifact Description'] + end + expect(mitigations).to be_empty + expect(artifact_descriptions).to be_empty end end end