diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index 6dae8e50908f2792ab2718efd2652a42031e4295..578d7c8a18c33fb5990dd3aa991b948c10016880 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -247,6 +247,8 @@ export default { :label="__('Training mode')" label-position="hidden" :disabled="!securityTrainingEnabled" + data-qa-selector="security_training_toggle" + :data-qa-training-provider="provider.name" @change="toggleProvider(provider)" /> <div v-if="$options.TEMP_PROVIDER_LOGOS[provider.name]" class="gl-ml-4"> diff --git a/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue b/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue index d3f11702f20bb33a1b9d42f4be169ce95b52858c..2ea2307619104ccbcc60331428f5205de1ade510 100644 --- a/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue +++ b/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue @@ -187,9 +187,19 @@ export default { role="presentation" ></div> </span> - <span class="gl-font-weight-bold gl-font-base">{{ name }}</span> + <span + class="gl-font-weight-bold gl-font-base" + data-qa-selector="security_training_text" + >{{ name }}</span + > </div> - <gl-link :href="url" target="_blank" @click="clickTrainingLink(name, identifier)"> + <gl-link + :href="url" + target="_blank" + data-qa-selector="security_training_link" + :data-qa-training-name="name" + @click="clickTrainingLink(name, identifier)" + > {{ $options.i18n.viewTraining }} <gl-icon class="gl-ml-2" name="external-link" :size="12" /> </gl-link> diff --git a/qa/qa/ee/fixtures/secure_premade_reports/gl-sast-report.json b/qa/qa/ee/fixtures/secure_premade_reports/gl-sast-report.json index 4ad2e9b1b82bc53437cde182232e03924f4ade7b..d03494eeaf4eae91e1de64519037fecd4f11f386 100644 --- a/qa/qa/ee/fixtures/secure_premade_reports/gl-sast-report.json +++ b/qa/qa/ee/fixtures/secure_premade_reports/gl-sast-report.json @@ -148,6 +148,62 @@ } ] }, + { + "id": "7e126e060d3d0b5f0b11506528c82fa40f5d144d85b2460ab01c44c7c9043be7", + "category": "sast", + "message": "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')", + "description": "Detected possible formatted SQL query. Use parameterized queries instead.\n", + "cve": "semgrep_id:bandit.B608:304:304", + "severity": "Medium", + "scanner": { + "id": "semgrep", + "name": "Semgrep" + }, + "location": { + "file": "django/contrib/gis/db/backends/postgis/operations.py", + "start_line": 304 + }, + "identifiers": [ + { + "type": "semgrep_id", + "name": "bandit.B608", + "value": "bandit.B608", + "url": "https://semgrep.dev/r/gitlab.bandit.B608" + }, + { + "type": "cwe", + "name": "CWE-89", + "value": "89", + "url": "https://cwe.mitre.org/data/definitions/89.html" + }, + { + "type": "owasp", + "name": "A1:2017 - Injection", + "value": "A1:2017" + }, + { + "type": "bandit_test_id", + "name": "Bandit Test ID B608", + "value": "B608" + } + ], + "tracking": { + "type": "source", + "items": [ + { + "file": "django/contrib/gis/db/backends/postgis/operations.py", + "line_start": 304, + "line_end": 304, + "signatures": [ + { + "algorithm": "scope_offset", + "value": "django/contrib/gis/db/backends/postgis/operations.py|PostGISOperations[0]|_get_postgis_func[0]:6" + } + ] + } + ] + } + }, { "id": "47f7fccbb39495c68dac599833fa631a5c043025e8a50fc036f86bafde7kl090", "category": "sast", @@ -171,6 +227,12 @@ "name": "Find Security Bugs-CIPHER_INTEGRITY", "value": "CIPHER_INTEGRITY", "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" + }, + { + "type": "cwe", + "name": "CWE-353", + "value": "353", + "url": "https://cwe.mitre.org/data/definitions/353.html" } ], "priority": "Medium", diff --git a/qa/qa/ee/page/project/secure/security_dashboard.rb b/qa/qa/ee/page/project/secure/security_dashboard.rb index 9475bc954e8c97b97c375ae7b1ea923945d4d88e..45f18e96212c866e9d42e994de1b7a748fbe839f 100644 --- a/qa/qa/ee/page/project/secure/security_dashboard.rb +++ b/qa/qa/ee/page/project/secure/security_dashboard.rb @@ -66,7 +66,7 @@ def export_vulnerabilities_to_csv end def wait_for_vuln_report_to_load - wait_until(max_duration: 10, sleep_interval: 2, message: "Vulnerability report not loaded yet") do + wait_until(max_duration: 20, sleep_interval: 2, message: "Vulnerability report not loaded yet") do has_element?(:vulnerability_report_header) end end diff --git a/qa/qa/ee/page/project/secure/vulnerability_details.rb b/qa/qa/ee/page/project/secure/vulnerability_details.rb index ea00884712a34fdf6d3a9d626e7c6084f9eb19c0..b0dbb174d7e0c7fe80e5da0eb40bc8822b29a205 100644 --- a/qa/qa/ee/page/project/secure/vulnerability_details.rb +++ b/qa/qa/ee/page/project/secure/vulnerability_details.rb @@ -39,6 +39,11 @@ class VulnerabilityDetails < QA::Page::Base element :jira_issue_link end + view 'ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue' do + element :security_training_text + element :security_training_link + end + def has_component?(component_name:) has_element?(component_name.to_sym) end @@ -47,6 +52,15 @@ def has_vulnerability_title?(title:) has_element?(:vulnerability_title, text: title) end + def security_training_present?(training_name:) + has_element?(:security_training_text, text: training_name) + end + + def training_link_present?(training_name:, url:) + element = find_element(:security_training_link, training_name: training_name) + element["href"].include?(url) + end + def has_vulnerability_description?(description:) has_element?(:vulnerability_description, text: description) end @@ -75,6 +89,10 @@ def choose_dismissal_reason find_element(:dismissal_reason_dropdown).find('li', text: reasons.sample).click end + def training_header_present? + has_css?('h3', text: 'Training') + end + def has_vulnerability_status?(status) has_element?(:vulnerability_status_dropdown, text: "#{status.capitalize}") end diff --git a/qa/qa/ee/page/project/secure/vulnerability_security_training.rb b/qa/qa/ee/page/project/secure/vulnerability_security_training.rb new file mode 100644 index 0000000000000000000000000000000000000000..a66933625fc781c1b59d66cd7b8f1a4d821ff6e9 --- /dev/null +++ b/qa/qa/ee/page/project/secure/vulnerability_security_training.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module EE + module Page + module Project + module Secure + class VulnerabilitySecurityTraining < QA::Page::Base + view 'app/assets/javascripts/security_configuration/components/training_provider_list.vue' do + element :security_training_toggle + end + + def toggle_training_provider(name, toggle_to = "on") + within_element(:security_training_toggle, training_provider: name) do + toggle = find('button.gl-toggle') + checked = toggle[:class].include?('is-checked') + toggle.click if (checked && toggle_to == "off") || (!checked && toggle_to == "on") + end + end + end + end + end + end + end +end diff --git a/qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb index 6f40383c0b1f384667ea26edce2da52bdf627c45..2e61aae4c8979dfde729c62023c0cff6e3bcdcac 100644 --- a/qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Govern', :runner, product_group: :threat_insights do describe 'Security Reports in a Merge Request Widget' do - let(:sast_vuln_count) { 6 } + let(:sast_vuln_count) { 7 } let(:dependency_scan_vuln_count) { 4 } let(:container_scan_vuln_count) { 8 } let(:vuln_name) { "Regular Expression Denial of Service in debug" } diff --git a/qa/qa/specs/features/ee/browser_ui/10_govern/vulnerability_security_training_spec.rb b/qa/qa/specs/features/ee/browser_ui/10_govern/vulnerability_security_training_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..be7de24107a572695bd50d9b751eaaab00160562 --- /dev/null +++ b/qa/qa/specs/features/ee/browser_ui/10_govern/vulnerability_security_training_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Govern', :runner, product_group: :threat_insights do + describe 'Vulnerability Report Security Training' do + let(:vuln_name) { "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" } + + let(:training_providers) { ["Kontra", "Secure Code Warrior"] } + let(:secure_code_warrior_url) { "portal.securecodewarrior.com" } + let(:kontra_url) { "application.security/gitlab/free-application-security-training" } + let(:secure_code_warrior_text) { training_providers[1] } + let(:kontra_text) { training_providers[0] } + let!(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'vulnerability-report-security-training' + project.description = 'To Test integration with security training in vulnerability report' + end + end + + let!(:artefacts_directory) do + Pathname.new(File.join(EE::Runtime::Path.fixtures_path, 'secure_premade_reports')) + end + + let!(:runner) do + Resource::ProjectRunner.fabricate_via_api! do |runner| + runner.project = project + runner.name = "runner-for-#{project.name}" + runner.tags = ['secure_report'] + end + end + + let!(:repository) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add report files and .gitlab-ci.yml' + commit.add_directory(artefacts_directory) + end + end + + before do + Flow::Login.sign_in + project.visit! + end + + after do + runner.remove_via_api! + project.remove_via_api! + end + + it 'shows security training section for supported vulnerabilities when the setting is toggled ON', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/411764' do + # Enable two security training providers + toggle_training_providers("on") + + visit_vulnerability(vuln_name) + + QA::EE::Page::Project::Secure::VulnerabilityDetails.perform do |vulnerability_details| + aggregate_failures "testing vulnerability details" do + expect(vulnerability_details.security_training_present?(training_name: secure_code_warrior_text)).to be true + expect(vulnerability_details.training_link_present?(training_name: secure_code_warrior_text, + url: secure_code_warrior_url)).to be true + + expect(vulnerability_details.security_training_present?(training_name: kontra_text)).to be true + expect(vulnerability_details.training_link_present?(training_name: kontra_text, + url: kontra_url)).to be true + end + end + end + + it 'does not show security training section in vulnerability details when the setting is turned OFF', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412398' do + toggle_training_providers("off") + visit_vulnerability(vuln_name) + + expect(QA::EE::Page::Project::Secure::VulnerabilityDetails.perform(&:training_header_present?)).to be false + end + + def toggle_training_providers(toggle_to = "on") + Page::Project::Menu.perform(&:go_to_security_configuration) + click_link("Vulnerability Management") + QA::EE::Page::Project::Secure::VulnerabilitySecurityTraining.perform do |security_training| + training_providers.each do |provider| + security_training.toggle_training_provider(provider, toggle_to) + end + end + end + + def visit_vulnerability(vulnerability_name) + Page::Project::Menu.perform(&:go_to_vulnerability_report) + QA::EE::Page::Project::Secure::SecurityDashboard.perform do |security_dashboard| + security_dashboard.wait_for_vuln_report_to_load + expect(security_dashboard).to have_vulnerability(description: vulnerability_name) + + security_dashboard.click_vulnerability(description: vulnerability_name) + end + end + end + end +end