Welcome to mirror list, hosted at ThFree Co, Russian Federation.

cyclonedx_properties.rb « sbom « parsers « ci « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 3dc73544208ccd410470b3ab2c81cbaac2296020 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# frozen_string_literal: true

module Gitlab
  module Ci
    module Parsers
      module Sbom
        # Parses GitLab CycloneDX metadata properties which are defined by the taxonomy at
        # https://gitlab.com/gitlab-org/security-products/gitlab-cyclonedx-property-taxonomy
        #
        # This parser knows how to process schema version 1 and will not attempt to parse
        # later versions. Each source type has it's own namespace in the property schema,
        # and is also given its own parser. Properties are filtered by namespace,
        # and then passed to each source parser for processing.
        class CyclonedxProperties
          SUPPORTED_SCHEMA_VERSION = '1'
          GITLAB_PREFIX = 'gitlab:'
          SOURCE_PARSERS = {
            'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning
          }.freeze
          SUPPORTED_PROPERTIES = %w[
            meta:schema_version
            dependency_scanning:category
            dependency_scanning:input_file:path
            dependency_scanning:source_file:path
            dependency_scanning:package_manager:name
            dependency_scanning:language:name
          ].freeze

          def self.parse_source(...)
            new(...).parse_source
          end

          def initialize(properties)
            @properties = properties
          end

          def parse_source
            return unless properties.present?
            return unless supported_schema_version?

            source
          end

          private

          attr_reader :properties

          def property_data
            @property_data ||= properties
              .each_with_object({}) { |property, data| parse_property(property, data) }
          end

          def parse_property(property, data)
            name = property['name']
            value = property['value']

            # The specification permits the name or value to be absent.
            return unless name.present? && value.present?
            return unless name.start_with?(GITLAB_PREFIX)

            namespaced_name = name.delete_prefix(GITLAB_PREFIX)

            return unless SUPPORTED_PROPERTIES.include?(namespaced_name)

            parse_name_value_pair(namespaced_name, value, data)
          end

          def parse_name_value_pair(name, value, data)
            # Each namespace in the property name reflects a key in the hash.
            # A property with the name `dependency_scanning:input_file:path`
            # and the value `package-lock.json` should be transformed into
            # this data:
            # {"dependency_scanning": {"input_file": {"path": "package-lock.json"}}}
            keys = name.split(':')

            # Remove last item from the keys and use it to create
            # the initial object.
            last = keys.pop

            # Work backwards. For each key, create a new hash wrapping the previous one.
            # Using `dependency_scanning:input_file:path` as an example:
            #
            # 1. memo = { "path" => "package-lock.json" } (arguments given to reduce)
            # 2. memo = { "input_file" => memo }
            # 3. memo = { "dependency_scanning" => memo }
            property = keys.reverse.reduce({ last => value }) do |memo, key|
              { key => memo }
            end

            data.deep_merge!(property)
          end

          def schema_version
            @schema_version ||= property_data.dig('meta', 'schema_version')
          end

          def supported_schema_version?
            schema_version == SUPPORTED_SCHEMA_VERSION
          end

          def source
            @source ||= property_data
              .slice(*SOURCE_PARSERS.keys)
              .lazy
              .filter_map { |namespace, data| SOURCE_PARSERS[namespace].source(data) }
              .first
          end
        end
      end
    end
  end
end