GDPR и маскиране на текст.

Статус
Not open for further replies.

dakata__92

Super Moderator
Необходимо ми е да оптимизирам клас, който да скрива чувствително съдържание в различен набор от текстове. Идеята е да се крият пароли, банкови сметки и прочие в текст, преди и след ключова дума. Маскира се при визуализиране на съдържанието. Сегашният код е дървен, неефективен и бавен. Търся дългосрочно и по-добро решение на проблема. Ако някой използва подобен код, ще се радвам да видя как е решил проблема.


PHP:
<?php

class Gdpr extends Model
{
    private array $patterns = [
        'pass',
        'password',
        'parola',
        'парола',
        'code',
        'kod',
        'код',
        'iban',
        'card',
        'karta',
        'карта',
        'pin',
        'пин',
        'smetka',
        'сметка',
    ];

    public function __construct()
    {
        $this->isActiveForUser = auth()->user()->gdpr == 1;
    }

    /**
     * Masking content when previewing.
     *
     * @param string $string
     * @param string $character
     * @param string $encoding
     * @return string
     */
    public function mask(
        string $string,
        string $character = '*',
        string $encoding = 'UTF-8'
    ): string
    {
        if (empty($string) || !$this->isActiveForUser) {
            return $string;
        }
        return $this->maskPatterns($string, $character, $encoding);
    }

    /**
     * @param string $string
     * @param string $character
     * @param string $encoding
     * @return string
     */
    public function maskPatterns(

        string $string,
        string $character = '*',
        string $encoding = 'UTF-8'
    ): string
    {
        $mask = $string;
        if (empty($string) || !$this->isActiveForUser) {
            return $mask;
        }
        $positions = [];
        foreach ($this->getPatterns() ?? [] as $pattern) {
            if (($firstPatternPosition = mb_stripos($string, $pattern, 0, $encoding)) !== false) {
                $positions[$firstPatternPosition] = $pattern;
            }
            if (($lastPatternPosition = mb_strripos($string, $pattern, 0, $encoding)) !== false) {
                $positions[$lastPatternPosition] = $pattern;
            }
        }
        if (count($positions) == 0) {
            return $mask;
        }
        ksort($positions);
        $mask = preg_replace('/\d/ui', $character, $mask); // Hide all digits.
        $space = ' ';
        $stringLength = mb_strlen($string, $encoding);
        foreach ($positions as $position => $pattern) {
            //Right
            $patternLength = mb_strlen($pattern, $encoding);
            $index = $position + $patternLength; //Adjust start index to not hide pattern
            $length = (int)round($stringLength / 10); //Default length after start index
            $nextPattern = next($positions);
            $nextPosition = key($positions);
            if ($nextPosition && $nextPattern) {
                //Correction with next pattern
                $length = ($index + $length) >= $nextPosition ? $nextPosition - $index : $length;
            }
            if ($space == mb_substr($string, ($index + $length) - 1, 1, $encoding)) {
                $length -= 1; //Correction for space to sow and break
            }
            $mask = Str::mask($mask, $character, $index, $length, $encoding);
        }
        return $mask;
    }

    /**
     * @return array
     */
    public function getPatterns(): array
    {
        return $this->patterns;
    }

    /**
     * @param array $patterns
     */
    public function setPatterns(array $patterns): void
    {
        $this->patterns = $patterns;
    }
}
 
А сегашното решение работи ли ти, както искаш? Защото като гледам криеш само първото и последното срещане на всеки шаблон. Ако имаш три пъти "код", ще пропуснеш второто.

PHP:
foreach ($this->getPatterns() ?? [] as $pattern) {
    if (($firstPatternPosition = mb_stripos($string, $pattern, 0, $encoding)) !== false) {
        $positions[$firstPatternPosition] = $pattern;
    }
    if (($lastPatternPosition = mb_strripos($string, $pattern, 0, $encoding)) !== false) {
        $positions[$lastPatternPosition] = $pattern;
    }
}
 
Не знам какъв текст се маскира, но шаблоните са прекалено конкретни. Ако дадена дума е членувана или последвана от съюз, то тогава маскирането ще фейлне.

Такива случаи искаш ли да покриваш?
 
То ако иска по-надеждно решение, ще трябва доста да помисли, а и добре да си познава данните. Например ако паролата ми е следната: парола, то може би ще се очаква решението да скрие не "ми", "ми е", или "ми е следната", ами чак "парола"?
 
Зависи от изпълнението е много вероятно да не трябва да минава цял текст през маскиране, а при извличане на данните, които образуват дадения текст директно да ги маскира. Но тук вероятно говорим за текст от имейли и смс-и, които са вече запазени с хардкоднати данни вместо преобразуване на темплейт.
 
Текста е с дължина не повече от 1000 символа. Няма определен шаблон по който се праща информацията, защото е генерирана от клиент. Информацията се крие само при визуализиране, не при въвеждане в базата с данни. Идеята е да огранича по-чувствителната информация, за определени клиенти, група клиенти или роля. Сегашното решение съм го писал преди доста време и е адаптирано към ситуацията, но е грубо и тежко.
Съобщенията могат да съдържат пароли, удостоверителни кодове, банкови сметки, ЕГН-та и прочие. Искам да се опитам максимално, да обхвана повече чувствителна информация, като се запази дължината на текста. Тоест ако стринга който трябва да се замени е с дължина 10 символа, то трябва да се замени с 10 звездички (примерно).
 
Вариант ми е да направя нещо такова:

PHP:
public function maskPatterns(

        string $string,
        string $character = '*',
        string $encoding = 'UTF-8'
    ): string
    {
        $mask = $string;
        if (empty($string) || !$this->isActiveForUser) {
            return $mask;
        }
        $patterns = [
            '/(\d{2})(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])(\d{4})/', // Mask EGN
        ];
        foreach ($patterns as $pattern) {
            $mask = preg_replace_callback($pattern, function ($matches) use ($character, $encoding) {
                $match = $matches[0] ?? '';
                $length = mb_strlen($match, $encoding);
                return Str::mask($match, $character, 0, $length, $encoding);

            }, $mask);
        }
        return $mask;
    }
 
Поне можеш ли да следваш даден формат на информацията, която се опитваш да маскираш. Например като срещнеш дадена дума, да речем "парола" то след това да ровиш за стринг, който е между определена дължина, съдържа знаци или числа (каквито валидации изисквате).
Ако е банкова сметка примерно да е винаги във формат с разстояния или без "0000 0000 0000 0000".

Ако можеш да следваш формат, то тогава си напиши мачъри и тествай според това какво засечеш. Ако има съвпадение разменяш думата с маската и продължаваш напред.

Например: "Вашата парола е Pa$$w0rd.12"
Четеш си стринга подред и засичаш дума, която смяташ, че може да има чувствителна информация. Флагваш я и продължаваш напред и се опитваш да задействаш мачър за пароли. Ако мачнеш нещо маскираш, махаш флага и продължаваш напред до следващата дума.

Със сигурност не е идеално и покритието не може да се гарантира да е на 100%, но е нещо.

Ако имам свободно време може да се опитам да съставя нещо.

Ако можеш все пак да споделиш ориентир за перформънс, ще е добре. Поне да следвам дадено време на 1000 думи.
 
То проблема е че текста не го съставям аз, а е въведен в свободен формат от клиентите. Драснах това поне на този етап е доста по-бързо и обхващам, част от казусите.

PHP:
private function replacePatterns(
        string $string,
        string $character = '*',
        string $encoding = 'UTF-8'
    ): string
    {
        //Add default search for space or : (before|after).
        $fnRegex = fn(
            string $regex,
            string $flags = '',
            string $lookAfter = '(?<=[\s:]|^)',
            string $lookBefore = '(?=[\s:.,]|$)',
        ) => "/$lookAfter($regex)$lookBefore/$flags";
        $patterns = [
            //Replace between 3 and 10 digits.
            $fnRegex('\d{3,10}') => [
                'group' => 1,
            ],
            //Replace any word with at least one digit (AXZK5W, kZK5vW).
            $fnRegex('((?=\w*\d)\w{6})', 'i') => [
                'group' => 1,
            ],
            //Replace IBAN
            $fnRegex('[a-z]{2}\d{2}[a-z\d]{1,30}', 'i') => [
                'group' => 1,
            ],
            //Replace EGN.
            $fnRegex('(\d{2})(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])(\d{4})') => [
                'group' => 1,
            ],
        ];
        foreach ($patterns as $pattern => $patternOptions) {
            $group = $patternOptions['group'] ?? 0;
            $string = preg_replace_callback(
                $pattern,
                function ($matches) use ($character, $encoding, $group) {
                    $subject = $matches[0] ?? '';
                    $search = $matches[$group] ?? '';
                    $replace = str_repeat($character, mb_strlen($search, $encoding));
                    return str_replace($search, $replace, $subject);
                },
                $string
            );
        }

        return $string;
    }
 
Последно редактирано:
А той е изцяло свободен текст. Разбрах!

Между другото, първия ти шаблон ще покрие шаблона за ЕГН.
 
А той е изцяло свободен текст. Разбрах!

Между другото, първия ти шаблон ще покрие шаблона за ЕГН.
Първоначално бе от 3 до 8 символа но го вдигнах на 10 и не съм се усетил. Благодаря за поправката.
 
Сегашното решение съм го писал преди доста време и е адаптирано към ситуацията, но е грубо и тежко.
При такъв малък вход виждаш performance проблем? Да бяха големи данни, да пробваш с in-place замяна (чета че substr_replace прави копие на стринга, демек ако го правиш много пъти, може да товари), но за хиляда символа ми се струва странно.

Три пъти мери, един път режи! :)
 
При такъв малък вход виждаш performance проблем? Да бяха големи данни, да пробваш с in-place замяна (чета че substr_replace прави копие на стринга, демек ако го правиш много пъти, може да товари), но за хиляда символа ми се струва странно.

Три пъти мери, един път режи! :)
На места имам странициране, което при примерно 100 резултата ще забави с някакво време. Затова търся по-оптимизиран вариант.
 
В такъв случай колкото и да оптимизираш ще имаш забавяне при зареждане. Обмисли вариант да си рънваш крон, който ти анонимизира данните и ги запазва в такъв формат в базата данни. Добави и един флаг, за да знаеш, че даден текст е минал маскирането.

След това зареждай директно от базата данни.

Проблем при този подход ще е, ако по-късно решиш, че имаш по-добро маскиране и искаш отново да ги минеш. В такъв случай най-лесното е да ресетнеш флага и да оставиш крон задачата да ги мине наново.
 
В такъв случай колкото и да оптимизираш ще имаш забавяне при зареждане. Обмисли вариант да си рънваш крон, който ти анонимизира данните и ги запазва в такъв формат в базата данни. Добави и един флаг, за да знаеш, че даден текст е минал маскирането.

След това зареждай директно от базата данни.

Проблем при този подход ще е, ако по-късно решиш, че имаш по-добро маскиране и искаш отново да ги минеш. В такъв случай най-лесното е да ресетнеш флага и да оставиш крон задачата да ги мине наново.
Прекалено много таблици и релации са. Не мога да дублирам базата, само заради единият GDPR и то пуснат само за някои клиенти. Няма да забави кой знае колко зареждането на страниците, че да му се притеснявам и да вдигам кронове и прочие. Говорим за промяна и на архивни данни и така нататък. Затова най-лесното решение е при визуализиране да става и то динамично. Търся повече регулярни изрази с които да маскирам, повече неща без да засягам много много текста.
 
Леко си противоречиш с постовете.
С кое си противореча?
Обобщение:
Опитвам се да оптимизирам класът и да подобря мачването на кода. Попитахте ме какъв текст ще обработва, основно до 1000 символа. Това, че на места имам странициране не променя много факта, че на всяка итерация ще обработва до 1000 символа (мога да кеширам резултата при пагинация). Съдържанието на текста се въвежда от клиенти и се опитвам да хвана максимално много чувствителна информация, която да заменя с *. Таблиците са на месечна разбивка, като средно всяка за месеца е с между 10 и 20 милиона записа. Кода ще се използва само при визуализиране от базата с данни.
 
Първо това
На места имам странициране, което при примерно 100 резултата ще забави с някакво време. Затова търся по-оптимизиран вариант.
а след това казваш това
Няма да забави кой знае колко зареждането на страниците, че да му се притеснявам и да вдигам кронове и прочие.

Това е противоречене. Хем те притеснява скоростта, хем не те.
 
Първо това

а след това казваш това


Това е противоречене. Хем те притеснява скоростта, хем не те.
Ако сравниш стартовият код, който съм пуснал и последната версия тук, ще откриеш, че подобрението в работата е огромно. Нямам възможността да сменям концепцията, просто защото твоето предложение с кроновете не е приложимо за проекта. Търся оптимизация на кода и хващане на повече чувствителна информация, която да се скрива.
Опитвам се да оптимизирам кода и неговата скорост на замяна, но не ми е чак до толкова проблемно, че да стигам до кронове.
 
Статус
Not open for further replies.

Back
Горе