The answer to "prove it".
GDPR Article 32, SOC 2, and HIPAA all require appropriate technical measures — and the ability to demonstrate them. A screenshot of your code isn't evidence. The compliance report is a dated, signed-off inventory of exactly which fields are protected, which aren't, who accepted the risk, and what fired recently — and it writes itself on your schedule.
Which fields are protected, which aren't, and who signed off.
One generated document answers the questions your security review and auditor will actually ask — from the model's real state at generation time, not a policy claiming coverage.
Encryption Compliance Report
1. Encryption configuration
2. Encrypted property inventory
| Entity | Property | Column | CLR type | Storage format |
|---|---|---|---|---|
| Customer | SocialSecurityNumber | ssn | String | Base64 |
| Customer | PassportNumber | passport_number | String | Base64 |
| Payment | CardNumber | card_number | String | Binary |
| Payee | SettlementAccount | settlement_acct | String | Base64 |
3. Outstanding scan findings
| Severity | Entity.Property | Column | Evidence & recommendation |
|---|---|---|---|
| Critical | Customer.SocialSecurityNumber | ssn_bak | 94% of 100 samples pattern-verified. Encrypt or drop the backup column. |
| Warning | Lead.Notes | notes | ML-classified free-text PII in 21% of samples. |
4. Accepted risks (scan exemptions)
| Entity | Property | Justification | Approved by |
|---|---|---|---|
| Payout | AccountLast4 | Masked at ingestion; only last 4 digits stored | CISO 2026-07-01 |
5. Recent security alerts
| Time (UTC) | Severity | Kind | Source |
|---|---|---|---|
| 2026-07-23 03:14:07 | Critical | AnomalyDetected | entity:Customer |
| 2026-07-23 09:00:05 | Warning | ScanFinding | scan:AppDbContext |
generator.ToHtml(report)
— A4 page rules included, so print-to-PDF is the whole workflow.It arrives on its own.
Set the cadence and it generates and emails itself, with the report attached. Recipients are separate from your alert list by default — the report names every unprotected column in your schema, so it belongs with compliance rather than the on-call rota.
- From
- Crypto Sentinel <security@yourapp.example>
- To
- dpo@yourapp.example, audit@yourapp.example
- Sent
- 1 Aug 2026, 06:00 — monthly, on the 1st
| Encryption coverage | 82.9% |
| Encrypted properties | 34 of 41 candidates |
| Critical findings | 1 |
| Accepted risks | 2 |
| Alerts in window | 14 |
| Dead-lettered deliveries | 0 |
Your cadence, your clock
Daily, weekly on a chosen weekday, or monthly on a chosen date — at your time of day, in your time zone.
Configured in appsettings.json, so changing it isn't a redeploy.
Unconfigured, it's the 1st of each month at 06:00 UTC.
A missed window isn't lost
Deploying at 06:00 on the 1st used to mean no report that month. Now the run is recorded, and a window missed while the process was down is sent late instead of skipped.
One report, not one per replica
Instances sharing a volume claim the run atomically, so three pods send one report between them. A failed send releases the claim so it retries rather than consuming the month.
// appsettings.json "Report": { "Enabled": true, "Cadence": "Weekly", // Daily | Weekly | Monthly | Custom "DayOfWeek": "Monday", "TimeOfDay": "09:00:00", "TimeZoneId": "UTC", "Recipients": [ "dpo@acme.example" ] }
Generate a report from code
// Scan the live model and data, then render the evidence pack. ScanReport scan = await scanner.ScanAsync(context); ComplianceReport report = generator.Generate( context, "AES-GCM 256-bit, keys in Azure Key Vault, rotated 2026-06-01", scan, recentAlerts); await File.WriteAllTextAsync("compliance.html", generator.ToHtml(report)); await File.WriteAllTextAsync("compliance.json", generator.ToJson(report));
CoveragePercent drops.
Evidence, not assertions.
The report is generated from your live model and data — so what you hand the auditor is what's actually true at that moment. Pairs with the alerting layer that tells you the moment something changes.