diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 04c9e999d06ea597a33c33f8c2f34c360f286226..0f9db92437244990b3da4ad234ce6e92cb141a3e 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2372,7 +2372,7 @@ Layout/LineLength:
     - 'lib/gitlab/ci/config/external/file/project.rb'
     - 'lib/gitlab/ci/config/external/file/remote.rb'
     - 'lib/gitlab/ci/parsers/coverage/cobertura.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/ci/parsers/security/common.rb'
     - 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
     - 'lib/gitlab/ci/pipeline/chain/create_cross_database_associations.rb'
@@ -3407,7 +3407,7 @@ Layout/LineLength:
     - 'spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb'
     - 'spec/lib/gitlab/ci/cron_parser_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb'
-    - 'spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
+    - 'spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/security/common_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
diff --git a/.rubocop_todo/lint/assignment_in_condition.yml b/.rubocop_todo/lint/assignment_in_condition.yml
index e86b19135659489fb8d051199a763b496a9fc8d3..49d42a5bfb4f92f80bc3afa65ecf3c1fd87ca006 100644
--- a/.rubocop_todo/lint/assignment_in_condition.yml
+++ b/.rubocop_todo/lint/assignment_in_condition.yml
@@ -154,7 +154,7 @@ Lint/AssignmentInCondition:
     - 'lib/gitlab/ci/build/rules/rule.rb'
     - 'lib/gitlab/ci/config/external/rules.rb'
     - 'lib/gitlab/ci/config/normalizer.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/ci/yaml_processor.rb'
     - 'lib/gitlab/counters/buffered_counter.rb'
     - 'lib/gitlab/data_builder/deployment.rb'
diff --git a/.rubocop_todo/naming/heredoc_delimiter_naming.yml b/.rubocop_todo/naming/heredoc_delimiter_naming.yml
index 09e03a992dd083ccc08088617211de6b51298c69..97355ed07f917c8cb06f29c5e2ec225c5f0731cd 100644
--- a/.rubocop_todo/naming/heredoc_delimiter_naming.yml
+++ b/.rubocop_todo/naming/heredoc_delimiter_naming.yml
@@ -56,7 +56,7 @@ Naming/HeredocDelimiterNaming:
     - 'spec/lib/banzai/pipeline/full_pipeline_spec.rb'
     - 'spec/lib/gitlab/auth/ldap/config_spec.rb'
     - 'spec/lib/gitlab/ci/config_spec.rb'
-    - 'spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
+    - 'spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
     - 'spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb'
     - 'spec/lib/gitlab/ci/templates/templates_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index 1156e89a01971f5eb473a682ba19786367d27e76..8dd12d76f4b3c6676014937fcf0c187f44df14c9 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -2355,7 +2355,6 @@ RSpec/FeatureCategory:
     - 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb'
-    - 'spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/instrumentation_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/security/sast_spec.rb'
     - 'spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb'
diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml
index 23e93ec6f178cedf2c0944fdfd3b626580d1cdcb..31970e804d2f9c73c450d363fc9b3b5058f53e27 100644
--- a/.rubocop_todo/style/guard_clause.yml
+++ b/.rubocop_todo/style/guard_clause.yml
@@ -401,7 +401,7 @@ Style/GuardClause:
     - 'lib/gitlab/ci/config/external/file/base.rb'
     - 'lib/gitlab/ci/config/external/file/remote.rb'
     - 'lib/gitlab/ci/config/external/file/template.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/ci/parsers/security/common.rb'
     - 'lib/gitlab/ci/pipeline/chain/command.rb'
     - 'lib/gitlab/ci/pipeline/chain/limit/rate_limit.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index bc1ef52bad9187774d683d7c327428ce7f825aa4..a3b7bcbffdf0f42e8372ed437e74c41e1e25acdd 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -599,7 +599,7 @@ Style/IfUnlessModifier:
     - 'lib/gitlab/ci/config/external/file/base.rb'
     - 'lib/gitlab/ci/config/external/file/template.rb'
     - 'lib/gitlab/ci/config/normalizer.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/ci/parsers/security/common.rb'
     - 'lib/gitlab/ci/pipeline/chain/validate/abilities.rb'
     - 'lib/gitlab/ci/pipeline/chain/validate/repository.rb'
diff --git a/.rubocop_todo/style/redundant_return.yml b/.rubocop_todo/style/redundant_return.yml
index 8a1812a32a6c7934faff779a540c49030b0f29eb..5b4b2d0d4ceb22a6a86e27fe0c60160c08d92a69 100644
--- a/.rubocop_todo/style/redundant_return.yml
+++ b/.rubocop_todo/style/redundant_return.yml
@@ -50,7 +50,7 @@ Style/RedundantReturn:
     - 'lib/feature/gitaly.rb'
     - 'lib/gitlab/auth/database/authentication.rb'
     - 'lib/gitlab/checks/diff_check.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/database/health_status/indicators/prometheus_alert_indicator.rb'
     - 'lib/gitlab/graphql/queries.rb'
     - 'lib/gitlab/hotlinking_detector.rb'
diff --git a/.rubocop_todo/style/redundant_self.yml b/.rubocop_todo/style/redundant_self.yml
index e4e3f52cbdc87647f5ff9f5df005db5589da9b06..a3f65de97e46f4a360366d8a8314632db12fb296 100644
--- a/.rubocop_todo/style/redundant_self.yml
+++ b/.rubocop_todo/style/redundant_self.yml
@@ -259,7 +259,7 @@ Style/RedundantSelf:
     - 'lib/gitlab/ci/config/entry/jobs.rb'
     - 'lib/gitlab/ci/config/entry/root.rb'
     - 'lib/gitlab/ci/jwt.rb'
-    - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
+    - 'lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
     - 'lib/gitlab/ci/pipeline/expression/lexeme/null.rb'
     - 'lib/gitlab/ci/pipeline/preloader.rb'
     - 'lib/gitlab/ci/queue/metrics.rb'
diff --git a/app/models/concerns/enums/ci/job_artifact.rb b/app/models/concerns/enums/ci/job_artifact.rb
index 18b0b9f3d3730d10b5532689e33597eacae9436d..938b371c4572fe297062135b371483c51bd97069 100644
--- a/app/models/concerns/enums/ci/job_artifact.rb
+++ b/app/models/concerns/enums/ci/job_artifact.rb
@@ -180,7 +180,7 @@ def self.file_type
           cyclonedx: 28, ## EE-specific
           requirements_v2: 29, ## EE-specific
           annotations: 30,
-          repository_xray: 31 ## EE-specifric
+          repository_xray: 31 ## EE-specific
         }
       end
 
diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb
index 6041907ef78da11a433821ed09e255ed1d1ca475..9f56b81fda9a7cf74b71f70078c31a1dd5f042c3 100644
--- a/lib/gitlab/ci/parsers/coverage/cobertura.rb
+++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb
@@ -9,7 +9,7 @@ class Cobertura
           InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError)
 
           def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil)
-            Nokogiri::XML::SAX::Parser.new(SaxDocument.new(coverage_report, project_path, worktree_paths)).parse(xml_data)
+            Nokogiri::XML::SAX::Parser.new(Documents::CoberturaDocument.new(coverage_report, project_path, worktree_paths)).parse(xml_data)
           end
         end
       end
diff --git a/lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb b/lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1bdfc9ab2b6adc943678ccb246eb2816bc99f202
--- /dev/null
+++ b/lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Parsers
+      module Coverage
+        module Documents
+          class CoberturaDocument < Nokogiri::XML::SAX::Document
+            GO_SOURCE_PATTERN = '/usr/local/go/src'
+            MAX_SOURCES = 100
+
+            def initialize(coverage_report, project_path, worktree_paths)
+              @coverage_report = coverage_report
+              @project_path = project_path
+              @paths = worktree_paths&.to_set
+
+              @matched_filenames = []
+              @parsed_lines = []
+              @sources = []
+            end
+
+            def error(error)
+              raise Cobertura::InvalidXMLError, "XML parsing failed with error: #{error}"
+            end
+
+            def start_element(node_name, attrs = [])
+              return unless node_name
+
+              self.node_name = node_name
+              node_attrs = Hash[attrs]
+
+              if node_name == 'class' && node_attrs["filename"].present?
+                self.filename = determine_filename(node_attrs["filename"])
+                self.matched_filenames << filename if filename
+              elsif node_name == 'line'
+                self.parsed_lines << parse_line(node_attrs)
+              end
+            end
+
+            def characters(node_content)
+              if node_name == 'source'
+                parse_source(node_content)
+              end
+            end
+
+            def end_element(node_name)
+              if node_name == "package"
+                remove_matched_filenames
+              elsif node_name == "class" && filename && parsed_lines.present?
+                coverage_report.add_file(filename, Hash[parsed_lines])
+                self.filename = nil
+                self.parsed_lines = []
+              end
+            end
+
+            private
+
+            attr_accessor :coverage_report, :project_path, :paths, :sources, :node_name, :filename, :parsed_lines, :matched_filenames
+
+            def parse_line(line)
+              [Integer(line["number"]), Integer(line["hits"])]
+            rescue StandardError
+              raise Cobertura::InvalidLineInformationError, "Line information had invalid values"
+            end
+
+            def parse_source(node)
+              return unless project_path && paths && node.exclude?(GO_SOURCE_PATTERN)
+
+              source = build_source_path(node)
+              self.sources << source if source.present?
+            end
+
+            def build_source_path(node)
+              # | raw source                  | extracted  |
+              # |-----------------------------|------------|
+              # | /builds/foo/test/SampleLib/ | SampleLib/ |
+              # | /builds/foo/test/something  | something  |
+              # | /builds/foo/test/           | nil        |
+              # | /builds/foo/test            | nil        |
+              # | D:\builds\foo\bar\app\      | app\       |
+              unixify(node).split("#{project_path}/", 2)[1]
+            end
+
+            def unixify(path)
+              path.tr('\\', '/')
+            end
+
+            def remove_matched_filenames
+              return unless paths
+
+              matched_filenames.each { |f| paths.delete(f) }
+            end
+
+            def determine_filename(filename)
+              return filename unless sources.any?
+
+              full_filename = nil
+
+              sources.each_with_index do |source, index|
+                break if index >= MAX_SOURCES
+                break if full_filename = check_source(source, filename)
+              end
+
+              full_filename
+            end
+
+            def check_source(source, filename)
+              full_path = File.join(source, filename)
+
+              return full_path if paths.include?(full_path)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/parsers/coverage/sax_document.rb b/lib/gitlab/ci/parsers/coverage/sax_document.rb
deleted file mode 100644
index eafeaf9d505adf3097734f220f24b646d2ab3d42..0000000000000000000000000000000000000000
--- a/lib/gitlab/ci/parsers/coverage/sax_document.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Ci
-    module Parsers
-      module Coverage
-        class SaxDocument < Nokogiri::XML::SAX::Document
-          GO_SOURCE_PATTERN = '/usr/local/go/src'
-          MAX_SOURCES = 100
-
-          def initialize(coverage_report, project_path, worktree_paths)
-            @coverage_report = coverage_report
-            @project_path = project_path
-            @paths = worktree_paths&.to_set
-
-            @matched_filenames = []
-            @parsed_lines = []
-            @sources = []
-          end
-
-          def error(error)
-            raise Cobertura::InvalidXMLError, "XML parsing failed with error: #{error}"
-          end
-
-          def start_element(node_name, attrs = [])
-            return unless node_name
-
-            self.node_name = node_name
-            node_attrs = Hash[attrs]
-
-            if node_name == 'class' && node_attrs["filename"].present?
-              self.filename = determine_filename(node_attrs["filename"])
-              self.matched_filenames << filename if filename
-            elsif node_name == 'line'
-              self.parsed_lines << parse_line(node_attrs)
-            end
-          end
-
-          def characters(node_content)
-            if node_name == 'source'
-              parse_source(node_content)
-            end
-          end
-
-          def end_element(node_name)
-            if node_name == "package"
-              remove_matched_filenames
-            elsif node_name == "class" && filename && parsed_lines.present?
-              coverage_report.add_file(filename, Hash[parsed_lines])
-              self.filename = nil
-              self.parsed_lines = []
-            end
-          end
-
-          private
-
-          attr_accessor :coverage_report, :project_path, :paths, :sources, :node_name, :filename, :parsed_lines, :matched_filenames
-
-          def parse_line(line)
-            [Integer(line["number"]), Integer(line["hits"])]
-          rescue StandardError
-            raise Cobertura::InvalidLineInformationError, "Line information had invalid values"
-          end
-
-          def parse_source(node)
-            return unless project_path && paths && node.exclude?(GO_SOURCE_PATTERN)
-
-            source = build_source_path(node)
-            self.sources << source if source.present?
-          end
-
-          def build_source_path(node)
-            # | raw source                  | extracted  |
-            # |-----------------------------|------------|
-            # | /builds/foo/test/SampleLib/ | SampleLib/ |
-            # | /builds/foo/test/something  | something  |
-            # | /builds/foo/test/           | nil        |
-            # | /builds/foo/test            | nil        |
-            # | D:\builds\foo\bar\app\      | app\       |
-            unixify(node).split("#{project_path}/", 2)[1]
-          end
-
-          def unixify(path)
-            path.tr('\\', '/')
-          end
-
-          def remove_matched_filenames
-            return unless paths
-
-            matched_filenames.each { |f| paths.delete(f) }
-          end
-
-          def determine_filename(filename)
-            return filename unless sources.any?
-
-            full_filename = nil
-
-            sources.each_with_index do |source, index|
-              break if index >= MAX_SOURCES
-              break if full_filename = check_source(source, filename)
-            end
-
-            full_filename
-          end
-
-          def check_source(source, filename)
-            full_path = File.join(source, filename)
-
-            return full_path if paths.include?(full_path)
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
index 65d85c7f1c0c52b753546f3c8ea1c06934b8ca92..8930129e35b65e7e2328da3720a6dfdb49d0479f 100644
--- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
@@ -16,8 +16,8 @@
     end
   end
 
-  it 'uses Sax parser' do
-    expect(Gitlab::Ci::Parsers::Coverage::SaxDocument).to receive(:new)
+  it 'uses Cobertura parser' do
+    expect(Gitlab::Ci::Parsers::Coverage::Documents::CoberturaDocument).to receive(:new)
 
     parse_report
   end
diff --git a/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document_spec.rb
similarity index 99%
rename from spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
rename to spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document_spec.rb
index b168ad0816a4b811293925362af36b2ab45ca1c2..9dee1100de47b6978bf4ea021673c46ab87eb6ef 100644
--- a/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document_spec.rb
@@ -2,7 +2,8 @@
 
 require 'fast_spec_helper'
 
-RSpec.describe Gitlab::Ci::Parsers::Coverage::SaxDocument do
+RSpec.describe Gitlab::Ci::Parsers::Coverage::Documents::CoberturaDocument,
+  feature_category: :code_testing do
   subject(:parse_report) { Nokogiri::XML::SAX::Parser.new(described_class.new(coverage_report, project_path, paths)).parse(cobertura) }
 
   describe '#parse!' do
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 5dae8a460d5240a14b820888ca8679c0883fd459..beef5f5175b7ec6c763ae83d003687a0299fecba 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -4854,7 +4854,7 @@
 - './spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
 - './spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
 - './spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb'
-- './spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
+- './spec/lib/gitlab/ci/parsers/coverage/documents/cobertura_document.rb'
 - './spec/lib/gitlab/ci/parsers/instrumentation_spec.rb'
 - './spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb'
 - './spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb'