Skip to content

LGPD & Data Protection

This guide explains how GrydAudit ensures compliance with LGPD (Lei Geral de Proteção de Dados), GDPR, and other data protection regulations when auditing entity changes.

The Problem

Audit logs capture every property change — but some properties contain sensitive personal data (PII) that must not be stored in audit logs per data protection laws:

  • CPF / SSN – national identification numbers
  • Credit Card Numbers – financial payment data
  • Medical Records – health-related information (LGPD Art. 11)
  • Salary / Financial Data – employment financial information
  • Passwords / Tokens – authentication secrets

GrydAudit provides a two-level exclusion mechanism so sensitive data never reaches audit storage.

How It Works

Discovery Flow

Two Levels of Protection

LevelWhereScopeExample
GlobalAuditInterceptorOptions.ExcludedPropertiesAll entitiesPassword, Token, ApiKey
Per EntityIAuditableEntity.AuditExcludedPropertiesSingle entityCpf, CreditCardNumber, Salary

Both levels are merged at runtime — a property excluded at either level will never appear in audit logs.

The IAuditableEntity Interface

The core interface that enables entities to declare their audit behavior:

csharp
namespace Gryd.Observability.Audit;

public interface IAuditableEntity
{
    /// <summary>
    /// Whether audit is enabled for this entity.
    /// Default: true. Set to false to completely skip auditing.
    /// </summary>
    bool AuditEnabled => true;

    /// <summary>
    /// Properties that should be excluded from audit logs.
    /// Use this to protect sensitive/PII data per LGPD/GDPR.
    /// </summary>
    IEnumerable<string> AuditExcludedProperties => [];

    /// <summary>
    /// Optional category for audit log classification.
    /// Examples: "PII", "Financial", "Medical", "Security"
    /// </summary>
    string? AuditCategory => null;
}

All members have default implementations — you only override what you need.

Usage Examples

Example 1: Customer with PII Data (LGPD Art. 5°)

A customer entity that protects CPF, credit card, address, and birth date:

csharp
public class Customer : BaseEntity, IAuditableEntity
{
    // ✅ These ARE audited (non-sensitive business data)
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string Category { get; set; } = "Standard";

    // ❌ These are NOT audited (LGPD-protected PII)
    public string Cpf { get; set; } = string.Empty;
    public string CreditCardNumber { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public DateTime BirthDate { get; set; }

    // IAuditableEntity implementation
    public bool AuditEnabled => true;
    public string? AuditCategory => "PII";
    public IEnumerable<string> AuditExcludedProperties =>
        ["Cpf", "CreditCardNumber", "Address", "BirthDate"];
}

Resulting audit log when updating a customer:

json
{
  "entityType": "Customer",
  "entityId": "a1b2c3d4-...",
  "action": "Updated",
  "category": "PII",
  "timestamp": "2026-02-07T10:30:00Z",
  "userId": "user123",
  "propertyChanges": [
    {
      "propertyName": "Name",
      "oldValue": "João Silva",
      "newValue": "João da Silva"
    },
    {
      "propertyName": "Category",
      "oldValue": "Standard",
      "newValue": "Premium"
    }
  ]
}

NOTICE

The Cpf, CreditCardNumber, Address, and BirthDate fields do not appear in the audit log — even if they were modified in the same operation.

Example 2: Patient Record — Health Data (LGPD Art. 11)

LGPD Art. 11 requires special protection for health-related data (dados sensíveis):

csharp
public class PatientRecord : BaseEntity, IAuditableEntity
{
    // ✅ Audited — operational data
    public string PatientName { get; set; } = string.Empty;
    public string Department { get; set; } = string.Empty;
    public string AttendingDoctor { get; set; } = string.Empty;
    public DateTime AdmissionDate { get; set; }

    // ❌ NOT audited — LGPD Art. 11 health data
    public string MedicalRecordNumber { get; set; } = string.Empty;
    public string Diagnosis { get; set; } = string.Empty;
    public string TreatmentPlan { get; set; } = string.Empty;
    public string Allergies { get; set; } = string.Empty;

    public bool AuditEnabled => true;
    public string? AuditCategory => "Medical";
    public IEnumerable<string> AuditExcludedProperties =>
        ["MedicalRecordNumber", "Diagnosis", "TreatmentPlan", "Allergies"];
}

Example 3: Employee — Financial & Personal Data

Salary, bank account, and personal identifiers require protection:

csharp
public class Employee : BaseEntity, IAuditableEntity
{
    // ✅ Audited — organizational data
    public string FullName { get; set; } = string.Empty;
    public string Department { get; set; } = string.Empty;
    public string Position { get; set; } = string.Empty;
    public DateTime HireDate { get; set; }

    // ❌ NOT audited — sensitive personal/financial data
    public string SocialSecurityNumber { get; set; } = string.Empty;
    public decimal Salary { get; set; }
    public string PersonalEmail { get; set; } = string.Empty;
    public string BankAccount { get; set; } = string.Empty;

    public bool AuditEnabled => true;
    public string? AuditCategory => "HR";
    public IEnumerable<string> AuditExcludedProperties =>
        ["SocialSecurityNumber", "Salary", "PersonalEmail", "BankAccount"];
}

Example 4: Financial Transaction — Mixed Compliance

Some financial data must be audited for compliance (e.g., transaction amount), while other data must be excluded for data protection:

csharp
public class FinancialTransaction : BaseEntity, IAuditableEntity
{
    // ✅ Audited — required for financial compliance
    public decimal Amount { get; set; }
    public string Currency { get; set; } = "BRL";
    public string TransactionType { get; set; } = string.Empty;
    public string Status { get; set; } = "Pending";

    // ❌ NOT audited — PII data
    public string BankAccount { get; set; } = string.Empty;
    public string CardLastFourDigits { get; set; } = string.Empty;

    public bool AuditEnabled => true;
    public string? AuditCategory => "Financial";
    public IEnumerable<string> AuditExcludedProperties =>
        ["BankAccount", "CardLastFourDigits"];
}

IMPORTANT

For financial transactions, you typically want to audit the Amount and Status changes for compliance, but exclude bank account details. Design your exclusion list carefully per your regulatory requirements.

Example 5: Conditional Audit — Draft Documents

Completely disable auditing for entities in a specific state:

csharp
public class Document : BaseEntity, IAuditableEntity
{
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public bool IsDraft { get; set; } = true;

    // Only audit when the document is published
    public bool AuditEnabled => !IsDraft;
    public string? AuditCategory => "Documents";
    public IEnumerable<string> AuditExcludedProperties => [];
}

Example 6: Non-Auditable Entity

If an entity should never be audited, simply don't implement IAuditableEntity — or set AuditEnabled to false:

csharp
// Option 1: Don't implement the interface (still audited with defaults)
public class TemporaryCache : BaseEntity
{
    public string Key { get; set; } = string.Empty;
    public string Value { get; set; } = string.Empty;
}

// Option 2: Implement and explicitly disable
public class TemporaryCache : BaseEntity, IAuditableEntity
{
    public string Key { get; set; } = string.Empty;
    public string Value { get; set; } = string.Empty;

    public bool AuditEnabled => false; // Never audited
}

Global Exclusions

Properties that should be excluded from all entities are configured globally:

csharp
// These are excluded by default in AuditInterceptorOptions:
public List<string> ExcludedProperties { get; set; } =
[
    "Password",
    "PasswordHash",
    "Secret",
    "Token",
    "ApiKey",
    "RefreshToken"
];

You can customize global exclusions in your service configuration:

csharp
services.AddScoped<AuditSaveChangesInterceptor>(sp =>
{
    var options = new AuditInterceptorOptions
    {
        ExcludedProperties =
        [
            "Password", "PasswordHash", "Secret",
            "Token", "ApiKey", "RefreshToken",
            // Add your own global exclusions:
            "InternalNotes", "SystemMetadata"
        ]
    };

    return new AuditSaveChangesInterceptor(
        sp.GetRequiredService<IAuditContext>(),
        sp.GetRequiredService<IAuditStore>(),
        options);
});

Precedence Rules

When both global and entity-level exclusions are defined, they are merged (union):

Final Exclusions = Global Exclusions ∪ Entity Exclusions
PropertyGlobal ExcludedEntity ExcludedAudited?
Name✅ Yes
Password❌ No
Cpf❌ No
Token❌ No

Audit Categories

Categories help organize audit logs for compliance reporting:

csharp
public string? AuditCategory => "PII";       // Personal data
public string? AuditCategory => "Financial";  // Financial transactions
public string? AuditCategory => "Medical";    // Health data (LGPD Art. 11)
public string? AuditCategory => "HR";         // Employee data
public string? AuditCategory => "Security";   // Authentication events
public string? AuditCategory => "Documents";  // Document management

Query audit logs by category:

csharp
var piiChanges = await mediator.Send(new SearchAuditLogsQuery(new AuditLogFilter
{
    Category = "PII",
    FromDate = DateTime.UtcNow.AddDays(-30)
}));

LGPD Compliance Checklist

Use this checklist to verify your application's LGPD compliance with GrydAudit:

#RequirementLGPD ArticleHow GrydAudit Handles It
1Personal data must be identifiableArt. 5°IAuditableEntity.AuditExcludedProperties — declare PII fields
2Sensitive data requires extra protectionArt. 11Excluded properties never reach audit storage
3Purpose limitationArt. 6° IIAuditCategory — classify why data is audited
4Data minimizationArt. 6° IIIOnly non-excluded properties are logged
5Traceability of accessArt. 6° X5W1H audit entries (Who, What, When, Where, Why, How)
6Data retention controlArt. 16AuditOptions.RetentionDays — automatic cleanup
7Multi-tenant isolationArt. 46TenantId in all audit entries
8Security secrets protectionArt. 46Global exclusions (Password, Token, ApiKey, etc.)

Testing LGPD Compliance

GrydAudit includes comprehensive integration tests to validate LGPD compliance. Here's how to write your own:

csharp
[Fact]
public async Task Cpf_Should_Not_Appear_In_Audit_Logs()
{
    // Arrange
    var customer = new Customer
    {
        Name = "João Silva",
        Cpf = "123.456.789-00",        // Sensitive!
        CreditCardNumber = "4111...",   // Sensitive!
        Category = "Premium"
    };

    // Act
    dbContext.Customers.Add(customer);
    await dbContext.SaveChangesAsync();

    // Assert
    var auditEntries = auditStore.GetEntries();
    var customerAudit = auditEntries.First(e => e.EntityType == "Customer");

    // ✅ Non-sensitive data IS recorded
    customerAudit.Changes.Should().Contain(c => c.PropertyName == "Name");
    customerAudit.Changes.Should().Contain(c => c.PropertyName == "Category");

    // ❌ Sensitive data is NOT recorded
    customerAudit.Changes.Should().NotContain(c => c.PropertyName == "Cpf");
    customerAudit.Changes.Should().NotContain(c => c.PropertyName == "CreditCardNumber");

    // Double-check: values don't appear anywhere in serialized audit
    var serialized = JsonSerializer.Serialize(customerAudit);
    serialized.Should().NotContain("123.456.789-00");
    serialized.Should().NotContain("4111");
}

TEST COVERAGE

GrydAudit includes 69 integration tests covering LGPD compliance scenarios including:

  • CPF / Credit Card exclusion (Art. 5°)
  • Medical record protection (Art. 11)
  • Employee financial data (salary, bank account)
  • Multi-tenant audit isolation
  • Complete 5W1H audit principle validation

Run them with:

bash
dotnet test tests/Integration/GrydCrud.Audit.IntegrationTests/

Best Practices

1. Always Declare Sensitive Properties

csharp
// ✅ Good — explicitly declare what's sensitive
public IEnumerable<string> AuditExcludedProperties =>
    ["Cpf", "CreditCardNumber", "BirthDate"];

// ❌ Bad — relying only on global exclusions for entity-specific PII
public IEnumerable<string> AuditExcludedProperties => [];

2. Use Categories for Compliance Reports

csharp
// ✅ Good — categorized for easy filtering
public string? AuditCategory => "Medical";

// ❌ Bad — no category makes compliance reports harder
public string? AuditCategory => null;

3. Test Your Exclusions

Always write tests to verify that sensitive data does not leak into audit logs:

csharp
// Verify exclusion by checking no changes contain the sensitive value
var allValues = auditEntries
    .SelectMany(e => e.Changes)
    .SelectMany(c => new[] { c.OldValue, c.NewValue });

allValues.Should().NotContain(sensitiveValue,
    "LGPD violation: sensitive data found in audit log");

4. Review Exclusions Periodically

As your domain evolves, new properties may be added that contain PII. Review AuditExcludedProperties whenever entity models change.

5. Configure Retention Policies

csharp
services.Configure<AuditOptions>(options =>
{
    options.RetentionDays = 365;          // LGPD Art. 16
    options.EnableAutomaticCleanup = true;
});

Released under the MIT License.