Appearance
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
| Level | Where | Scope | Example |
|---|---|---|---|
| Global | AuditInterceptorOptions.ExcludedProperties | All entities | Password, Token, ApiKey |
| Per Entity | IAuditableEntity.AuditExcludedProperties | Single entity | Cpf, 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| Property | Global Excluded | Entity Excluded | Audited? |
|---|---|---|---|
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 managementQuery 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:
| # | Requirement | LGPD Article | How GrydAudit Handles It |
|---|---|---|---|
| 1 | Personal data must be identifiable | Art. 5° | IAuditableEntity.AuditExcludedProperties — declare PII fields |
| 2 | Sensitive data requires extra protection | Art. 11 | Excluded properties never reach audit storage |
| 3 | Purpose limitation | Art. 6° II | AuditCategory — classify why data is audited |
| 4 | Data minimization | Art. 6° III | Only non-excluded properties are logged |
| 5 | Traceability of access | Art. 6° X | 5W1H audit entries (Who, What, When, Where, Why, How) |
| 6 | Data retention control | Art. 16 | AuditOptions.RetentionDays — automatic cleanup |
| 7 | Multi-tenant isolation | Art. 46 | TenantId in all audit entries |
| 8 | Security secrets protection | Art. 46 | Global 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;
});Related Documentation
- Getting Started — Initial GrydAudit setup
- Configuration — Full configuration reference
- Querying Audit Logs — Query and filter audit data
- Audit Integration — GrydCrud + GrydAudit integration