CodeGym /Kursy /ChatGPT Apps /Audit & lifecycle: dzienniki audytu, retencja danych,...

Audit & lifecycle: dzienniki audytu, retencja danych, usuwanie na żądanie, continuity/backups

ChatGPT Apps
Poziom 15 , Lekcja 4
Dostępny

1. Po co wam w ogóle audit & lifecycle w ChatGPT App

Gdy piszecie prototyp, użytkownikiem jesteście wy sami, baza danych to lokalny SQLite, a „incydenty” leczy git reset --hard — wszystko wydaje się miłe i domowe.

Ale gdy tylko wasz GiftGenius (lub inna aplikacja ChatGPT (App)) zyskuje prawdziwych użytkowników, zwłaszcza z płatnościami i PII, nagle pojawiają się:

  • specjaliści ds. bezpieczeństwa klienta z pytaniem „kto może zobaczyć nasze zamówienia i kto je zmieniał?”;
  • prawnicy z pytaniem „jak długo przechowujecie dane i jak realizujecie żądanie ‘usuń mnie’?”;
  • rzeczywistość produkcyjna z pytaniem „co będzie, jeśli deweloper zrzuci tabelę na produkcji?”

W tym wykładzie omówimy cztery filary:

  1. Audit‑logi — oddzielna warstwa logowania dla bezpieczeństwa i audytu.
  2. Data retention — okresy życia różnych typów danych i jak je wdrożyć.
  3. Usuwanie na żądanie użytkownika — „prawo do bycia zapomnianym” w praktyce.
  4. Business continuity & backups — jak przetrwać awarie, nie tracąc twarzy i danych.

Wszystkie przykłady będziemy, o ile to możliwe, wiązać z naszą szkoleniową aplikacją (warunkowy GiftGenius na Next.js + Apps SDK + MCP).

2. Audit‑logi: kto, co, kiedy i z jakim wynikiem

Czym dzienniki audytu różnią się od zwykłych logów

Zwykłe application‑logi — to przyjazne komunikaty dla dewelopera. Żyją w nich stack trace’y, informacje debug, dziwne wartości zmiennych, testowe console.log("tutaj na pewno nie powinno być null"). Żyją krótko i są czytane przez inżynierów.

Audit‑logi — to inna galaktyka. Ich główna publiczność to specjaliści bezpieczeństwa, audytorzy, czasem prawnicy. Nie potrzebują wiersza „NullPointer na 55. linii”, potrzebują zapisu w stylu „użytkownik X zmienił ustawienia płatności organizacji Y o tej i o tej godzinie, wynik — sukces”. Zapisy audytowe zwykle żyją znacznie dłużej (lata) i są dowodem w przypadku dochodzeń.

Kluczowe różnice:

Cecha Application Logs Audit Logs
Cel Debugowanie, diagnostyka Bezpieczeństwo, compliance, dochodzenia
Odbiorcy Deweloperzy, SRE Specjaliści ds. bezpieczeństwa, prawnicy, czasem regulatorzy
Zawartość danych Szczegóły techniczne, stack trace Kto/co/kiedy/do jakiego zasobu/z jakim wynikiem
Okres przechowywania Tygodnie–miesiąc Lata (często ≥ 1 roku)
Operacje na logach Można usuwać/nadpisywać Preferowany tryb append‑only, bez UPDATE/DELETE

OWASP i podobne wytyczne podkreślają: audit‑logi lepiej trzymać w oddzielnym magazynie lub tabeli, a nie mieszać ze zwykłymi logami aplikacji.

Co logować w kontekście aplikacji ChatGPT

Dla aplikacji ChatGPT, zwłaszcza z komercją, rozsądne minimum audytu to:

  • zdarzenia uwierzytelniania: logowanie, wylogowanie, próby wejścia;
  • operacje na krytycznych danych: tworzenie/aktualizacja/usuwanie profili, zamówień, ustawień płatności;
  • działania administracyjne: zmiana ról, zmiana ustawień dzierżawy (tenanta);
  • wywołania wrażliwych narzędzi MCP/Agents: create_order, charge_customer, cancel_subscription itp.

Dobra intuicja: wszystko, o co przy incydencie będziecie chcieli zapytać w stylu „kto to zrobił i przez co?”, powinno trafiać do audytu.

Struktura zdarzenia audytu

Wygodny model mentalny: każdy wpis to „kto / jakie działanie / na czym / w jakim kontekście / z jakim wynikiem”. Często formułuje się to jako strukturę who, action, resource, context, outcome.

Dla naszego GiftGenius opiszemy interfejs w TypeScript:

// lib/audit.ts
export type AuditAction =
  | "auth.login"
  | "auth.logout"
  | "order.create"
  | "order.cancel"
  | "account.delete"
  | "giftidea.generate";

export interface AuditEvent {
  eventId: string;          // uuid
  timestamp: string;        // ISO
  actor: {
    userId: string | null;  // może być null przed logowaniem
    tenantId?: string | null;
    ip?: string | null;
    client: "chatgpt-app" | "admin-panel" | string;
  };
  action: AuditAction;
  resource?: {
    type: string;           // "order", "user", ...
    id?: string;
  };
  context?: {
    mcpTool?: string;
    requestId?: string;
  };
  outcome: {
    status: "success" | "failure";
    reason?: string | null;
  };
}

Zauważcie, że do zdarzenia nie trafiają pełne adresy e‑mail, numery kart i inne PII, których w poprzednich wykładach uczyliśmy się maskować i nie logować bez potrzeby.

Gdzie i jak przechowywać audit‑logi

Minimalne wymagania dla magazynu:

  • oddzielna tabela lub nawet oddzielna baza od zwykłych logów, aby trudniej było coś przypadkiem nadpisać;
  • w miarę możliwości tryb append‑only: technicznie może to być po prostu polityka „nigdy nie wykonujemy UPDATE/DELETE na tej tabeli”, plus rola bazy z prawami tylko do INSERT i SELECT;
  • ograniczony dostęp: nie cały zespół inżynierski powinien mieć prawa do odczytu pełnego audytu.

Jeśli używacie PostgreSQL przez Prisma/Drizzle, model może wyglądać tak (uproszczony przykład):

CREATE TABLE audit_events (
  event_id   uuid PRIMARY KEY,
  created_at timestamptz NOT NULL DEFAULT now(),
  actor_user_id text,
  actor_tenant_id text,
  actor_ip      inet,
  action        text NOT NULL,
  resource_type text,
  resource_id   text,
  context_mcp_tool text,
  context_request_id text,
  outcome_status text NOT NULL,
  outcome_reason text
);

Schemat dostosowujecie do swoich potrzeb, ale najważniejsza jest ustrukturyzowanie. Jednolinijkowy śmieciowy JSON będziecie potem przeklinać.

Implementacja audytu w naszej aplikacji

Zróbmy małego helpera w aplikacji Next.js (środowisko Node, np. w serwerze MCP lub w API‑route):

// lib/audit.ts
import { randomUUID } from "crypto";
import { db } from "./db"; // twój klient do bazy danych

export async function logAudit(event: Omit<AuditEvent, "eventId" | "timestamp">) {
  const full: AuditEvent = {
    ...event,
    eventId: randomUUID(),
    timestamp: new Date().toISOString(),
  };

  // W prawdziwym życiu — przez kolejkę/w tle, tutaj tylko insert
  await db.insertInto("audit_events").values({
    event_id: full.eventId,
    created_at: full.timestamp,
    actor_user_id: full.actor.userId,
    action: full.action,
    outcome_status: full.outcome.status,
    outcome_reason: full.outcome.reason ?? null,
    // ...pozostałe pola
  });
}

Teraz dodamy wywołanie do handlera, który tworzy zamówienie (załóżmy, że to narzędzie MCP lub serwerowy endpoint):

// app/api/orders/route.ts
export async function POST(req: Request) {
  const user = await requireUser(req); // z modułu uwierzytelniania
  const body = await req.json();
  const order = await createOrderInDb(user, body);

  await logAudit({
    actor: { userId: user.id, client: "chatgpt-app" },
    action: "order.create",
    resource: { type: "order", id: order.id },
    context: { mcpTool: "create_order_tool" },
    outcome: { status: "success" },
  });

  return Response.json(order);
}

To samo można zrobić wokół niebezpiecznych operacji — anulowanie zamówienia, zmiana danych płatniczych, usuwanie konta.

Mamy oddzielną ustrukturyzowaną warstwę audytu — świetnie. Kolejne naturalne pytanie: jak długo wszystkie te zdarzenia (i pozostałe dane użytkownika) powinny żyć i co z nimi zrobić po upływie czasu?

3. Data retention: jak długo żyją wasze dane

Dlaczego nie można przechowywać wszystkiego wiecznie

Inżynierski odruch „a nuż się przyda” w kontekście danych użytkowników jest bardzo niebezpieczny.

Po pierwsze, im dłużej i więcej danych gromadzicie, tym poważniejsze skutki w przypadku wycieku: im większa beczka z benzyną, tym straszniejszy pożar. Wiele przewodników ochrony danych nazywa dane wręcz „toksywnym aktywem”: trzeba je przechowywać z pożytkiem, ale minimalizować ich wolumen i czas.

Po drugie, przepisy pokroju GDPR/CCPA wprowadzają zasadę „nie dłużej niż to konieczne dla celu przetwarzania”. To znaczy danych osobowych nie wolno trzymać w nieskończoność „na wszelki wypadek”. Dla każdego typu danych powinny być jasne okresy przechowywania oraz procedury usuwania lub anonimizacji.

Po trzecie, chmura kosztuje. Duże tabele logów i historii czatów rosną szybko i po roku okazuje się, że połowa rachunku od dostawcy to „wczorajsze śmieci”.

Różne dane — różne okresy

Doświadczenie firm i publiczne wytyczne dają mniej więcej taki obraz:

Typ danych Typowe okresy przechowywania
Logi debug, metryki techniczne od 1 do 12 miesięcy
Dzienniki audytu ≥ 12 miesięcy, czasem 2–5 lat
Zamówienia, płatności, faktury 3–7 lat (wymogi księgowe/podatkowe)
Sesje, tymczasowe tokeny godziny–dni
Surowe czaty/zapytania od kilku tygodni do kilku miesięcy, albo w ogóle nie przechowujemy
Anonimowe agregaty (analityka) dłużej, ponieważ PII już nie ma

Ważne: to nie jest porada prawna, tylko inżynierskie drogowskazy. Dla realnego produktu okresy uzgodnicie z prawnikami, ale technicznie powinniście być gotowi wdrożyć różne TTL.

Jak wdrożyć retencję w kodzie

Najczęstszy wzorzec: tabela ma pola created_at lub expires_at, a wy macie okresowy proces, który usuwa lub anonimizuje stare rekordy.

Przykład: czyszczenie zwykłych logów starszych niż 90 dni.

// scripts/cleanup-logs.ts
import { db } from "../lib/db";

async function cleanup() {
  await db
    .deleteFrom("app_logs")
    .where("created_at", "<", new Date(Date.now() - 90 * 24 * 60 * 60 * 1000));
  console.log("Old logs removed");
}

cleanup().catch(console.error);

Taki skrypt można uruchamiać z crona, przez GitHub Actions według harmonogramu lub przy pomocy planera w chmurze.

Dla PII zamiast usuwania często stosuje się anonimizację. Na przykład zamówienia starsze niż N lat tracą powiązanie z konkretnym użytkownikiem:

UPDATE orders
SET user_id = NULL
WHERE created_at < now() - interval '3 years';

Przy tym zachowują się kwoty, produkty i pozostała „księgowość”, ale znika powiązanie z konkretną osobą.

Pamiętajcie, że kopie zapasowe również muszą mieć swoje okresy życia. Częstotliwość i czas przechowywania kopii omówimy osobno w bloku o backupach, ale idea jest ta sama: nawet archiwów nie należy trzymać w nieskończoność, inaczej „prawo do bycia zapomnianym” staje się fikcją.

4. Usuwanie na żądanie użytkownika: „prawo do bycia zapomnianym” w kodzie

Skąd to wymaganie

Europejskie GDPR (i podobne ustawy) wprowadza tak zwane „prawo do bycia zapomnianym”: użytkownik może zażądać usunięcia swoich danych osobowych, a firma powinna to zrobić bez nieuzasadnionej zwłoki.

Z punktu widzenia dewelopera takiej aplikacji oznacza to: prędzej czy później nadejdzie żądanie „usuń wszystkie dane o mnie” (albo sami zrobicie przycisk „Delete my data”) i trzeba będzie nie tylko usunąć wiersz w tabeli users, lecz także przejść cały ślad: zamówienia, sesje, tokeny, dzienniki działań, CRM, płatności itd.

Ale są też przepisy, które wymagają przechowywania określonych danych: na przykład transakcji finansowych. Więc tutaj jest więcej zawiłości prawnych niż technicznych.

Co dokładnie należy wyczyścić

Minimalny zestaw dla naszego GiftGenius:

  • profil użytkownika (imię, e‑mail, ustawienia);
  • sesje, refresh‑tokeny, powiązania z dostawcami OAuth;
  • zamówienia, jeśli nie są potrzebne w „spersonalizowanej formie” (lub można je zanonimizować);
  • logi i zapisy audytu, gdzie w środku leży PII (np. e‑mail w surowej postaci).

Pozostają dane ważne dla raportowania, ale już zdeidentyfikowane — kwoty zamówień, liczba transakcji, agregaty po krajach itp.

Przykładowy algorytm usuwania

Schemat scenariusza:

  1. Użytkownik (zalogowany) klika „Usuń moje konto”.
  2. Na serwer trafia żądanie z jego userId.
  3. Serwer:
    • usuwa/anonimizuje zależne rekordy (zamówienia, sesje, integracje);
    • czyści PII w profilu;
    • zapisuje wpis w audit‑logu „obsłużono żądanie usunięcia danych”.

Dla prostoty pokażemy minimalny wariant na dwóch tabelach. W prawdziwym produkcie wokół tego samego rdzenia dodacie dodatkowe byty (integracje, usługi zewnętrzne itp.).

Kod serwisowy w Next.js (uproszczony przykład):

// app/api/delete-me/route.ts
import { db } from "@/lib/db";
import { logAudit } from "@/lib/audit";

export async function POST(req: Request) {
  const user = await requireUser(req);

  await db.transaction(async (tx) => {
    await tx.deleteFrom("sessions").where("user_id", "=", user.id);
    await tx.deleteFrom("orders").where("user_id", "=", user.id);

    await tx.updateTable("users")
      .set({
        is_deleted: true,
        name: null,
        email: null,
      })
      .where("id", "=", user.id);

    await logAudit({
      actor: { userId: user.id, client: "chatgpt-app" },
      action: "account.delete",
      outcome: { status: "success" },
    });
  });

  return new Response(null, { status: 204 });
}

W realnym świecie dodacie tu wywołania zewnętrznych API (np. Stripe — żeby odpiąć customer), a samą transakcję zrobicie poważniejszą. Ale zasada już jest: wszystko w jednym miejscu, z wpisem audytowym.

Powiązanie z backupami

Z podchwytliwą częścią „a co z kopiami zapasowymi” wiąże się wiele ciekawych pytań. Nawet jeśli usuniecie użytkownika z produkcyjnej bazy, jego dane mogą zostać w nocnych snapshotach. Żeby nie przeradzało się to w „nigdy nikogo faktycznie nie usuwamy”, są dwa podejścia:

  1. Kopie zapasowe żyją ograniczony czas (np. 30–90 dni) i po tym czasie znikają razem z danymi. Po upływie retencji ani główna baza, ani archiwa nie zawierają użytkownika.
  2. Jeśli jednak podnosicie system z backupu, macie rejestr „usuniętych” ID i po odtworzeniu ponownie uruchamiacie skrypty usuwania/anonimizacji.

W dużych firmach czasem praktykuje się crypto‑shredding: PII użytkownika szyfruje się oddzielnym kluczem, a na żądanie usunięcia niszczy się właśnie klucz. Nawet jeśli gdzieś zostały kopie zaszyfrowanych danych (w logach, backupach), bez klucza to bezużyteczny śmieć. To świetne, ale dla startupu trochę „rakietowa technologia”.

Ważny aspekt UX

Pamiętajcie, że usuwanie to nie tylko SQL. Użytkownik oczekuje:

  • zrozumiałego sposobu wysłania żądania (przycisk, formularz, e‑mail);
  • rozsądnych terminów realizacji (w praktyce do 30 dni);
  • powiadomienia o sukcesie albo uzasadnionej odmowy (np. gdy część danych trzeba przechowywać z mocy prawa).

Od strony technicznej macie już wszystko: potraficie przeprowadzić czyszczenie, zalogować działanie i nie trzymać nadmiaru w backupach.

5. Business continuity & kopie zapasowe

Teraz wyobraźcie sobie, że wszystko powyższe działa idealnie… dopóki nie wydarzy się fatalne DROP TABLE orders, awaria w chmurze albo upadek regionu. Potrzebujemy mechanizmów, aby przywrócić usługę do życia w rozsądnym czasie i nie utracić krytycznych danych.

RTO i RPO — dwie litery definiujące wasz ból

Dwa podstawowe parametry Disaster Recovery:

  • RTO (Recovery Time Objective) — ile czasu możecie sobie pozwolić być niedostępni. Na przykład, jeśli RTO = 1 godzina, to po poważnej awarii musicie podnieść system maksymalnie w godzinę.
  • RPO (Recovery Point Objective) — ile danych w czasie jesteście gotowi utracić. Jeśli RPO = 10 minut, to przy odtwarzaniu można utracić ostatnie 10 minut historii, ale nie więcej.

Im bardziej krytyczny produkt (bankowość, systemy transakcyjne), tym bliżej zera oba parametry. Dla edukacyjnego GiftGenius można żyć z RTO ~ kilka godzin i RPO ~ 15–60 minut, ale nawet to trzeba jakoś zrealizować.

Co może pójść nie tak w waszym stosie

W kontekście aplikacji ChatGPT na Vercel + chmurowa baza + zewnętrzne API typowa lista kłopotów jest taka:

  • OpenAI API niedostępne: wasza aplikacja odpowiada błędami na wywołania narzędzi.
  • Vercel (lub ktoś inny) ma awarię: widżet nie może dobić się do waszego backendu.
  • Baza danych uszkodzona albo coś przypadkowo usunięto (np. DROP TABLE).
  • Uszkodzone lub skompromitowane konto zarządzające infrastrukturą.

Na to wszystko odpowiadacie kombinacją kopii zapasowych, replikacji oraz rozsądnego zachowania aplikacji przy awarii.

Strategie tworzenia kopii zapasowych

Współczesne chmurowe Postgres/inne bazy zwykle dają co najmniej trzy opcje:

  1. Pełne kopie + przyrostowe.
    Robić pełny snapshot bazy raz dziennie, a między nimi trzymać zmiany przyrostowe. Odtwarzanie — powrót do konkretnego snapshotu plus przewinięcie dziennika zmian.
  2. Point‑in‑Time Recovery (PITR).
    Baza zapisuje dziennik transakcji (WAL) i pozwala odtworzyć się do dowolnego momentu (np. „stan na 14:03:00, zanim zdropowaliśmy tabelę”).
  3. Replikacja do innego regionu.
    Trzymać pasywną lub aktywną replikę bazy w innym regionie/chmurze. Przy utracie regionu głównego można przełączyć aplikację na replikę, tracąc tylko dane, które nie zdążyły dolecieć.

Na naszą skalę zwykle wystarczy włączyć PITR u dostawcy bazy i okresowe off‑site backupy.

Prosty przykład: codzienny dump dla lokalnej/dev bazy

Nawet jeśli na produkcji polegacie na bazie zarządzanej, dla staging/dev czasem chce się mieć prosty skrypt:

# scripts/backup.sh
#!/usr/bin/env bash
set -e
DATE=$(date +%F)
pg_dump "$DATABASE_URL" > "backups/backup-$DATE.sql"
echo "Backup created: backups/backup-$DATE.sql"

Uruchamiać to można z crona lub GitHub Actions. Najważniejsze — nie zapomnieć, że kopie zapasowe też trzeba usuwać po upływie okresu życia.

Zachowanie aplikacji przy awarii usług zewnętrznych

Kopie zapasowe i PITR rozwiązują problem „co zrobić, jeśli wszystko całkiem padło albo dane są uszkodzone”. Ale w biznesowej rzeczywistości częściej zdarzają się częściowe awarie — padło zewnętrzne API, sieć przycięta, zawisła płatność.

Gdy OpenAI API lub płatność leży, najgorsza strategia to zwracać surowe 500 i bezsensowny stack trace w odpowiedzi. W idealnym scenariuszu:

  • backend zwraca ustrukturyzowany błąd w rodzaju { error: "upstream_unavailable" };
  • widżet pokazuje użytkownikowi zrozumiały komunikat: „Usługa chwilowo niedostępna, spróbuj później”;
  • system nie bombarduje leżącego API nieskończonymi retry (wzorce Circuit Breaker itd. omówimy szerzej w module o odporności).

Przykład obsługi narzędzia MCP, które uwzględnia błąd zewnętrzny:

// mcp/tools/createGiftIdea.ts
export async function createGiftIdea(args: Input): Promise<Output> {
  try {
    return await callOpenAiModel(args);
  } catch (err) {
    await logAudit({
      actor: { userId: args.userId ?? null, client: "chatgpt-app" },
      action: "giftidea.generate",
      outcome: { status: "failure", reason: "openai_unavailable" },
    });
    throw new Error("UPSTREAM_UNAVAILABLE");
  }
}

Dalej wasza warstwa pośrednia między MCP a widżetem już wie, jak elegancko pokazać ten błąd w UI.

Weryfikacja odtwarzania: backup bez restore to tylko plik

Klasyczny antywzorzec: backup robi się codziennie, wszyscy zadowoleni… dopóki nie okaże się, że nie da się ich odtworzyć (format się zmienił, klucz zaginął, zabrakło miejsca).

Minimalny plan:

  • okresowo (np. raz w miesiącu) podnosić środowisko staging z backupu;
  • przejść podstawowe scenariusze: logowanie, tworzenie zamówienia, działanie aplikacji;
  • upewnić się, że czas odtwarzania i utrata danych mieszczą się w waszych RTO/RPO.

Zamiast kursu z „religii DevOps” w ramach tego wykładu wystarczy zrozumieć: procesy tworzenia kopii zapasowych są częścią architektury aplikacji, a nie „coś, co zrobi ktoś tam w chmurze”.

6. Wizualizacja: cykl życia danych i zdarzeń

Żeby to nie były tylko słowa, narysujmy dwa proste schematy.

Cykl życia danych użytkownika

flowchart TD
  A["Tworzenie danych<br/>(rejestracja, zamówienie)"] --> B["Przechowywanie i użycie<br/>(prod DB)"]
  B --> C["Archiwum/agregacja<br/>(anonimowe metryki)"]
  B --> D[Żądanie usunięcia]
  D --> E[Usunięcie/anonimizacja<br/>w prod DB]
  E --> F["Wygaśnięcie okresu życia kopii zapasowych<br/>(retencja)"]
    

Główna idea: cykl życia danych nie kończy się na produkcyjnej bazie — trwa również w backupach.

Przepływ audytu dla niebezpiecznej operacji

sequenceDiagram
  participant User as Użytkownik
  participant ChatGPT as ChatGPT
  participant App as Twój backend/MCP
  participant DB as Baza
  participant Audit as Magazyn audytu

  User->>ChatGPT: "Anuluj zamówienie #123"
  ChatGPT->>App: callTool cancel_order
  App->>DB: UPDATE orders SET status='canceled'
  App->>Audit: INSERT audit_event {actor, action, resource, outcome}
  App-->>ChatGPT: Wynik operacji
  ChatGPT-->>User: Komunikat o wyniku
    

7. Typowe błędy w audit & lifecycle

Błąd nr 1: Mieszanie audit‑logów i zwykłych logów.
Kiedy wszystkie komunikaty trafiają do jednego wspólnego indeksu logs, po pół roku nikt nie odróżni „użytkownik zmienił rolę administratora” od „znowu mamy null reference”. W audycie powinny być ustrukturyzowane zdarzenia na poziomie biznesowym (zob. rozdział o strukturze zdarzenia audytu) oraz oddzielny magazyn z ograniczonym dostępem.

Błąd nr 2: Logowanie PII w audycie i logach debug.
Pełny e‑mail, telefon, adres dostawy, ostatnie cztery cyfry karty — to wszystko często przypadkowo ląduje w logach. Zwiększa to ryzyko wycieku i stoi w sprzeczności z zaleceniami dot. prywatności. Zamiast tego logujcie identyfikatory i zamaskowane wartości.

Błąd nr 3: Brak polityki retencji — „przechowujemy wszystko zawsze”.
Na etapie MVP wydaje się to „w porządku”, a po roku wasze tabele rosną do monstrualnych rozmiarów i każde zapytanie analityczne zamienia się w DDoS własnej bazy. Plus łamiecie zasadę minimalizacji obecną we współczesnym prawie o ochronie danych. Minimalne TTL według typów danych muszą być przemyślane, a czyszczenie — zautomatyzowane.

Błąd nr 4: „Usuwanie na żądanie” == DELETE FROM users.
Jeśli po prostu usuniecie wiersz użytkownika, ale zostawicie jego PII w zamówieniach, sesjach i logach, to w istocie nikogo nie usunęliście. Poprawne podejście — transakcyjnie przejść po wszystkich powiązanych bytach, a gdzie nie można usunąć — zanonimizować. I nie zapomnieć zalogować samego faktu usunięcia jako zdarzenia audytu.

Błąd nr 5: Ignorowanie backupów przy usuwaniu danych.
Usunęliście użytkownika w produkcji — dobrze, ale jego dane jeszcze rok żyją w starych snapshotach. Przy odtworzeniu z nich wszystko „zmartwychwstaje”, i znów łamiecie własne obietnice użytkownikowi i w Privacy Policy. Trzeba albo ograniczać okres życia kopii, albo mieć procedurę ponownego zastosowania usunięć po odtworzeniu.

Błąd nr 6: „Mamy włączone backupy, więc wszystko jest OK”, ale nikt nie próbował odtworzyć.
Backup, którego nigdy nie sprawdzono przy restore, to tylko drogi plik. Bez okresowej weryfikacji odtwarzania nie znacie ani faktycznego RTO/RPO, ani tego, czy wasz plan DR w ogóle działa. Minimum — regularne testowe podnoszenie stagingu z backupu według checklisty.

Błąd nr 7: Rozjazd między dokumentacją a rzeczywistością.
W Privacy Policy piszecie, że przechowujecie logi 30 dni i usuwacie dane na żądanie, a w kodzie wszystko zostaje na zawsze. Sklep ChatGPT, klienci korporacyjni i audytorzy łatwo to wychwycą pytaniami w rodzaju „pokażcie tabelę retencji” oraz „zademonstrujcie usunięcie konkretnego użytkownika”. Lepiej najpierw zrobić, potem napisać.

1
Ankieta/quiz
Bezpieczeństwo, poziom 15, lekcja 4
Niedostępny
Bezpieczeństwo
Bezpieczeństwo i zgodność
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION