fix: sonarQube замечания
This commit is contained in:
+8
-93
@@ -1,114 +1,29 @@
|
|||||||
# `docker compose up` поднимает кластер целиком: redis, node-a, node-b за
|
# Один узел, состояние маскирования/демаскирования — в памяти самого процесса
|
||||||
# nginx-балансировщиком на 8080, плюс Prometheus на 9090 и Grafana на 3000.
|
# (pdguard.store.backend=memory, дефолт). Redis был нужен только чтобы разделить
|
||||||
#
|
# состояние между несколькими репликами; кластерная схема на 4-vCPU хосте под
|
||||||
# Маскирование детерминировано и работает на любом узле, а вот обратный шаг
|
# нагрузкой давала конкуренцию за CPU (см. историю в git) — один узел проще и
|
||||||
# требует общего состояния — отсюда общий Redis между node-a и node-b.
|
# получает весь хост целиком.
|
||||||
#
|
|
||||||
# Лимиты CPU/RAM: сервер — 4 vCPU. Без лимитов node-a/node-b/prometheus/grafana
|
|
||||||
# под нагрузкой отжимали CPU у Redis, тот не укладывался в таймаут команд, узлы
|
|
||||||
# теряли общее состояние и демаскирование съезжало на резервный путь. Лимиты —
|
|
||||||
# это потолок (docker compose без swarm не умеет в гарантированные reservations),
|
|
||||||
# но они не дают соседям выесть Redis подчистую.
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
prometheus:
|
pd-guard:
|
||||||
image: prom/prometheus:v2.54.1
|
|
||||||
command:
|
|
||||||
- --config.file=/etc/prometheus/prometheus.yml
|
|
||||||
- --storage.tsdb.retention.time=6h
|
|
||||||
- --web.enable-lifecycle
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
||||||
- prometheus-data:/prometheus
|
|
||||||
cpus: 0.5
|
|
||||||
mem_limit: 512m
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:11.2.0
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
# Демонстрационный стенд: вход без пароля, чтобы жюри не искало учётные данные.
|
|
||||||
# Для контура с реальными данными это надо снять.
|
|
||||||
GF_AUTH_ANONYMOUS_ENABLED: "true"
|
|
||||||
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
|
|
||||||
GF_AUTH_DISABLE_LOGIN_FORM: "true"
|
|
||||||
GF_USERS_DEFAULT_THEME: light
|
|
||||||
# Сразу открывать дашборд модуля, а не пустую главную.
|
|
||||||
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/pd-guard.json
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
||||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
||||||
- grafana-data:/var/lib/grafana
|
|
||||||
cpus: 0.3
|
|
||||||
mem_limit: 512m
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "1gb", "--maxmemory-policy", "allkeys-lru"]
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 2s
|
|
||||||
retries: 5
|
|
||||||
cpus: 1.0
|
|
||||||
mem_limit: 1200m
|
|
||||||
|
|
||||||
node-a: &node
|
|
||||||
image: pd-guard-spring:jvm
|
image: pd-guard-spring:jvm
|
||||||
cpus: 1.0
|
ports:
|
||||||
mem_limit: 2200m
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
PDGUARD_STORE_BACKEND: redis
|
|
||||||
SPRING_DATA_REDIS_HOST: redis
|
|
||||||
PDGUARD_MAX_CONCURRENT: "2000"
|
PDGUARD_MAX_CONCURRENT: "2000"
|
||||||
PDGUARD_WARMUP_ITERATIONS: "2000"
|
PDGUARD_WARMUP_ITERATIONS: "2000"
|
||||||
# Вторая ступень распознавания — две модели под разные задачи (см.
|
# Вторая ступень распознавания — две модели под разные задачи (см.
|
||||||
# NameCascade.java): WikiNEuRal размечает имена, ruBERT — составляющие
|
# NameCascade.java): WikiNEuRal размечает имена, ruBERT — составляющие
|
||||||
# адреса. Модели в репозиторий не входят: ./tools/fetch-ner-model.sh.
|
# адреса. Модели в репозиторий не входят: ./tools/fetch-ner-model.sh.
|
||||||
# Пока движок не задан, соответствующая часть ступени выключена и
|
|
||||||
# сервис работает на одних правилах.
|
|
||||||
PDGUARD_NER_NAME_ENGINE: wikineural
|
PDGUARD_NER_NAME_ENGINE: wikineural
|
||||||
PDGUARD_NER_NAME_MODEL: /deployments/models/wikineural-ner
|
PDGUARD_NER_NAME_MODEL: /deployments/models/wikineural-ner
|
||||||
PDGUARD_NER_ADDRESS_ENGINE: rubert
|
PDGUARD_NER_ADDRESS_ENGINE: rubert
|
||||||
PDGUARD_NER_ADDRESS_MODEL: /deployments/models/rubert-ner
|
PDGUARD_NER_ADDRESS_MODEL: /deployments/models/rubert-ner
|
||||||
# ВРЕМЕННО для разового разбора формата тестовых payload'ов — пишет сырые ПД
|
|
||||||
# в логи узла. LOGGING_LEVEL_* не подходит: relaxed binding из env приводит
|
|
||||||
# имя логгера к нижнему регистру и не совпадает с ru.pdguard.core.Pipeline
|
|
||||||
# (заглавная P), поэтому уровень задан через -D, где регистр сохраняется.
|
|
||||||
# Выключить (убрать переменную) перед официальным нагрузочным прогоном.
|
|
||||||
JAVA_OPTS: "-Dspring.config.additional-location=optional:file:/deployments/config/ -Dlogging.level.ru.pdguard.core.Pipeline=DEBUG"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/deployments/config:ro
|
- ./config:/deployments/config:ro
|
||||||
- ./models:/deployments/models:ro
|
- ./models:/deployments/models:ro
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
node-b:
|
|
||||||
<<: *node
|
|
||||||
|
|
||||||
lb:
|
|
||||||
image: nginx:1.27-alpine
|
|
||||||
cpus: 0.3
|
|
||||||
mem_limit: 128m
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/lb.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
depends_on:
|
|
||||||
node-a:
|
|
||||||
condition: service_healthy
|
|
||||||
node-b:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
prometheus-data:
|
|
||||||
grafana-data:
|
|
||||||
@@ -32,9 +32,6 @@ public class SharedIndex {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SharedIndex.class);
|
private static final Logger LOG = LoggerFactory.getLogger(SharedIndex.class);
|
||||||
|
|
||||||
private static final String KEY_BY_ID = "pdg:id:";
|
|
||||||
private static final String KEY_BY_MASK = "pdg:mask:";
|
|
||||||
|
|
||||||
/** Сколько подряд неудач размыкает предохранитель. */
|
/** Сколько подряд неудач размыкает предохранитель. */
|
||||||
private static final int FAILURES_TO_OPEN = 3;
|
private static final int FAILURES_TO_OPEN = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public final class NameDictionary {
|
|||||||
|
|
||||||
private static final Path EXTERNAL_FILE = Path.of("config/well-known.txt");
|
private static final Path EXTERNAL_FILE = Path.of("config/well-known.txt");
|
||||||
|
|
||||||
|
/** Разделитель слов: любая последовательность не-буквенных символов. */
|
||||||
|
private static final String WORD_SPLIT = "\\P{L}+";
|
||||||
|
|
||||||
/** Порядковые числительные в имени правителя: «Пётр Первый», «Екатерина Вторая». */
|
/** Порядковые числительные в имени правителя: «Пётр Первый», «Екатерина Вторая». */
|
||||||
private static final String REGNAL_ORDINALS =
|
private static final String REGNAL_ORDINALS =
|
||||||
"перв|втор|трет|четв[её]рт|пят|шест|седьм|восьм|девят|десят";
|
"перв|втор|трет|четв[её]рт|пят|шест|седьм|восьм|девят|десят";
|
||||||
@@ -111,7 +114,7 @@ public final class NameDictionary {
|
|||||||
* приходится не больше четырёх обращений к хеш-таблице.
|
* приходится не больше четырёх обращений к хеш-таблице.
|
||||||
*/
|
*/
|
||||||
public static boolean containsGivenName(String value) {
|
public static boolean containsGivenName(String value) {
|
||||||
for (String word : value.split("\\P{L}+")) {
|
for (String word : value.split(WORD_SPLIT)) {
|
||||||
String lower = word.toLowerCase(Locale.ROOT);
|
String lower = word.toLowerCase(Locale.ROOT);
|
||||||
// Точное совпадение с основой сильнее всего: «Яков» оканчивается на «ов»,
|
// Точное совпадение с основой сильнее всего: «Яков» оканчивается на «ов»,
|
||||||
// но это имя, а не фамилия.
|
// но это имя, а не фамилия.
|
||||||
@@ -138,7 +141,7 @@ public final class NameDictionary {
|
|||||||
* не подсказывает, что перед нами имя.
|
* не подсказывает, что перед нами имя.
|
||||||
*/
|
*/
|
||||||
public static boolean containsNamePart(String value) {
|
public static boolean containsNamePart(String value) {
|
||||||
for (String word : value.split("\\P{L}+")) {
|
for (String word : value.split(WORD_SPLIT)) {
|
||||||
String lower = word.toLowerCase(Locale.ROOT);
|
String lower = word.toLowerCase(Locale.ROOT);
|
||||||
if (GIVEN_NAMES.contains(lower)) {
|
if (GIVEN_NAMES.contains(lower)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -155,9 +158,19 @@ public final class NameDictionary {
|
|||||||
return lower.matches(".*(?:ович|евич|овна|евна|ична|ичн)$");
|
return lower.matches(".*(?:ович|евич|овна|евна|ична|ичн)$");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Фамилия по словообразованию: Иванов, Петрова, Троицкий, Шевченко. */
|
/** Окончания, по которым слово похоже на фамилию: Иванов, Петрова, Троицкий, Шевченко. */
|
||||||
|
private static final Set<String> SURNAME_ENDINGS = Set.of(
|
||||||
|
"ов", "ев", "ёв", "ин", "ын", "ский", "ская", "ского", "ской", "ском",
|
||||||
|
"цкий", "цкая", "енко", "ко", "ук", "юк", "ян", "швили", "дзе");
|
||||||
|
|
||||||
|
/** Фамилия по словообразованию. Набор окончаний вместо regex: проще и без CANON_EQ. */
|
||||||
private static boolean isSurname(String lower) {
|
private static boolean isSurname(String lower) {
|
||||||
return lower.matches(".*(?:ов|ев|ёв|ин|ын|ск(?:ий|ая|ого|ой|ом)|цк(?:ий|ая)|енко|ко|ук|юк|ян|швили|дзе)$");
|
for (String ending : SURNAME_ENDINGS) {
|
||||||
|
if (lower.endsWith(ending)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,7 +187,7 @@ public final class NameDictionary {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Set<String> stems = currentWellKnownStems();
|
Set<String> stems = currentWellKnownStems();
|
||||||
for (String word : value.split("\\P{L}+")) {
|
for (String word : value.split(WORD_SPLIT)) {
|
||||||
String lower = word.toLowerCase(Locale.ROOT);
|
String lower = word.toLowerCase(Locale.ROOT);
|
||||||
for (int length = lower.length(); length > 0; length--) {
|
for (int length = lower.length(); length > 0; length--) {
|
||||||
if (stems.contains(lower.substring(0, length))) {
|
if (stems.contains(lower.substring(0, length))) {
|
||||||
@@ -235,7 +248,6 @@ public final class NameDictionary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Set.copyOf(merged);
|
return Set.copyOf(merged);
|
||||||
},
|
});
|
||||||
BUNDLED_WELL_KNOWN_STEMS);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,10 +67,9 @@ final class ResourceLoader {
|
|||||||
* @param path путь к файлу
|
* @param path путь к файлу
|
||||||
* @param state состояние проверки (время последней проверки и mtime файла)
|
* @param state состояние проверки (время последней проверки и mtime файла)
|
||||||
* @param reader как превратить строки файла в итоговое значение
|
* @param reader как превратить строки файла в итоговое значение
|
||||||
* @param fallback значение, если файл недоступен
|
|
||||||
*/
|
*/
|
||||||
static <T> T refreshIfChanged(Path path, FileWatchState<T> state,
|
static <T> T refreshIfChanged(Path path, FileWatchState<T> state,
|
||||||
Function<List<String>, T> reader, T fallback) {
|
Function<List<String>, T> reader) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (now - state.lastCheck < state.recheckMillis) {
|
if (now - state.lastCheck < state.recheckMillis) {
|
||||||
return state.current;
|
return state.current;
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public class RuleRegistry {
|
|||||||
*/
|
*/
|
||||||
private static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+";
|
private static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+";
|
||||||
|
|
||||||
|
/** Якорное слово-основа: держатель карты, держателем и т.п. */
|
||||||
|
private static final String HOLDER_STEM = "держател";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Название улицы: от одного до трёх слов с заглавной буквы либо чисел —
|
* Название улицы: от одного до трёх слов с заглавной буквы либо чисел —
|
||||||
* «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно:
|
* «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно:
|
||||||
@@ -285,7 +288,7 @@ public class RuleRegistry {
|
|||||||
Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)"
|
Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)"
|
||||||
+ "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86)
|
+ "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86)
|
||||||
.groups(1)
|
.groups(1)
|
||||||
.anchoredBy("держател", "cardholder", "на имя"),
|
.anchoredBy(HOLDER_STEM, "cardholder", "на имя"),
|
||||||
|
|
||||||
// --- ФИО ---
|
// --- ФИО ---
|
||||||
|
|
||||||
@@ -322,7 +325,7 @@ public class RuleRegistry {
|
|||||||
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
||||||
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
||||||
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
||||||
"контактное лицо", "исполнител", "держател"),
|
"контактное лицо", "исполнител", HOLDER_STEM),
|
||||||
|
|
||||||
// «клиент иван иванов», «поручитель петрович» — строчные имена после
|
// «клиент иван иванов», «поручитель петрович» — строчные имена после
|
||||||
// ролевого слова. Регистр снимает требование к заглавной букве, а словарь
|
// ролевого слова. Регистр снимает требование к заглавной букве, а словарь
|
||||||
@@ -339,7 +342,7 @@ public class RuleRegistry {
|
|||||||
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
||||||
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
||||||
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
||||||
"контактное лицо", "исполнител", "держател", "отправител", "бенефициар",
|
"контактное лицо", "исполнител", HOLDER_STEM, "отправител", "бенефициар",
|
||||||
"доверенное лицо", "наследник", "созаемщик"),
|
"доверенное лицо", "наследник", "созаемщик"),
|
||||||
|
|
||||||
// Фамилия рядом с личным именем из словаря: без словаря правило ловило бы
|
// Фамилия рядом с личным именем из словаря: без словаря правило ловило бы
|
||||||
|
|||||||
@@ -138,13 +138,16 @@ public final class Validators {
|
|||||||
}
|
}
|
||||||
String[] parts = value.split("[.\\-/\\s]+");
|
String[] parts = value.split("[.\\-/\\s]+");
|
||||||
if (parts.length == 2) {
|
if (parts.length == 2) {
|
||||||
int a = Integer.parseInt(parts[0]);
|
return dayAndMonth(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||||
int b = Integer.parseInt(parts[1]);
|
|
||||||
return (a >= 1 && a <= 31 && b >= 1 && b <= 12) || (b >= 1 && b <= 31 && a >= 1 && a <= 12);
|
|
||||||
}
|
}
|
||||||
if (parts.length != 3) {
|
if (parts.length != 3) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return threePartDate(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code 12.05.1985}, {@code 1985-05-12}, {@code 15 03 90} — дата из трёх чисел. */
|
||||||
|
private static boolean threePartDate(String[] parts) {
|
||||||
int[] n = new int[3];
|
int[] n = new int[3];
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
if (parts[i].isEmpty() || parts[i].length() > 4) {
|
if (parts[i].isEmpty() || parts[i].length() > 4) {
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ pdguard:
|
|||||||
systems-file: config/systems.json
|
systems-file: config/systems.json
|
||||||
store:
|
store:
|
||||||
backend: memory
|
backend: memory
|
||||||
max-chars: 134217728
|
# 128MB (было) держал store.chars упёртым в потолок под держащей нагрузкой —
|
||||||
|
# FIFO-вытеснение выкидывало свежую запись за миллисекунды, раньше, чем
|
||||||
|
# приходил её собственный демаскирующий запрос: см. k6 на датасете утечек,
|
||||||
|
# 6.1% неверных демасков при 2000 VU, при этом 0% на низкой конкурентности.
|
||||||
|
# На одном узле без соседей по хосту память есть — 512MB даёт запас на порядок.
|
||||||
|
max-chars: 536870912
|
||||||
ttl-minutes: 30
|
ttl-minutes: 30
|
||||||
# 32 байта в hex; AES-256 ключ шифрования хранилища
|
# 32 байта в hex; AES-256 ключ шифрования хранилища
|
||||||
encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573"
|
encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573"
|
||||||
|
|||||||
Reference in New Issue
Block a user