F.65. transp_anon#

F.65. transp_anon

F.65. transp_anon #

transp_anon (прозрачная анонимизация) является расширением для маскировки или замены персонально идентифицируемой информации или коммерчески чувствительных данных из базы данных PostgreSQL.

F.65.1. Определения терминов, используемых в этом проекте #

Данные могут быть изменены с помощью нескольких методов:

  • Удаление или Обнуление просто удаляет данные.

  • Статическая замена последовательно заменяет данные на общее значение. Например: замена всех значений столбца TEXT на значение КОНФИДЕНЦИАЛЬНО.

  • Вариация — это действие сдвига дат и числовых значений. Например, применяя вариацию +/- 10% к столбцу зарплаты, набор данных останется значимым.

  • Обобщение снижает точность данных, заменяя их диапазоном значений. Вместо того чтобы сказать Бобу 28 лет, вы можете сказать Бобу от 20 до 30 лет. Это полезно для аналитики, потому что данные остаются верными.

  • Перемешивание смешивает значения в пределах одних и тех же столбцов. Этот метод может быть обратимым, если алгоритм перемешивания может быть расшифрован.

  • Рандомизация заменяет конфиденциальные данные случайными, но правдоподобными значениями. Цель состоит в том, чтобы избежать любой идентификации из записи данных, оставаясь при этом пригодной для тестирования, анализа данных и обработки данных.

  • Частичное перемешивание похоже на статическую замену, но исключает часть данных. Например: номер кредитной карты может быть заменен на 40XX XXXX XXXX XX96

  • Пользовательские правила предназначены для изменения данных в соответствии с конкретными потребностями. Например, одновременная рандомизация почтового индекса и названия города при сохранении их согласованности.

  • Псевдонимизация — это способ защиты личной информации путем ее сокрытия с использованием дополнительной информации. Шифрование и Хеширование — два примера техник псевдонимизации. Однако псевдонимизированные данные все еще связаны с исходными данными.

F.65.2. Анонимизация & Маскирование данных для PostgreSQL #

Проект имеет декларативный подход к анонимизации. Это означает, что вы можете объявить правила маскировки с использованием языка определения данных PostgreSQL (DDL) и указать свою стратегию анонимизации непосредственно в определении таблицы.

Как только правила маскировки определены, вы можете получить доступ к анонимизированным данным, используя: * Динамическая маскировка: Скрыть личную информацию только для замаскированных пользователей

Кроме того, доступны различные Функции маскировки: рандомизация, подделка, частичное перемешивание, перемешивание, шум или ваша собственная пользовательская функция!

Помимо маскировки, также возможно использовать четвертый подход, называемый Обобщение, который идеально подходит для статистики и анализа данных.

F.65.2.1. Пример #

=# SELECT * FROM people;
id | firstname | lastname |   phone
----+-----------+----------+------------
T1 | Sarah     | Conor    | 0609110911
            

Шаг 1 : Активируйте движок динамического маскирования

=# CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
=# SELECT transp_anon.init();
            

Шаг 2 : Объявите маскированного пользователя и предоставьте ему разрешения на выбор данных

=# CREATE ROLE skynet LOGIN;
=# GRANT SELECT ON TABLE people TO skynet;
=# SECURITY LABEL FOR transp_anon ON ROLE skynet IS 'MASKED';
            

Шаг 3 : Объявите правила маскировки

=# SECURITY LABEL FOR transp_anon ON COLUMN people.lastname
-# IS 'MASKED WITH FUNCTION transp_anon.fake_last_name()';

=# SECURITY LABEL FOR transp_anon ON COLUMN people.phone
-# IS 'MASKED WITH FUNCTION transp_anon.partial(phone,2,$$******$$,2)';
            

Шаг 4 : Подключитесь от имени маскированного пользователя

=# \connect - skynet
=> SELECT * FROM people;
id | firstname | lastname  |   phone
----+-----------+-----------+------------
T1 | Sarah     | Stranahan | 06******11
            

F.65.3. Конфигурация #

Расширение в настоящее время имеет несколько параметров, которые можно определить для всей инстанции ( внутри postgresql.conf или с помощью ALTER SYSTEM).

Также возможно и часто хорошей идеей является определение их на уровне базы данных следующим образом:

ALTER DATABASE people SET transp_anon.restrict_to_trusted_schemas = on;
            

Только суперпользователь может изменить следующие параметры:

F.65.3.1. transp_anon.enabled #

Тип Логический
Значение по умолчанию true
Видимый только для суперпользователей

Позволяет глобально включать или отключать transp_anon.

F.65.3.2. transp_anon.inherit_labels #

Тип Логический
Значение по умолчанию true
Видимый только для суперпользователей

Наследовать метки безопасности от родительских отношений (секционированные таблицы и таблицы наследования), если таковые имеются.

F.65.3.3. transp_anon.algorithm #

Тип Текст
Значение по умолчанию sha256
Видимый только для суперпользователей

Это метод хеширования, используемый псевдонимизирующими функциями. Ознакомьтесь с pgcrypto документацией для списка доступных опций.

См. transp_anon.salt, чтобы узнать, почему этот параметр является очень конфиденциальной информацией.

F.65.3.4. transp_anon.restrict_to_trusted_schemas #

Тип Логический
Значение по умолчанию выкл
Видимый для всех пользователей

Включив этот параметр, правила маскирования должны быть определены с использованием функций, расположенных в ограниченном списке пространств имен. По умолчанию доверяется только схема transp_anon.

Это улучшает безопасность, предотвращая пользователей от объявления своих собственных фильтров маскировки. Это также означает, что схема должна быть явной внутри правил маскировки.

Для получения дополнительной информации, ознакомьтесь с разделом Напишите свои собственные маски главы Функции маскирования.

F.65.3.5. transp_anon.salt #

Тип Текст
Значение по умолчанию (пусто)
Видимый только для суперпользователей

Это соль, используемая псевдонимизирующими функциями. Очень важно определить пользовательскую соль для каждой базы данных следующим образом:

ALTER DATABASE foo SET transp_anon.salt = 'This_Is_A_Very_Secret_Salt';
            

Если замаскированный пользователь может прочитать соль, он/она может провести атаку методом перебора, чтобы восстановить исходные данные на основе 3 элементов:

  • Псевдонимизированные данные

  • Алгоритм хеширования (см. transp_anon.algorithm)

  • Соль

Соль и название алгоритма хеширования должны быть защищены с тем же уровнем безопасности, что и сами данные. Именно поэтому вы должны хранить соль непосредственно в базе данных с помощью ALTER DATABASE.

F.65.4. Объявить правила маскировки #

Основная идея этого расширения заключается в предложении анонимизации по замыслу.

Правила маскирования данных должны быть написаны людьми, которые разрабатывают приложение, потому что они лучше всего знают, как работает модель данных. Поэтому правила маскирования должны быть реализованы непосредственно внутри схемы базы данных.

Это позволяет маскировать данные непосредственно внутри экземпляра PostgreSQL без использования внешнего инструмента, тем самым ограничивая воздействие и риски утечки данных.

Правила маскирования данных объявляются просто с использованием SECURITY LABEL:

CREATE TABLE player( id SERIAL, name TEXT, points INT);

INSERT INTO player VALUES
( 1, 'Kareem Abdul-Jabbar', 38387),
( 5, 'Michael Jordan', 32292 );

SECURITY LABEL FOR transp_anon ON COLUMN player.name
IS 'MASKED WITH FUNCTION transp_anon.fake_last_name()';

SECURITY LABEL FOR transp_anon ON COLUMN player.id
IS 'MASKED WITH VALUE NULL';
        

F.65.4.1. Экранирование строковых литералов #

Как вы могли заметить, определения правил маскировки заключены в одинарные кавычки. Поэтому, если вам нужно использовать строку внутри правила маскировки, вам нужно использовать C-Style escapes следующим образом:

SECURITY LABEL FOR transp_anon ON COLUMN player.name
IS E'MASKED WITH VALUE \'CONFIDENTIAL\'';
            

Или используйте долларовое цитирование, которое легче читать:

SECURITY LABEL FOR transp_anon ON COLUMN player.name
IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';
            

F.65.4.2. Перечисление правил маскировки #

Чтобы отобразить все правила маскировки, объявленные в текущей базе данных, ознакомьтесь с transp_anon.pg_masking_rules:

SELECT * FROM transp_anon.pg_masking_rules;
            

F.65.4.3. Отладка правил маскирования #

Когда возникает ошибка из-за неправильного правила маскировки, вы можете получить более подробную информацию о проблеме, установив client_min_messages в DEBUG, и вы получите полезные детали

SET client_min_messages=DEBUG;
            

F.65.4.4. Удаление правила маскировки #

Вы можете просто удалить правило маскировки следующим образом:

SECURITY LABEL FOR transp_anon ON COLUMN player.name IS NULL;
            

Чтобы удалить все правила сразу, вы можете использовать:

SELECT transp_anon.remove_masks_for_all_columns();
            

F.65.4.5. Ограничения #

F.65.5. Функции маскирования #

Расширение предоставляет функции для реализации следующих основных стратегий анонимизации:

Также вы можете использовать дополнительные функции маскирования или написать свои собственные:

В зависимости от ваших данных, вам может потребоваться использовать разные стратегии для разных столбцов :

F.65.5.1. Уничтожение #

Прежде всего, самый быстрый и безопасный способ анонимизировать данные — уничтожить их.

Во многих случаях лучшим подходом для скрытия содержимого столбца является замена всех значений на одно статическое значение.

Например, вы можете заменить целую колонку словом КОНФИДЕНЦИАЛЬНО вот так:

SECURITY LABEL FOR transp_anon
ON COLUMN users.address
IS 'MASKED WITH VALUE ''CONFIDENTIAL'' ';
            

F.65.5.2. Добавление шума #

Это также называется дисперсия. Идея заключается в сдвиге дат и числовых значений. Например, применяя дисперсию +/- 10% к столбцу зарплат, набор данных останется значимым.

  • transp_anon.noise(original_value,ratio), где original_value может быть integer, bigint или double precision. Если ratio равно 0.33, возвращаемое значение будет исходным значением, случайным образом смещенным на +/- 33%

  • transp_anon.dnoise(original_value, interval), где original_value может быть датой, временной меткой или временем. Если interval = 2 days, возвращаемое значение будет исходным значением, случайным образом сдвинутым на +/- 2 дня

ПРЕДУПРЕЖДЕНИЕ : Функции маскировки noise() уязвимы к форме повторной атаки, особенно при использовании Динамической Маскировки. Маскированный пользователь может угадать исходное значение, запрашивая его маскированное значение несколько раз, а затем просто использовать функцию AVG() для получения близкого приближения. (См. Атака на снижение шума для получения более подробной информации). В двух словах, эти функции лучше всего подходят для Анонимных Снимков и Статической Маскировки. Их следует избегать при использовании Динамической Маскировки.

F.65.5.3. Рандомизация #

Расширение предоставляет большой выбор функций для генерации чисто случайных данных:

F.65.5.3.1. Основные случайные значения #
  • transp_anon.random_date() возвращает дату

  • transp_anon.random_string(n) возвращает значение типа TEXT, содержащее n букв

  • transp_anon.random_zip() возвращает 5-значный код

  • transp_anon.random_phone(p) возвращает 8-значный телефонный номер с префиксом p

  • transp_anon.random_hash(seed) возвращает хэш случайной строки для заданного seed

F.65.5.3.2. Случайное значение между #

Чтобы выбрать любое значение между двумя границами:

  • transp_anon.random_date_between(d1,d2) возвращает дату между d1 и d2

  • transp_anon.random_int_between(i1,i2) возвращает целое число между i1 и i2

  • transp_anon.random_bigint_between(b1,b2) возвращает bigint между b1 и b2

ПРИМЕЧАНИЕ: С этими функциями нижние и верхние границы включены. Например, transp_anon.random_int_between(1,3) возвращает либо 1, 2 или 3.

Для более продвинутых описаний интервалов, ознакомьтесь с разделом Случайное значение в диапазоне.

F.65.5.3.3. Случайный элемент в массиве #

Функция random_in возвращает элемент заданного массива

Например:

  • transp_anon.random_in(ARRAY[1,2,3]) возвращает int между 1 и 3

  • transp_anon.random_in(ARRAY['red','green','blue']) возвращает текст

F.65.5.3.4. Случайный в Enum #

Это особенно полезно при работе с типами ENUM!

  • transp_anon.random_in_enum(variable_of_an_enum_type) возвращает любое значение

CREATE TYPE card AS ENUM ('visa', 'mastercard', ‘amex’);

SELECT transp_anon.random_in_enum(NULL::CARD);
random_in_enum
----------------
mastercard

CREATE TABLE customer (
id INT,
...
credit_card CARD
);

SECURITY LABEL FOR transp_anon ON COLUMN customer.creditcard
IS 'MASKED WITH FUNCTION transp_anon.random_in_enum(creditcard)'
                
F.65.5.3.5. Случайное в диапазоне #

Типы RANGE являются мощным способом описания интервала значений, где можно задать включительные или исключительные границы, см. примеры.

Существует функция для каждого подтипа диапазона:

  • transp_anon.random_in_int4range('[5,6)') возвращает INT со значением 5

  • transp_anon.random_in_int8range('(6,7]') возвращает BIGINT со значением 7

  • `transp_anon.random_in_numrange([0.1,0.9]) возвращает NUMERIC между 0.1 и 0.9

  • transp_anon.random_in_daterange('[2001-01-01, 2001-12-31)') возвращает дату в 2001 году

  • transp_anon.random_in_tsrange('[2022-10-01,2022-10-31]') возвращает TIMESTAMP в октябре 2022

  • transp_anon.random_in_tstzrange('[2022-10-01,2022-10-31]') возвращает TIMESTAMP WITH TIMEZONE в октябре 2022

ПРИМЕЧАНИЕ: Невозможно получить случайное значение из ДИАПАЗОНА с бесконечной границей. Например, transp_anon.random_in_int4range('[2022,)') возвращает NULL.

F.65.5.4. Подделка #

Идея Фальсификации заключается в замене конфиденциальных данных случайными, но правдоподобными значениями. Цель состоит в том, чтобы избежать любой идентификации из записи данных, оставаясь при этом пригодными для тестирования, анализа данных и обработки данных.

Для использования поддельных функций, сначала необходимо init() расширение в вашей базе данных:

SELECT transp_anon.init();
            

Функция init() импортирует набор данных по умолчанию, состоящий из случайных данных (iban, имена, города и т.д.).

Примечание

Этот набор данных на русском языке и очень маленький (1000 значений для каждой категории). Если вы хотите использовать локализованные данные или загрузить определенный набор данных, пожалуйста, прочитайте раздел Пользовательские поддельные данные.

После загрузки фальшивых данных у вас есть доступ к следующим функциям подделки:

  • transp_anon.fake_address() возвращает полный почтовый адрес

  • transp_anon.fake_city() возвращает существующий город

  • transp_anon.fake_country() возвращает страну

  • transp_anon.fake_company() возвращает общее название компании

  • transp_anon.fake_email() возвращает действительный адрес электронной почты

  • transp_anon.fake_first_name() возвращает общее имя

  • transp_anon.fake_iban() возвращает действительный IBAN

  • transp_anon.fake_last_name() возвращает обобщенную фамилию

  • transp_anon.fake_postcode() возвращает действительный почтовый индекс

Для столбцов TEXT и VARCHAR вы можете использовать классический Lorem Ipsum генератор:

  • transp_anon.lorem_ipsum() возвращает 5 абзацев

  • transp_anon.lorem_ipsum(2) возвращает 2 абзаца

  • transp_anon.lorem_ipsum( paragraphs := 4 ) возвращает 4 абзаца

  • transp_anon.lorem_ipsum( words := 20 ) возвращает 20 слов

  • transp_anon.lorem_ipsum( characters := 7 ) возвращает 7 символов

  • transp_anon.lorem_ipsum( characters := transp_anon.length(table.column) ) возвращает то же количество символов, что и исходная строка

F.65.5.5. Псевдонимизация #

Псевдонимизация аналогична Фальсификации в том смысле, что она генерирует реалистичные значения. Основное отличие заключается в том, что псевдонимизация является детерминированной: функции всегда будут возвращать одно и то же поддельное значение на основе начального значения и необязательной соли.

Для использования поддельных функций, сначала необходимо init() расширение в вашей базе данных:

SELECT transp_anon.init();
            

После загрузки фиктивных данных у вас есть доступ к 10 псевдо функциям:

  • transp_anon.pseudo_first_name(seed,salt) возвращает общее имя

  • transp_anon.pseudo_last_name(seed,salt) возвращает общее фамилия

  • transp_anon.pseudo_email(seed,salt) возвращает действительный адрес электронной почты

  • transp_anon.pseudo_city(seed,salt) возвращает существующий город

  • transp_anon.pseudo_country(seed,salt) возвращает страну

  • transp_anon.pseudo_company(seed,salt) возвращает общее название компании

  • transp_anon.pseudo_iban(seed,salt) возвращает действительный IBAN

Второй аргумент (salt) является необязательным. Вы можете вызвать каждую функцию только с затравкой, например, так: transp_anon.pseudo_city('bob'). Соль используется для увеличения сложности и предотвращения атак словарем и методом перебора (см. предупреждение ниже). Если конкретная соль не указана, используется значение параметра GUC transp_anon.salt (см. раздел Общее хеширование для получения дополнительных сведений).

Затравкой может служить любая информация, связанная с предметом. Например, мы можем последовательно генерировать один и тот же фальсифицированный адрес электронной почты для данного человека, используя его логин в качестве затравки:

SECURITY LABEL FOR transp_anon
ON COLUMN users.emailaddress
IS 'MASKED WITH FUNCTION transp_anon.pseudo_email(users.login) ';
            

ПРИМЕЧАНИЕ : Возможно, вам потребуется создавать уникальные значения с использованием функции псевдонимизации. Например, если вы хотите замаскировать столбец email, который объявлен как UNIQUE. В этом случае вам нужно будет инициализировать расширение с фальшивым набором данных, который значительно больше количества строк в таблице. В противном случае вы можете столкнуться с коллизиями, т.е. два разных исходных значения могут производить одно и то же псевдозначение.

ПРЕДУПРЕЖДЕНИЕ : Псевдонимизация часто путается с анонимизацией, но на самом деле они служат двум разным целям: pseudonymization является способом защитить личную информацию, но псевдонимизированные данные все еще связаны с реальными данными. GDPR ясно указывает, что личные данные, прошедшие псевдонимизацию, все еще относятся к человеку. (см. GDPR Recital 26)

F.65.5.6. Общее хеширование #

В теории хеширование не является допустимой техникой анонимизации, однако на практике иногда необходимо сгенерировать детерминированный хеш исходных данных.

Например, когда пара первичный ключ / внешний ключ является естественным ключом, она может содержать фактическую информацию (например, номер клиента, содержащий дату рождения или что-то подобное).

Хеширование таких столбцов позволяет сохранить целостность ссылок даже для относительно необычных исходных данных. Поэтому,

  • transp_anon.digest(value,salt,algorithm) позволяет вам выбрать соль и алгоритм хеширования из предопределенного списка

  • transp_anon.hash(value) вернет текстовый хеш значения, используя секретный соль (определяемый параметром transp_anon.salt) и алгоритм хеширования (определяемый параметром transp_anon.algorithm). Значение по умолчанию для transp_anon.algorithmsha256, и возможные значения: md5, sha1, sha224, sha256, sha384 или sha512. Значение по умолчанию для transp_anon.salt — пустая строка. Вы можете изменить эти значения с помощью:

    ALTER DATABASE foo SET transp_anon.salt TO 'xsfnjefnjsnfjsnf';
    ALTER DATABASE foo SET transp_anon.algorithm TO 'sha384';
                        

Имейте в виду, что хеширование является формой псевдонимизации. Это означает, что данные могут быть деанонимизированы с использованием хешированного значения и функции маскировки. Если злоумышленник получит доступ к этим двум элементам, он или она сможет повторно идентифицировать некоторых лиц, используя brute force или dictionary атаки. Поэтому, дополнение к паролю и алгоритм, используемые для хеширования данных, должны быть защищены с тем же уровнем безопасности, что и исходный набор данных.

В двух словах, мы рекомендуем использовать функцию transp_anon.hash() вместо transp_anon.digest(), потому что соль не будет явно видна в правиле маскировки.

Более того: на практике хэш-функция вернет длинную строку символов, подобную этой:

SELECT transp_anon.hash('bob');
                                hash
----------------------------------------------------------------------------------------------------------------------------------
95b6accef02c5a725a8c9abf19ab5575f99ca3d9997984181e4b3f81d96cbca4d0977d694ac490350e01d0d213639909987ef52de8e44d6258d536c55e427397
            

Для некоторых столбцов это может быть слишком длинным, и вам, возможно, придется обрезать некоторые части хэша, чтобы он поместился в столбец. Например, если у вас есть внешний ключ, основанный на номере телефона, и столбец является VARCHAR(12), вы можете преобразовать данные следующим образом:

SECURITY LABEL FOR transp_anon ON COLUMN people.phone_number
IS 'MASKED WITH FUNCTION transp_anon.left(transp_anon.hash(phone_number),12)';

SECURITY LABEL FOR transp_anon ON COLUMN call_history.fk_phone_number
IS 'MASKED WITH FUNCTION transp_anon.left(transp_anon.hash(fk_phone_number),12)';
            

Конечно, сокращение хеш-значения до 12 символов увеличит риск коллизии (2 разных значения имеют один и тот же фальшивый хеш). В таком случае, вам нужно оценить этот риск.

F.65.5.7. Частичное Перемешивание #

Частичное перемешивание исключает часть данных. Например: номер кредитной карты может быть заменен на 40XX XXXX XXXX XX96.

Доступны 2 функции:

  • transp_anon.partial('abcdefgh',1,'xxxx',3) вернет axxxxfgh;

  • transp_anon.partial_email('[email protected]') станет da******@gm******.com

F.65.5.8. Условное Маскирование #

В некоторых ситуациях вы можете захотеть применить фильтр маскирования только для некоторых значений или для ограниченного количества строк в таблице.

Например, если вы хотите сохранить значения NULL, т.е. маскировать только строки, содержащие значение, вы можете использовать функцию transp_anon.ternary, которая работает как оператор CASE WHEN x THEN y ELSE z:

SECURITY LABEL FOR transp_anon ON COLUMN player.score
IS 'MASKED WITH FUNCTION transp_anon.ternary(score IS NULL,
                                        NULL,
                                        transp_anon.random_int_between(0,100));
            

Вы также можете захотеть исключить некоторые строки в таблице. Например, сохранить пароли некоторых пользователей, чтобы они все еще могли подключаться к тестовому развертыванию вашего приложения:

SECURITY LABEL FOR transp_anon ON COLUMN account.password
IS 'MASKED WITH FUNCTION transp_anon.ternary( id > 1000, NULL::TEXT, password)';
            

ПРЕДУПРЕЖДЕНИЕ : Условное маскирование может создать частично детерминированную связь между исходными данными и замаскированными данными. И эту связь можно использовать для извлечения личной информации из замаскированных данных. Например, если значения NULL сохраняются для столбца deceased_date, это покажет, какие люди на самом деле еще живы… В двух словах: условное маскирование часто может создавать набор данных, который не полностью анонимизирован и, следовательно, технически все еще содержит личную информацию.

F.65.5.9. Обобщение #

Обобщение — это принцип замены исходного значения диапазоном, содержащим это значение. Например, вместо того чтобы сказать Полу 42 года, вы бы сказали Полу от 40 до 50 лет.

Примечание

Функции обобщения являются преобразованием типа данных. Поэтому их невозможно использовать с динамическим механизмом маскировки. Однако они полезны для создания анонимизированных представлений. См. пример ниже.

Давайте представим таблицу, содержащую информацию о здоровье:

SELECT * FROM patient;
id |   name   |  zipcode |   birth    |    disease
----+----------+----------+------------+---------------
1 | Alice    |    47678 | 1979-12-29 | Heart Disease
2 | Bob      |    47678 | 1959-03-22 | Heart Disease
3 | Caroline |    47678 | 1988-07-22 | Heart Disease
4 | David    |    47905 | 1997-03-04 | Flu
5 | Eleanor  |    47909 | 1999-12-15 | Heart Disease
6 | Frank    |    47906 | 1968-07-04 | Cancer
7 | Geri     |    47605 | 1977-10-30 | Heart Disease
8 | Harry    |    47673 | 1978-06-13 | Cancer
9 | Ingrid   |    47607 | 1991-12-12 | Cancer
            

Мы можем создать представление на основе этой таблицы, чтобы скрыть некоторые столбцы (SSN и name) и обобщить почтовый индекс и дату рождения следующим образом:

CREATE VIEW anonymized_patient AS
SELECT
    'REDACTED' AS lastname,
    transp_anon.generalize_int4range(zipcode,100) AS zipcode,
    transp_anon.generalize_tsrange(birth,'decade') AS birth
    disease
FROM patients;
            

Анонимизированная таблица теперь выглядит так:

SELECT * FROM anonymized_patient;
lastname |   zipcode     |           birth             |    disease
----------+---------------+-----------------------------+---------------
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1950-01-01","1960-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1980-01-01","1990-01-01") | Heart Disease
REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Flu
REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Heart Disease
REDACTED | [47900,48000) | ["1960-01-01","1970-01-01") | Cancer
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Cancer
REDACTED | [47600,47700) | ["1990-01-01","2000-01-01") | Cancer
            

Обобщенные значения по-прежнему полезны для статистики, потому что они остаются верными, но они менее точны, и поэтому снижают риск повторной идентификации.

PostgreSQL предлагает несколько типов ДИАПАЗОНОВ которые идеально подходят для дат и числовых значений.

Для числовых значений доступны 3 функции:

  • generalize_int4range(value, step)

  • generalize_int8range(value, step)

  • generalize_numrange(value, step)

…где value — это данные, которые будут обобщены, а step — это размер каждого диапазона.

F.65.5.10. Перемешивание #

Перемешивание смешивает значения в пределах одних и тех же столбцов.

  • anon.shuffle_column(shuffle_table, shuffle_column, primary_key) перемешает все значения в указанном столбце. Вам нужно указать первичный ключ таблицы.

Это полезно для внешних ключей, потому что будет сохранена ссылочная целостность.

F.65.5.11. Использование функций pg_catalog #

Начиная с версии 1.3, схема pg_catalog по умолчанию не является доверенной. Это мера безопасности, предназначенная для предотвращения использования пользователями сложных функций в правилах маскировки (таких как pg_catalog.query_to_xml, pg_catalog.ts_stat или функции системного администрирования), которые не должны использоваться в качестве функций маскировки.

Однако расширение предоставляет привязки к некоторым полезным и безопасным функциям из схемы pg_catalog для вашего удобства:

  • transp_anon.concat(TEXT,TEXT)

  • transp_anon.concat(ТЕКСТ,ТЕКСТ, ТЕКСТ)

  • transp_anon.date_add(TIMESTAMP WITH TIME ZONE,ИНТЕРВАЛ)

  • transp_anon.date_part(TEXT,TIMESTAMP)

  • transp_anon.date_part(TEXT,INTERVAL)

  • transp_anon.date_subtract(TIMESTAMP WITH TIME ZONE, ИНТЕРВАЛ )

  • transp_anon.date_trunc(TEXT,TIMESTAMP)

  • transp_anon.date_trunc(ТЕКСТ,МЕТКА ВРЕМЕНИ С ЧАСОВЫМ ПОЯСОМ,ТЕКСТ)

  • transp_anon.date_trunc(TEXT,INTERVAL)

  • transp_anon.left(TEXT,INTEGER)

  • transp_anon.length(TEXT)

  • transp_anon.lower(TEXT)

  • transp_anon.make_date(INT,INT,INT )

  • transp_anon.make_time(INT,INT,DOUBLE PRECISION)

  • transp_anon.md5(TEXT)

  • transp_anon.random()

  • transp_anon.replace(TEXT,TEXT,TEXT)

  • transp_anon.regexp_replace(TEXT,TEXT,TEXT)

  • transp_anon.regexp_replace(TEXT,TEXT,TEXT,TEXT)

  • transp_anon.right(TEXT,INTEGER)

  • transp_anon.substr(TEXT,INTEGER)

  • transp_anon.substr(TEXT,INTEGER,INTEGER)

  • transp_anon.upper(TEXT)

Если вам нужно больше привязок, вы можете либо

  • Напишите свою собственную функцию отображения в доверенной схеме (см. ниже)

  • Установите схему pg_catalog как TRUSTED (не рекомендуется)

  • открыть проблему

F.65.5.12. Напишите свои собственные Маски #

Вы также можете использовать свою собственную функцию в качестве маски. Функция должна быть либо деструктивной (как Частичное Перемешивание), либо вносить некоторую случайность в набор данных (как Подделка).

Особенно для сложных типов данных, вам, возможно, придется написать свою собственную функцию. Это будет обычным случаем, если вам нужно скрыть определенные части поля JSON.

Например:

CREATE TABLE company (
    business_name TEXT,
    info JSONB
)
            

Поле info содержит неструктурированные данные, такие как:

SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
        jsonb_pretty
----------------------------------
{
    "employees": [
        {
            "lastName": "Doe",
            "firstName": "John"
        },
        {
            "lastName": "Smith",
            "firstName": "Anna"
        },
        {
            "lastName": "Jones",
            "firstName": "Peter"
        }
    ]
}
(1 row)
            

Используя функции и операторы JSON в PostgreSQL, вы можете пройти по ключам и заменить конфиденциальные значения по мере необходимости.

CREATE SCHEMA custom_masks;

-- This step requires superuser privilege
SECURITY LABEL FOR transp_anon ON SCHEMA custom_masks IS 'TRUSTED';

CREATE FUNCTION custom_masks.remove_last_name(j JSONB)
RETURNS JSONB
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
json_build_object(
    'employees' ,
    array_agg(
    jsonb_set(e ,'{lastName}', to_jsonb(transp_anon.fake_last_name()))
    )
)::JSONB
FROM jsonb_array_elements( j->'employees') e
$func$;
            

Затем проверьте, что функция работает правильно:

SELECT custom_masks.remove_last_name(info) FROM company;
            

Когда это будет в порядке, вы можете объявить эту функцию как маску для поля info:

SECURITY LABEL FOR transp_anon ON COLUMN company.info
IS 'MASKED WITH FUNCTION custom_masks.remove_last_name(info)';
            

Выберите данные:

# SELECT anonymize_table('company');
# SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
            jsonb_pretty
-------------------------------------
{
    "employees": [                 +
        {                          +
            "lastName": "Prawdzik",+
            "firstName": "John"    +
        },                         +
        {                          +
            "lastName": "Baltazor",+
            "firstName": "Anna"    +
        },                         +
        {                          +
            "lastName": "Taylan",  +
            "firstName": "Peter"   +
        }                          +
    ]                              +
}
(1 row)
            

Как вы можете видеть, манипулирование сложной структурой JSON с помощью SQL возможно, но поначалу это может быть сложно! Существует несколько способов обхода ключей и обновления значений. Вероятно, вам придется попробовать разные подходы, в зависимости от ваших реальных данных JSON и производительности, которую вы хотите достичь.

F.65.6. Динамическое маскирование #

Вы можете скрыть некоторые данные от роли, объявив эту роль как MASKED. Другие роли по-прежнему будут иметь доступ к оригинальным данным.

Пример:

CREATE TABLE people ( id TEXT, firstname TEXT, lastname TEXT, phone TEXT);
INSERT INTO people VALUES ('T1','Sarah', 'Conor','0609110911');
SELECT * FROM people;

=# SELECT * FROM people;
id | firstname | lastname |   phone
----+----------+----------+------------
T1 | Sarah    | Conor    | 0609110911
(1 row)
        

Шаг 1 : Активируйте движок динамического маскирования

=# CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
=# SELECT transp_anon.init();
        

Шаг 2 : Объявить замаскированного пользователя

=# CREATE ROLE skynet LOGIN;
=# GRANT SELECT ON TABLE people TO skynet;
=# SECURITY LABEL FOR transp_anon ON ROLE skynet IS 'MASKED';
        

Шаг 3 : Объявите правила маскировки

SECURITY LABEL FOR transp_anon ON COLUMN people.lastname
IS 'MASKED WITH FUNCTION transp_anon.fake_last_name()';

SECURITY LABEL FOR transp_anon ON COLUMN people.phone
IS 'MASKED WITH FUNCTION transp_anon.partial(phone,2,$$******$$,2)';
        

Шаг 4 : Подключитесь от имени маскированного пользователя

=# \c - skynet
=> SELECT * FROM people;
id | firstname | lastname  |   phone
----+----------+-----------+------------
T1 | Sarah    | Stranahan | 06******11
(1 row)
        

F.65.6.1. Как снять маску с роли #

Просто удалите метку безопасности следующим образом:

SECURITY LABEL FOR transp_anon ON ROLE bob IS NULL;
            

Чтобы снять маскировку со всех замаскированных ролей сразу, вы можете ввести:

SELECT transp_anon.remove_masks_for_all_roles();
            

F.65.6.2. Ограничения #

F.65.6.2.1. Производительность #

Динамическое маскирование известно своей медлительностью с некоторыми запросами, особенно если вы пытаетесь объединить 2 таблицы по замаскированному ключу, используя хеширование или псевдонимизацию.

F.65.7. Обобщение #

F.65.7.1. Снижение точности конфиденциальных данных #

Идея обобщения заключается в замене данных на более широкое, менее точное значение. Например, вместо того чтобы сказать Бобу 28 лет, можно сказать Бобу от 20 до 30 лет. Это интересно для аналитики, потому что данные остаются верными, избегая при этом риска повторной идентификации.

PostgreSQL может легко справляться с обобщением с помощью типов RANGE, очень мощного способа хранения и манипулирования набором значений, заключенных между нижней и верхней границей.

F.65.7.2. Пример #

Вот основная таблица, содержащая медицинские данные:

# SELECT * FROM patient;
    ssn     | firstname | zipcode |   birth    |    disease
-------------+-----------+---------+------------+---------------
253-51-6170 | Alice     |   47012 | 1989-12-29 | Heart Disease
091-20-0543 | Bob       |   42678 | 1979-03-22 | Allergy
565-94-1926 | Caroline  |   42678 | 1971-07-22 | Heart Disease
510-56-7882 | Eleanor   |   47909 | 1989-12-15 | Acne
098-24-5548 | David     |   47905 | 1997-03-04 | Flu
118-49-5228 | Jean      |   47511 | 1993-09-14 | Flu
263-50-7396 | Tim       |   47900 | 1981-02-25 | Heart Disease
109-99-6362 | Bernard   |   47168 | 1992-01-03 | Asthma
287-17-2794 | Sophie    |   42020 | 1972-07-14 | Asthma
409-28-2014 | Arnold    |   47000 | 1999-11-20 | Diabetes
(10 rows)
            

Мы хотим, чтобы анонимные данные оставались достоверными, потому что они будут использоваться для статистики. Мы можем создать представление на основе этой таблицы, чтобы удалить ненужные столбцы и обобщить косвенные идентификаторы:

CREATE MATERIALIZED VIEW generalized_patient AS
SELECT
'REDACTED'::TEXT AS firstname,
transp_anon.generalize_int4range(zipcode,1000) AS zipcode,
transp_anon.generalize_daterange(birth,'decade') AS birth,
disease
FROM patient;
            

Это даст нам менее точное представление о данных:

# SELECT * FROM generalized_patient;
firstname |    zipcode    |          birth          |    disease
-----------+---------------+-------------------------+---------------
REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Heart Disease
REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Allergy
REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Heart Disease
REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Acne
REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Flu
REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Flu
REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Heart Disease
REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Asthma
REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Asthma
REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Diabetes
(10 rows)
            

F.65.7.3. Функции обобщения #

PostgreSQL Anonymizer предоставляет 6 функций обобщения. По одной для каждого типа RANGE. Обычно эти функции принимают исходное значение в качестве первого параметра и второй параметр для длины каждого шага.

Для числовых значений :

  • transp_anon.generalize_int4range(42,5) возвращает диапазон [40,45)

  • transp_anon.generalize_int8range(12345,1000) возвращает диапазон [12000,13000)

  • transp_anon.generalize_numrange(42.32378,10) возвращает диапазон [40,50)

Для значений времени :

  • transp_anon.generalize_tsrange('1904-11-07','year') возвращает ['1904-01-01','1905-01-01')

  • transp_anon.generalize_tstzrange('1904-11-07','week') возвращает ['1904-11-07','1904-11-14')

  • transp_anon.generalize_daterange('1904-11-07','десятилетие') возвращает [1900-01-01,1910-01-01)

Возможные шаги: микросекунды, миллисекунды, секунда, минута, час, день, неделя, месяц, год, десятилетие, век и тысячелетие.

F.65.7.4. Ограничения #

F.65.7.4.1. Выделение и экстремальные значения #

Выделение - это возможность изолировать отдельного индивидуума в наборе данных, используя экстремальные значения или исключительные значения.

Например:

# SELECT * FROM employees;

id  |  name          | job  | salary
------+----------------+------+--------
1578 | xkjefus3sfzd   | NULL |    1498
2552 | cksnd2se5dfa   | NULL |    2257
5301 | fnefckndc2xn   | NULL |   45489
7114 | npodn5ltyp3d   | NULL |    1821
                

В этой таблице мы видим, что у конкретного сотрудника очень высокая зарплата, значительно превышающая среднюю зарплату. Поэтому, вероятно, этот человек является генеральным директором компании.

С обобщением это важно, потому что размер диапазона (шаг) должен быть достаточно широким, чтобы предотвратить идентификацию одного конкретного человека.

ПРЕДУПРЕЖДЕНИЕ : По определению, при обобщении данные остаются верными, но тип столбца изменяется. Это означает, что преобразование не является прозрачным.

F.65.8. Пользовательские поддельные данные #

Это расширение поставляется с небольшим набором фиктивных данных по умолчанию. Для каждой фиктивной функции (fake_email(), fake_first_name()) мы предоставляем только 1000 уникальных значений, и они только на русском языке.

Вот как вы можете создать свой собственный набор фальшивых данных!

F.65.8.1. Создайте свой собственный фальшивый набор данных #

В качестве примера, вот скрипт на Python, который сгенерирует для вас фальшивые данные:

$TANTOR_DIR/utils/data_generator/data_generator.py

Этот скрипт требует библиотеку Faker. Вы можете установить её с помощью:

pip3 install faker
                

Чтобы создать 5000 писем на русском и английском языках, вы бы вызвали скрипты следующим образом:

data_generator.py --table email --locales ru_RU,en --lines 5000
            

Это выведет фальшивые данные в формате CSV.

Используйте data_generator.py --help для получения более подробной информации о параметрах скрипта.

Вы можете загрузить фиктивные данные непосредственно в расширение следующим образом:

TRUNCATE transp_anon.email;

COPY transp_anon.email
FROM
PROGRAM 'data_generator.py --table email --locales ru_RU,en --lines 5000';

SELECT setval('transp_anon.email_oid_seq', max(oid))
FROM transp_anon.email;

CLUSTER transp_anon.email;
            

F.65.8.2. Загрузите свои собственные фиктивные данные #

Если вы хотите использовать свой собственный набор данных, вы можете импортировать пользовательские CSV файлы с :

SELECT transp_anon.init('/path/to/custom_csv_files/')
            

Посмотрите в папку data, чтобы найти формат файлов CSV.

F.65.9. Производительность #

Любой процесс анонимизации имеет свою цену, так как он будет потреблять время ЦП, пространство ОЗУ и, вероятно, значительное количество операций ввода-вывода на диск… Вот краткий обзор вопроса в зависимости от используемой вами стратегии….

В двух словах, производительность анонимизации будет в основном зависеть от 2 важных факторов:

  • Размер базы данных

  • Количество правил маскировки

F.65.9.1. Динамическое Маскирование #

С динамическим маскированием реальные данные заменяются на лету каждый раз, когда замаскированный пользователь отправляет запрос в базу данных. Это означает, что у пользователей с маскированием будет более медленное время отклика по сравнению с обычными (незамаскированными) пользователями. Это, как правило, приемлемо, потому что обычно замаскированные пользователи не считаются такими важными, как обычные.

Если вы примените 3 или 4 правила к таблице, время отклика для маскированных пользователей должно быть примерно на 20% - 30% медленнее, чем для обычных пользователей.

Поскольку правила маскировки применяются к каждому запросу маскированных пользователей, динамическая маскировка подходит, когда у вас ограниченное количество маскированных пользователей, которые подключаются к базе данных только время от времени. Например, аналитик данных, подключающийся раз в неделю для создания бизнес-отчета.

Если есть несколько замаскированных пользователей или если замаскированный пользователь очень активен, вам, вероятно, следует экспортировать замаскированные данные раз в неделю на вторичную инстанцию и позволить этим пользователям подключаться к этой вторичной инстанции.

Примечание

В этом случае стоимость анонимизации оплачивается только замаскированными пользователями.

F.65.9.2. Как ускорить процесс? #

F.65.9.2.1. Предпочитайте MASKED WITH VALUE всякий раз, когда это возможно #

Всегда быстрее заменить исходные данные статическим значением, чем вызывать функцию маскировки.

F.65.9.2.2. Выборка #

Если вам нужно анонимизировать данные для тестирования, скорее всего, будет достаточно меньшего подмножества вашей базы данных. В этом случае вы можете легко ускорить процесс анонимизации, уменьшив объем данных.

Ознакомьтесь с разделом Выборка для получения дополнительной информации.

F.65.9.2.3. Материализованные представления #

Динамическое маскирование не всегда требуется! В некоторых случаях более эффективно создать CREATE MATERIALIZED VIEW вместо этого.

Например:

CREATE MATERIALIZED VIEW masked_customer AS
SELECT
    id,
    anon.random_last_name() AS name,
    anon.random_date_between('1920-01-01'::DATE,now()) AS birth,
    fk_last_order,
    store_id
FROM customer;
            

F.65.10. Безопасность #

F.65.10.1. Разрешения #

Вот обзор того, что пользователи могут делать в зависимости от имеющихся у них привилегий:

Действие Суперпользователь Владелец Маскированная роль
Создать расширение Да
Удалить расширение Да
Запустить расширение Да
Сбросить расширение Да
Настроить расширение Да
Замаскировать роль Да
Начать динамическое маскирование Да
Остановить динамическое маскирование Да
Создать таблицу Да Да
Объявить правило маскировки Да Да
Вставить, удалить, обновить строку Да Да
Статическое маскирование Да Да
Выбрать реальные данные Да Да
Обычная выгрузка Да Да
Анонимный дамп Да Да
Использовать функции маскировки Да Да Да
Выбрать замаскированные данные Да Да Да
Посмотреть правил маскировки Да Да Да

F.65.10.2. Ограничьте фильтры маскировки только доверенными схемами #

По умолчанию владелец базы данных может писать правила маскировки только с функциями, которые находятся в доверенных схемах, контролируемых суперпользователями.

Из коробки только схема transp_anon объявлена доверенной. Это означает, что по умолчанию функции из pg_catalog не могут использоваться в правилах маскирования.

Для получения дополнительной информации прочитайте раздел Использование функций pg_catalog.

F.65.10.3. Контекст безопасности функций #

Большинство функций этого расширения объявлены с тегом SECURITY INVOKER. Это означает, что эти функции выполняются с привилегиями пользователя, который их вызывает. Это важное ограничение.

Это расширение содержит еще несколько функций, объявленных с тегом SECURITY DEFINER.

F.65.11. Демонстрации #

Демонстрации показывают разные случаи.

F.65.11.1. Добавить больше электронных писем #

Добавить значения в transp_anon.email

BEGIN;
CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
SELECT transp_anon.init();

SELECT count(*) FROM transp_anon.email;

CREATE TEMPORARY TABLE tmp_email
AS SELECT * FROM transp_anon.email;
TRUNCATE transp_anon.email;
INSERT INTO transp_anon.email
SELECT
ROW_NUMBER() OVER (),
concat(u.username,'@', d.domain)
FROM
(
SELECT split_part(val,'@',1) AS username
FROM tmp_email
ORDER BY RANDOM()
LIMIT 10
) u,
(
SELECT split_part(val,'@',2) AS domain
FROM tmp_email
ORDER BY RANDOM()
LIMIT 5
) d
;

SELECT count(*) FROM transp_anon.email;

ROLLBACK;
            

F.65.11.2. Динамическая анонимизация для другой роли #

В этой демонстрации мы создаем маскированную роль skynet. Операции выбора будут возвращать анонимные данные для этой роли.

-- STEP 1 : Activate the masking engine
CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
SELECT transp_anon.init();

-- STEP 2 : Declare a masked user
CREATE ROLE skynet LOGIN;
SECURITY LABEL FOR transp_anon ON ROLE skynet IS 'MASKED';

-- STEP 3 : Declare the masking rules
CREATE TABLE people ( id TEXT, firstname TEXT, lastname TEXT, phone TEXT);
INSERT INTO people VALUES ('T1','Sarah', 'Conor','0609110911');
SELECT * FROM people;

-- STEP 3 : Declare the masking rules
SECURITY LABEL FOR transp_anon ON COLUMN people.lastname
IS 'MASKED WITH FUNCTION transp_anon.fake_last_name()';

SECURITY LABEL FOR transp_anon ON COLUMN people.phone
IS 'MASKED WITH FUNCTION transp_anon.partial(phone,2,$$******$$,2)';

-- STEP 4 : Connect with the masked user
\! psql demo -U skynet -c 'SELECT * FROM people;'

-- STEP 5 : Clean up
DROP EXTENSION anon CASCADE;
REASSIGN OWNED BY skynet TO postgres;
DROP OWNED BY skynet;
DROP ROLE skynet;
            

F.65.11.3. Объявить правила маскирования для столбца #

В этой демонстрации правила маскировки объявлены для столбца player.name .

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

SELECT transp_anon.init();

CREATE TABLE player( id SERIAL, name TEXT, points INT);

INSERT INTO player VALUES  
( 1, 'Kareem Abdul-Jabbar',	38387),
( 5, 'Michael Jordan',	32292);

SECURITY LABEL FOR transp_anon ON COLUMN player.name 
IS 'MASKED WITH FUNCTION transp_anon.fake_last_name()';

ROLLBACK;
            

F.65.11.4. Обобщение #

Демонстрация использования функций обобщения.

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

CREATE TABLE patient (
    ssn TEXT,
    firstname TEXT,
    zipcode INTEGER,
    birth DATE,
    disease TEXT
);

INSERT INTO patient
VALUES
('253-51-6170','Alice',47012,'1989-12-29','Heart Disease'),
('091-20-0543','Bob',42678,'1979-03-22','Allergy'),
('565-94-1926','Caroline',42678,'1971-07-22','Heart Disease'),
('510-56-7882','Eleanor',47909,'1989-12-15','Acne'),
('098-24-5548','David',47905,'1997-03-04','Flu'),
('118-49-5228','Jean',47511,'1993-09-14','Flu'),
('263-50-7396','Tim',47900,'1981-02-25','Heart Disease'),
('109-99-6362','Bernard',47168,'1992-01-03','Asthma'),
('287-17-2794','Sophie',42020,'1972-07-14','Asthma'),
('409-28-2014','Arnold',47000,'1999-11-20','Diabetes')
;

SELECT * FROM patient;

CREATE MATERIALIZED VIEW generalized_patient AS
SELECT
    'REDACTED' AS firstname,
    transp_anon.generalize_int4range(zipcode,1000) AS zipcode,
    transp_anon.generalize_daterange(birth,'decade') AS birth,
    disease
FROM patient;

SELECT * FROM generalized_patient;

ROLLBACK;
            

F.65.11.5. Хеширование #

Это демонстрация функции хеширования Мы попытаемся псевдонимизировать естественный внешний ключ

BEGIN;

-- We have a simplistic customer table
CREATE TABLE customer (
id SERIAL,
name TEXT,
phone_number TEXT UNIQUE NOT NULL
);

INSERT INTO customer VALUES
(2046,'Omar Little','410-719-9009'),
(8123,'Russell Bell','410-617-7308'),
(3456,'Avon Barksdale','410-385-2983');

-- We have a log of their calls to our customer service hotline
CREATE TABLE hotline_call_history (
id SERIAL,
fk_phone_number TEXT REFERENCES customer(phone_number) DEFERRABLE,
call_start_time TIMESTAMP,
call_end_time TIMESTAMP
);

INSERT INTO hotline_call_history VALUES
(834,'410-617-7308','2004-05-17 09:41:01','2004-05-17 09:44:24'),
(835,'410-385-2983','2004-05-17 11:22:55','2004-05-17 11:34:18'),
(839,'410-719-9009','2004-05-18 16:02:03','2004-05-18 16:22:56'),
(878,'410-385-2983','2004-05-20 13:13:34','2004-05-18 13:51:00');

-- We can get a details view of the calls like this
SELECT
c.id    AS customer_id,
c.name  AS customer_name,
h.call_start_time,
h.call_end_time
FROM
hotline_call_history h
JOIN customer c ON h.fk_phone_number = c.phone_number
WHERE
extract(year from h.call_start_time) = 2004
ORDER BY h.call_start_time;

-- Now let's pseudonymize this !
CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

-- Init
SELECT transp_anon.init();
SET transp_anon.salt = '4ag3b803de6180361';
SET transp_anon.algorithm = 'md5';

-- SELECT transp_anon.set_secret_salt('4ag3b803de6180361');
-- SELECT transp_anon.set_secret_algorithm('md5');

-- Masking Rules
SECURITY LABEL FOR transp_anon ON COLUMN customer.name
IS 'MASKED WITH FUNCTION transp_anon.concat(
                                transp_anon.pseudo_first_name(name),
                                $$ $$,
                                transp_anon.pseudo_last_name(name))';

SECURITY LABEL FOR transp_anon ON COLUMN customer.phone_number
IS 'MASKED WITH FUNCTION transp_anon.hash(phone_number)';

SECURITY LABEL FOR transp_anon ON COLUMN hotline_call_history.fk_phone_number
IS 'MASKED WITH FUNCTION transp_anon.hash(fk_phone_number)';


-- Apply the masking rules
SET CONSTRAINTS ALL DEFERRED;
SELECT current_user \gset
SECURITY LABEL FOR transp_anon ON ROLE :current_user IS 'MASKED';

-- Launch the same request
SELECT
c.id    AS customer_id,
c.name  AS customer_name,
h.call_start_time,
h.call_end_time
FROM
hotline_call_history h
JOIN customer c ON h.fk_phone_number = c.phone_number
WHERE
extract(year from h.call_start_time) = 2004
ORDER BY h.call_start_time;

--
ROLLBACK;
            

F.65.11.6. Наследование правил маскирования #

Демонстрация анонимизации при наследовании

BEGIN;

CREATE TABLE public.invoice (
    name            text,
    amount          float,
    published_date  date
);

CREATE TABLE public.invoice_2020 (
    CHECK ( published_date >= DATE '2020-01-01'
        AND published_date <= DATE '2020-12-31' )
) INHERITS (invoice);

INSERT INTO invoice_2020
VALUES('John DOE', 236.25, '2020-12-09');

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
SELECT transp_anon.init();

SELECT current_user \gset
SECURITY LABEL FOR transp_anon ON ROLE :current_user IS 'MASKED';

--
-- Put mask on the mother table
--
SECURITY LABEL FOR transp_anon ON COLUMN public.invoice.name
IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';

SELECT public.invoice.name, public.invoice_2020.name
    FROM public.invoice, public.invoice_2020;

--      name     |     name     
-- --------------+--------------
--  CONFIDENTIAL | CONFIDENTIAL

--
-- Put mask on the child table
--
SECURITY LABEL FOR transp_anon ON COLUMN public.invoice_2020.name
IS 'MASKED WITH VALUE $$DELETED$$';

SELECT public.invoice.name, public.invoice_2020.name
    FROM public.invoice, public.invoice_2020;

--      name     |  name   
-- --------------+---------
--  CONFIDENTIAL | DELETED

ROLLBACK; 
            

F.65.11.7. Атака на снижение шума #

Это базовый пример повторных атак на функции маскировки noise(), когда они используются с динамической маскировкой. Мы продемонстрируем, как роль с маскировкой может угадать реальные данные, используя только представление маскировки

BEGIN;

-- STEP 1 : Activate the masking engine
CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;
SELECT transp_anon.init();

-- STEP 2: Declare a masked user
CREATE ROLE attacker LOGIN;
SECURITY LABEL FOR transp_anon ON ROLE attacker IS 'MASKED';

-- STEP 3: Load the data
DROP TABLE IF EXISTS people CASCADE;
CREATE TABLE people ( id INT, name TEXT, age INT);
INSERT INTO people VALUES (157, 'Mike Ehrmantraut' , 63);
INSERT INTO people VALUES (482, 'Jimmy McGill',  47);

-- STEP 4: Declare the masking rules
SECURITY LABEL FOR transp_anon ON COLUMN people.name
IS 'MASKED WITH VALUE $$CONFIDENTIAL$$ ';

SECURITY LABEL FOR transp_anon ON COLUMN people.age
IS 'MASKED WITH FUNCTION transp_anon.noise(age, 0.33)';

-- STEP 5: Now let's connect with the masked user

SELECT current_user \gset
SECURITY LABEL FOR transp_anon ON ROLE :current_user IS 'MASKED';

-- When attacker asks for the age of person #157, he/she gets a "noised" value
SELECT age FROM people WHERE id = 157;

-- Now let's do this 10000 times and get the average
DO
$$
DECLARE
v iNT;
a int[];
BEGIN
FOR i in 1..10000
LOOP
    SELECT age into v FROM people WHERE id=157;
    a:=array_append(a,v);
END LOOP;
SELECT avg(u) into v FROM unnest(a) u;
RAISE NOTICE 'Age of Person 157: %', v; END
$$;

ROLLBACK;  
            

F.65.11.8. Возвращает NULL или фиктивное значение #

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

SELECT transp_anon.init();


CREATE OR REPLACE FUNCTION null_or_random_date(val TIMESTAMPTZ)
RETURNS TIMESTAMPTZ
AS $$
SELECT transp_anon.random_date();
$$ LANGUAGE SQL VOLATILE RETURNS NULL ON NULL INPUT;

SELECT null_or_random_date(NULL);

SELECT null_or_random_date('2020-05-05');

ROLLBACK;
            

F.65.11.9. Частичное #

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

SELECT transp_anon.partial('abcdefgh',1,'xxxx',3);

SELECT transp_anon.partial('+33142928100',4,'******',2);

SELECT transp_anon.partial_email('[email protected]');

ROLLBACK;
            

F.65.11.10. Различие фальшивых функций #

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

SELECT transp_anon.init();

--let's use `TEMPORARY` instead of `pg_temp` for clarity
--SET search_path TO pg_temp, public;

CREATE TEMPORARY TABLE customer(
id SERIAL,
full_name TEXT,
hair_color TEXT,
birth DATE,
employer TEXT,
zipcode TEXT,
fk_shop INTEGER
);

INSERT INTO customer
VALUES
(911,'Chuck Norris','brown','1940-03-10','Texas Rangers', '75001',12),
(312,'David Hasselhoff','black','1952-07-17','Baywatch', '90001',423)
;

SELECT * FROM customer;

UPDATE customer
SET
full_name=transp_anon.fake_first_name() || ' ' || transp_anon.fake_last_name(),
hair_color=transp_anon.random_in(ARRAY['black','blonde','brown','red',NULL]),
birth=transp_anon.random_date_between('1920-01-01'::DATE,now()),
employer=transp_anon.fake_company(),
zipcode=transp_anon.random_zip()
;

SELECT * FROM customer;

ROLLBACK;
            

F.65.11.11. Напишите свою маску #

BEGIN;

CREATE EXTENSION IF NOT EXISTS transp_anon CASCADE;

SELECT transp_anon.init();

CREATE TABLE company (
business_name TEXT,
info JSONB
);

INSERT INTO company
VALUES ( 'Soylent Green',
'{"employees":[
    {"firstName":"John", "lastName":"Doe"},
    {"firstName":"Anna", "lastName":"Smith"},
    {"firstName":"Peter", "lastName":"Jones"}
]}');

SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';

CREATE FUNCTION transp_anon.anonymize_last_name(j JSONB)
RETURNS JSONB
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
json_build_object(
    'employees' ,
    array_agg(
    jsonb_set(e ,'{lastName}', to_jsonb(transp_anon.fake_last_name()))
    )
)::JSONB
FROM jsonb_array_elements( j->'employees') e
$func$;

SELECT transp_anon.anonymize_last_name(info) FROM company;

SECURITY LABEL FOR transp_anon ON COLUMN company.info
IS 'MASKED WITH FUNCTION transp_anon.anonymize_last_name(info)';

SELECT current_user \gset
SECURITY LABEL FOR transp_anon ON ROLE :current_user IS 'MASKED';

SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';

ROLLBACK;