# 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