Audit Reports and Retention
This spoke covers compliance audit reports, data retention policies, SQL query examples, and GDPR deletion workflows for Mapify's change tracking system. For the full overview, see Multi-User Collaboration.
Audit Report Formats for Compliance
Mapify includes four pre-built compliance report types for SOX, GDPR, and HIPAA.
1. Change Activity Report (All Entities)
Purpose: Comprehensive report of all changes for a given period.
Fields: Entity Type, Entity Name, Changed By, Changed Date, Field Name, Old Value, New Value, Change Reason
C# Excel generation (EPPlus):
public async Task<byte[]> GenerateChangeActivityReportAsync(
DateTime startDate,
DateTime endDate,
string entityType = null)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Change Activity");
// Header row
string[] headers = {
"Entity Type", "Entity Name", "Field Name",
"Old Value", "New Value", "Changed By", "Changed Date", "Change Reason"
};
for (int col = 0; col < headers.Length; col++)
worksheet.Cells[1, col + 1].Value = headers[col];
// Style header: bold, blue background, white text
using (var range = worksheet.Cells[1, 1, 1, headers.Length])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(0, 112, 192));
range.Style.Font.Color.SetColor(Color.White);
}
// Fetch and populate data
var query = _context.EntityChangeLogs
.Where(c => c.ChangedDate >= startDate && c.ChangedDate <= endDate);
if (!string.IsNullOrEmpty(entityType))
query = query.Where(c => c.EntityType == entityType);
var changes = await query.OrderByDescending(c => c.ChangedDate).ToListAsync();
int row = 2;
foreach (var change in changes)
{
worksheet.Cells[row, 1].Value = change.EntityType;
worksheet.Cells[row, 2].Value = change.EntityName;
worksheet.Cells[row, 3].Value = change.FieldName ?? "(Entity Created)";
worksheet.Cells[row, 4].Value = change.OldValue ?? "";
worksheet.Cells[row, 5].Value = change.NewValue ?? "";
worksheet.Cells[row, 6].Value = change.ChangedBy;
worksheet.Cells[row, 7].Value = change.ChangedDate;
worksheet.Cells[row, 7].Style.Numberformat.Format = "yyyy-mm-dd hh:mm:ss";
worksheet.Cells[row, 8].Value = change.ChangeReason ?? "";
row++;
}
worksheet.Cells.AutoFitColumns();
return package.GetAsByteArray();
}
}
2. User Activity Report (By Person)
Purpose: Track all changes by specific users — useful for access reviews and offboarding.
Fields: User Name, User Email, Total Changes, Entity Types Modified, Most Frequently Modified Entities
Sample output:
User Activity Report – January 2026
| User Name | Email | Total Changes | Integrations | Systems | Most Edited Entity |
|:---|:---|:---|:---|:---|:---|
| Alice Johnson | alice@contoso.com | 142 | 98 | 32 | SAP to Salesforce (23×) |
| Bob Taylor | bob@contoso.com | 87 | 45 | 28 | Dynamics CRM API (18×) |
| Charlie Davis | charlie@contoso.com| 63 | 40 | 15 | Legacy EDI Bridge (15×) |
3. Compliance Audit Report (Filtered)
Purpose: Audit changes to entities tagged for specific compliance scope (GDPR, SOX, HIPAA, PCI-DSS).
SQL query example:
-- All changes to GDPR-regulated integrations in Q1 2026
SELECT
ecl.EntityType,
ecl.EntityName,
ecl.FieldName,
ecl.OldValue,
ecl.NewValue,
ecl.ChangedBy,
ecl.ChangedDate,
ecl.ChangeReason,
cm.MetadataValue AS ComplianceTag
FROM EntityChangeLog ecl
INNER JOIN Entities e ON ecl.EntityId = e.EntityId
INNER JOIN CustomMetadata cm ON e.EntityId = cm.EntityId
WHERE cm.MetadataKey = 'ComplianceTag'
AND cm.MetadataValue LIKE '%GDPR%'
AND ecl.ChangedDate >= '2026-01-01'
AND ecl.ChangedDate < '2026-04-01'
ORDER BY ecl.ChangedDate DESC;
4. High-Risk Change Report
Purpose: Extra scrutiny for critical production system changes, off-hours deployments, and undocumented changes.
Report Criteria:
- Changes to production environments
- Changes to entities tagged "Critical"
- Changes outside business hours (off-hours)
- Changes without a
ChangeReasonvalue
Sample SQL:
-- High-risk changes: production, off-hours, or no change reason
SELECT
ecl.EntityName,
cm.MetadataValue AS Environment,
ecl.ChangedBy,
ecl.ChangedDate,
ecl.FieldName,
ecl.ChangeReason,
CASE
WHEN ecl.ChangeReason IS NULL THEN 'HIGH'
WHEN DATEPART(HOUR, ecl.ChangedDate) NOT BETWEEN 8 AND 18 THEN 'MEDIUM'
ELSE 'LOW'
END AS RiskLevel
FROM EntityChangeLog ecl
INNER JOIN Entities e ON ecl.EntityId = e.EntityId
INNER JOIN CustomMetadata cm ON e.EntityId = cm.EntityId AND cm.MetadataKey = 'Environment'
WHERE cm.MetadataValue = 'Production'
AND ecl.ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY RiskLevel, ecl.ChangedDate DESC;
Audit Query Examples
Useful SQL queries for governance and investigations:
-- 1. Full change history for a specific entity
SELECT ChangeType, FieldName, OldValue, NewValue, ChangedBy, ChangedDate, ChangeReason
FROM EntityChangeLog
WHERE EntityId = '3fa85f64-5717-4562-b3fc-2c963f66afa6'
ORDER BY ChangedDate DESC;
-- 2. All changes by a specific user in last 30 days
SELECT EntityType, EntityName, ChangeType, FieldName, ChangedDate, ChangeReason
FROM EntityChangeLog
WHERE ChangedBy = 'alice.johnson@contoso.com'
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY ChangedDate DESC;
-- 3. High-churn entities — changed more than 10 times in 30 days
SELECT EntityName, EntityType, COUNT(*) AS ChangeCount
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
GROUP BY EntityId, EntityName, EntityType
HAVING COUNT(*) > 10
ORDER BY ChangeCount DESC;
-- 4. Changes without documentation (no ChangeReason)
SELECT EntityName, ChangedBy, ChangedDate, FieldName
FROM EntityChangeLog
WHERE ChangeReason IS NULL
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY ChangedDate DESC;
-- 5. Off-hours changes (outside 8 AM – 6 PM)
SELECT EntityName, ChangedBy, ChangedDate, FieldName, ChangeReason
FROM EntityChangeLog
WHERE (DATEPART(HOUR, ChangedDate) < 8 OR DATEPART(HOUR, ChangedDate) > 18)
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY ChangedDate DESC;
-- 6. Most frequently changed fields across all entities
SELECT FieldName, COUNT(*) AS ChangeCount
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -90, GETUTCDATE())
AND ChangeType = 'UPDATE'
GROUP BY FieldName
ORDER BY ChangeCount DESC;
-- 7. Rollback detection (same field changed back to original value)
WITH ChangeHistory AS (
SELECT *, LEAD(NewValue) OVER (PARTITION BY EntityId, FieldName ORDER BY ChangedDate) AS NextValue
FROM EntityChangeLog
WHERE EntityId = '3fa85f64-5717-4562-b3fc-2c963f66afa6'
)
SELECT EntityName, FieldName, OldValue, NewValue, ChangedBy, ChangedDate
FROM ChangeHistory
WHERE NewValue = LEAD(OldValue) OVER (PARTITION BY EntityId, FieldName ORDER BY ChangedDate);
Retention Policy and Data Management
| Environment | Default Retention | Configurable Range | Compliance Notes |
|---|---|---|---|
| Production | 365 days (1 year) | 90 days – Indefinite | SOX requires 7 years for financial systems; GDPR allows deletion after purpose fulfilled |
| Test/QA | 90 days | 30 days – 180 days | No regulatory requirement; focus on performance data |
| Development | 30 days | 7 days – 90 days | No compliance requirements; short retention minimizes storage |
Configuration (appsettings.json):
{
"ChangeTracking": {
"RetentionDays": {
"Production": 365,
"Test": 90,
"Development": 30
},
"ComplianceOverrides": {
"SOX": 2555,
"HIPAA": 2190,
"GDPR": 365
},
"AutoDelete": {
"Enabled": true,
"Schedule": "0 2 * * 0",
"ArchiveBeforeDelete": true,
"ArchivePath": "\\\\fileserver\\audit-archives"
}
}
}
Automated cleanup background service:
public class AuditLogCleanupService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var retentionDate = DateTime.UtcNow.AddDays(-_config.RetentionDays.Production);
// Archive before deleting
if (_config.AutoDelete.ArchiveBeforeDelete)
{
var expiredLogs = await _context.EntityChangeLogs
.Where(e => e.ChangedDate < retentionDate)
.ToListAsync();
await ExportToCsvArchive(expiredLogs, _config.AutoDelete.ArchivePath);
}
// Delete expired records
var deleted = await _context.EntityChangeLogs
.Where(e => e.ChangedDate < retentionDate)
.ExecuteDeleteAsync();
_logger.LogInformation(
"Audit log cleanup: deleted {Count} records older than {RetentionDate}",
deleted, retentionDate);
// Run weekly (schedule: Sunday 2 AM)
await Task.Delay(TimeSpan.FromDays(7), stoppingToken);
}
}
}
GDPR Compliance and Right to Deletion
Key Principle: Change logs containing personal data must respect GDPR "right to be forgotten" (Article 17), but audit retention requirements (SOX, HIPAA) may override in certain cases. Consult legal counsel for your regulatory context.
GDPR Deletion Workflow
- User submits deletion request via formal GDPR request form
- Legal review determines if audit retention overrides apply (financial systems = 7 years SOX)
- If deletion approved:
- Anonymize
ChangedByfield → replace with"User_DELETED_20260119" - Preserve change metadata (what/when) for audit continuity
- Delete or redact PII in
OldValue/NewValuefields
- Anonymize
- Generate deletion certificate for the data subject
SQL Anonymization Script
DECLARE @UserEmail NVARCHAR(255) = 'alice.johnson@contoso.com';
DECLARE @AnonymizedId NVARCHAR(255) =
CONCAT('User_DELETED_', FORMAT(GETUTCDATE(), 'yyyyMMdd'));
BEGIN TRANSACTION;
UPDATE EntityChangeLog
SET ChangedBy = @AnonymizedId
WHERE ChangedBy = @UserEmail;
-- Redact PII in change values
UPDATE EntityChangeLog
SET OldValue = '[REDACTED]'
WHERE OldValue LIKE '%' + @UserEmail + '%';
UPDATE EntityChangeLog
SET NewValue = '[REDACTED]'
WHERE NewValue LIKE '%' + @UserEmail + '%';
COMMIT TRANSACTION;
-- Log the GDPR deletion for compliance audit
INSERT INTO GDPRDeletionLog (UserEmail, DeletionDate, RecordsAffected)
VALUES (@UserEmail, GETUTCDATE(), @@ROWCOUNT);
For full GDPR, SOX, HIPAA, and PCI-DSS compliance guidelines, see the Compliance and Data Governance Guide.
Related Topics
- Multi-User Collaboration Hub — full feature overview
- Architecture and Presence — real-time system design
- Scalability and Offline Editing — performance and offline
- Change Tracking and Audit Trail — entity metadata and history timeline
- Comments and Annotations — team collaboration
- Comment Status and UI — approval workflows