summaryrefslogtreecommitdiff
path: root/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb
blob: 2b049ea2d2f164a01698cb28d3f29744672cacdd (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
113
114
115
116
117
118
119
120
121
122
123
124
# frozen_string_literal: true

require 'parser/ruby27'

module Gitlab
  module BackgroundMigration
    # This migration fixes raw_metadata entries which have incorrectly been passed a Ruby Hash instead of JSON data.
    class FixVulnerabilityOccurrencesWithHashesAsRawMetadata
      CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
      GENERIC_REPORT_TYPE = 99

      # Type error is used to handle unexpected types when parsing stringified hashes.
      class TypeError < ::StandardError
        attr_reader :message, :type

        def initialize(message, type)
          @message = message
          @type = type
        end
      end

      # Migration model namespace isolated from application code.
      class Finding < ActiveRecord::Base
        include EachBatch

        self.table_name = 'vulnerability_occurrences'

        scope :by_api_report_types, -> { where(report_type: [CLUSTER_IMAGE_SCANNING_REPORT_TYPE, GENERIC_REPORT_TYPE]) }
      end

      def perform(start_id, end_id)
        Finding.by_api_report_types.where(id: start_id..end_id).each do |finding|
          next if valid_json?(finding.raw_metadata)

          metadata = hash_from_s(finding.raw_metadata)

          finding.update(raw_metadata: metadata.to_json) if metadata
        end
        mark_job_as_succeeded(start_id, end_id)
      end

      def hash_from_s(str_hash)
        ast = Parser::Ruby27.parse(str_hash)

        unless ast.type == :hash
          ::Gitlab::AppLogger.error(message: "expected raw_metadata to be a hash", type: ast.type)
          return
        end

        parse_hash(ast)
      rescue Parser::SyntaxError => e
        ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message)
        nil
      rescue TypeError => e
        ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message, type: e.type)
        nil
      end

      private

      def mark_job_as_succeeded(*arguments)
        Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
          'FixVulnerabilityOccurrencesWithHashesAsRawMetadata',
          arguments
        )
      end

      def valid_json?(metadata)
        Oj.load(metadata)
        true
      rescue Oj::ParseError, Encoding::UndefinedConversionError
        false
      end

      def parse_hash(hash)
        out = {}
        hash.children.each do |node|
          unless node.type == :pair
            raise TypeError.new("expected child of hash to be a `pair`", node.type)
          end

          key, value = node.children

          key = parse_key(key)
          value = parse_value(value)

          out[key] = value
        end

        out
      end

      def parse_key(key)
        case key.type
        when :sym, :str, :int
          key.children.first
        else
          raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
        end
      end

      def parse_value(value)
        case value.type
        when :sym, :str, :int
          value.children.first
        # rubocop:disable Lint/BooleanSymbol
        when :true
          true
        when :false
          false
        # rubocop:enable Lint/BooleanSymbol
        when :nil
          nil
        when :array
          value.children.map { |c| parse_value(c) }
        when :hash
          parse_hash(value)
        else
          raise TypeError.new("value of a pair was an unexpected type", value.type)
        end
      end
    end
  end
end