# Daily DevOps & .NET > Opinionated .NET, Azure and DevOps engineering. Hard-won lessons from production, no tutorials. Source: https://daily-devops.net/ Generated: 2026-05-26 Feeds: - RSS: https://daily-devops.net/feed.rss - Atom: https://daily-devops.net/feed.atom - JSON Feed: https://daily-devops.net/feed.json --- # Section: ISO Standards # Your Privacy Docs Are Fiction: Let's Fix That with .NET CLI Tools URL: https://daily-devops.net/posts/privacy-audit-automation-dotnet-cli/ Published: 2026-04-30 Modified: 2026-05-26 Authors: martin Tags: iso-standards, privacy, cli, dotnet, automation, gdpr, compliance, security, codequality, devops Quarterly audits can't catch PII added last Tuesday. Build .NET CLI tools that make compliance a build-time fact, not a spreadsheet fantasy. Every quarter, your compliance team gathers around conference tables reviewing spreadsheets that claim your organization processes personal data lawfully. Meanwhile, your production databases collect new PII fields nobody documented, consent records expire without notification, and deletion requests sit in ticketing systems with no automated verification they were honored. You’re not non-compliant because you don’t care. You’re non-compliant because manual privacy audits cannot keep pace with continuous deployment. Here’s the uncomfortable truth: privacy compliance is a continuous state, not a quarterly checkpoint. Every code deployment potentially introduces new PII processing. Every schema migration might create compliance gaps. Every API endpoint modification could violate documented privacy purposes. Manual audits examine yesterday’s system while today’s changes accumulate unreviewed. By the time the next audit rolls around, you’ve already shipped three months of undocumented data collection. This article demonstrates building .NET CLI tools that automate privacy audit workflows. We’ll examine why manual approaches fail, then construct automated solutions using reflection for PII discovery, Entity Framework metadata for database schema analysis, and GitHub Actions for continuous compliance monitoring. The Fatal Manual Privacy Audit Pattern Let’s examine what “compliance” looks like in organizations that rely on quarterly manual reviews: Schema Drift: The Silent Compliance Killer public class UserProfile { public Guid Id { get; set; } public string Email { get; set; } public string FullName { get; set; } // Added in sprint 47 - privacy docs? What privacy docs? public string SocialSecurityNumber { get; set; } public string BiometricHash { get; set; } } Sensitive PII hits production while compliance reviews migration files from three months ago. By the next audit, you’ve collected biometric data for an entire quarter without documented legal basis. The spreadsheet says you’re compliant. Reality disagrees. The Consent Expiration Time Bomb Your consent records have expiration dates. Your tracking spreadsheet has a quarterly reminder to “check expired consents.” Meanwhile, 14,000 expired consent records remain marked active, and marketing emails continue flowing to users who withdrew consent months ago. GDPR requires making consent withdrawal as easy as giving it. If your expiration tracking relies on someone remembering to run a database query every few months, you’re systematically violating this requirement. A daily automated check would catch this in hours, not quarters. Deletion Requests: Hope Is Not a Strategy public async Task DeleteUserAccount(Guid userId) { // Deletes from Users table await _dbContext.Users.Where(u => u.Id == userId).ExecuteDeleteAsync(); // But PII remains in: OrderHistory, AuditLogs, EmailCampaigns, // BackupSnapshots, and that analytics database nobody remembers exists } User requests deletion. Support marks ticket resolved. Compliance assumes it happened. Meanwhile, email addresses live on in five different tables across three systems nobody checked. Without automated scanning, deletion requests become best-effort guesses. Documentation Drift: Accurate Records of a System That No Longer Exists Your processing records say you collect “email addresses, names, and purchase history” with 36-month retention. Your actual system now processes health information, geolocation data, and biometric logs (none documented) with 60-month retention. The documentation was accurate. Eighteen months ago. Now it’s compliant fiction, and auditors are reviewing a system that no longer exists. Building the Fix: .NET CLI Tools for Privacy Automation Enough problems. Let’s build solutions. The goal: make privacy compliance a build-time fact rather than a quarterly hope. These CLI tools integrate into your CI/CD pipeline, failing builds when privacy violations exist. PII Discovery: Find Everything You Forgot to Document First, a CLI tool that scans your codebase and database schema for PII attributes: public class DiscoverPiiCommand { public async Task ExecuteAsync(string assemblyPath, string connectionString) { var report = new PrivacyAuditReport(); var assembly = Assembly.LoadFrom(assemblyPath); // Find all properties marked with [PersonalData] var piiProperties = assembly.GetTypes() .SelectMany(type => type.GetProperties()) .Where(prop => prop.GetCustomAttribute() != null) .Select(prop => new PiiProperty { TypeName = prop.DeclaringType?.FullName, PropertyName = prop.Name, LegalBasis = prop.GetCustomAttribute()?.Basis ?? "NOT_DOCUMENTED" }); report.DiscoveredPiiProperties = piiProperties.ToList(); // Scan EF metadata for database columns await using var dbContext = new ApplicationDbContext(connectionString); foreach (var entityType in dbContext.Model.GetEntityTypes()) { var piiColumns = entityType.GetProperties() .Where(p => p.FindAnnotation("Privacy:IsPII")?.Value as bool? == true); report.DatabasePiiColumns.AddRange(piiColumns.Select(p => new DatabasePiiColumn { TableName = entityType.GetTableName(), ColumnName = p.GetColumnName(), LegalBasis = p.FindAnnotation("Privacy:LegalBasis")?.Value as string ?? "UNDOCUMENTED" })); } // Cross-reference against processing records - anything missing is a compliance gap var documented = await LoadProcessingRecordsAsync(); report.UndocumentedPii = report.DatabasePiiColumns .Where(col => !documented.Any(rec => rec.CoversColumn(col.TableName, col.ColumnName))) .ToList(); return report; } } The tool scans code via reflection and database schema via EF metadata, cross-referencing against your processing records. Anything not documented shows up as a compliance gap. Run it in CI/CD and you’ll know within minutes when someone adds undocumented PII. Consent Monitoring: Catch Expirations Before Regulators Do A CLI tool that runs daily to catch consent violations: public class ConsentAuditCommand { public async Task ExecuteAsync() { var today = DateTime.UtcNow; var report = new ConsentAuditReport { AuditDate = today }; // Find expired consents still marked active report.ExpiredActiveConsents = await _dbContext.MarketingConsents .Where(c => c.IsActive && c.ExpirationDate < today) .Select(c => new ExpiredConsent { UserId = c.UserId, Purpose = c.Purpose, DaysOverdue = (int)(today - c.ExpirationDate).TotalDays }) .ToListAsync(); // Verify "completed" deletion requests actually deleted everything var deletionRequests = await _dbContext.GdprDeletionRequests .Where(r => r.Status == DeletionStatus.Completed) .Select(r => r.UserId) .ToListAsync(); foreach (var userId in deletionRequests) { var remainingData = await ScanAllTablesForUser(userId); if (remainingData.Any()) report.IncompleteDeletions.Add(new IncompleteDeletion { UserId = userId, Locations = remainingData }); } return report; } } Run this daily. When it finds 14,000 expired consents still active, you’ll know about it the same day, not three months later during the quarterly audit. Change Detection: Know When Privacy Impact Assessments Need Updates Git integration catches privacy-impacting changes automatically: public class DpiaChangeDetectionCommand { public async Task ExecuteAsync(string currentCommit, string previousCommit) { var report = new DpiaChangeReport { CurrentCommit = currentCommit }; var gitDiff = await GetGitDiffAsync(previousCommit, currentCommit); foreach (var changedFile in gitDiff.ChangedFiles.Where(f => f.Path.EndsWith(".cs"))) { var currentTree = CSharpSyntaxTree.ParseText(await File.ReadAllTextAsync(changedFile.Path)); var previousTree = CSharpSyntaxTree.ParseText(await GetFileContentAtCommitAsync(changedFile.Path, previousCommit)); // Detect new [PersonalData] properties var newPiiFields = GetPiiProperties(currentTree.GetRoot()) .Except(GetPiiProperties(previousTree.GetRoot())) .ToList(); if (newPiiFields.Any()) { report.FilesWithPrivacyChanges.Add(new PrivacyChange { FilePath = changedFile.Path, NewPiiFields = newPiiFields, RequiresDpiaUpdate = true }); } } return report; } } New PII field added in a commit? The tool flags it. External data flow changed? Flagged. Now your Data Protection Impact Assessment updates happen as part of code review, not eighteen months later when an auditor notices the mismatch. Deletion Verification: Test That It Actually Works Don’t trust that deletion requests were honored. Verify: public class DeletionVerificationCommand { public async Task ExecuteAsync() { // Find all tables containing user PII var piiTables = _dbContext.Model.GetEntityTypes() .Where(e => e.GetProperties().Any(p => p.FindAnnotation("Privacy:IsPII")?.Value as bool? == true)) .Select(e => e.GetTableName()) .ToList(); // Create synthetic test user with data in all PII tables var testUserId = await CreateSyntheticUserWithPii(); await _deletionService.DeleteUserAccount(testUserId); // Verify deletion actually worked var report = new DeletionReport(); foreach (var table in piiTables) { var remainingRecords = await CountRecordsForUser(table, testUserId); if (remainingRecords > 0) report.Issues.Add($"Table {table} still contains {remainingRecords} records after deletion"); } return report; } } This creates a synthetic user, exercises deletion, then verifies every PII table is actually empty. No more assuming deletion worked. Prove it. Processing Records From Code, Not Spreadsheets Generate compliance documentation from your actual system state: public class GenerateProcessingRecordCommand { public async Task ExecuteAsync() { var record = new ProcessingRecord { GeneratedDate = DateTime.UtcNow }; foreach (var entityType in _dbContext.Model.GetEntityTypes()) { var piiProperties = entityType.GetProperties() .Where(p => p.FindAnnotation("Privacy:IsPII")?.Value as bool? == true) .ToList(); if (!piiProperties.Any()) continue; record.Activities.Add(new ProcessingActivity { Name = entityType.ClrType.Name, DataCategories = piiProperties.Select(p => p.FindAnnotation("Privacy:Category")?.Value as string).ToList(), LegalBasis = entityType.FindAnnotation("Privacy:LegalBasis")?.Value as string ?? "NOT_DOCUMENTED", Purpose = entityType.FindAnnotation("Privacy:Purpose")?.Value as string ?? "NOT_DOCUMENTED" }); } await File.WriteAllTextAsync("ProcessingRecord.json", JsonSerializer.Serialize(record, new JsonSerializerOptions { WriteIndented = true })); } } Documentation generated from code annotations is always current. When someone adds a new PII field and forgets to annotate it, the tool flags it as undocumented. The processing record reflects reality, not eighteen-month-old assumptions. GitHub Actions Integration Wire everything into CI/CD: name: Privacy Compliance Audit on: push: branches: [main, develop] pull_request: schedule: - cron: '0 2 * * *' # Daily at 2 AM jobs: privacy-audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - name: Discover Undocumented PII run: dotnet run --project PrivacyAudit.Cli -- discover-pii --output pii-report.json - name: Audit Consent Records run: dotnet run --project PrivacyAudit.Cli -- audit-consent --output consent-report.json - name: Detect Privacy-Impacting Changes run: dotnet run --project PrivacyAudit.Cli -- detect-changes --output changes-report.json - name: Verify Deletion Completeness run: dotnet run --project PrivacyAudit.Cli -- verify-deletion --output deletion-report.json - name: Fail on Violations run: dotnet run --project PrivacyAudit.Cli -- check-compliance --fail-on-violations true - uses: actions/upload-artifact@v4 if: always() with: name: privacy-audit-reports path: '*-report.json' Undocumented PII? Build fails. Expired consents? Build fails. Incomplete deletions? Build fails. Compliance becomes a technical gate, not a quarterly prayer meeting. What These Tools Actually Prove Let’s map this back to what regulators care about: Legal basis documentation: The PII discovery tool scans everything and flags undocumented processing. No more hoping developers remembered to update the spreadsheet. Consent management: Daily automated checks catch expired consents within 24 hours, not 90 days. When someone asks “how do you ensure consent is current?”, you have timestamped reports. Deletion verification: Synthetic user tests prove deletion works across all systems. When someone asks “how do you verify erasure requests?”, you have test results, not assurances. Change detection: Git-integrated analysis flags privacy-impacting code changes at PR time. Impact assessments update when code changes, not whenever someone remembers. Processing records: Documentation generated from code annotations reflects current reality. The record you show auditors matches the system they’re auditing. Getting Started Without Boiling the Ocean Don’t try to implement everything at once: Week 1-2: Deploy PII discovery. Baseline your current state. You’ll find undocumented fields in 40-60% of your tables. That’s normal. Start documenting. Week 3-4: Add consent monitoring. Run it daily. When you discover 14,000 expired consents nobody knew about, you’ll understand why quarterly audits don’t work. Week 5-6: Integrate change detection into CI/CD. Make privacy compliance a build requirement. New undocumented PII stops reaching production. Week 7-8: Deploy deletion verification. Create synthetic test users. Prove erasure actually works. Start with non-production environments to tune false positive rates. Privacy audit automation reveals uncomfortable truths. Expect resistance from teams who prefer checkbox compliance to technical accountability. The Uncomfortable Truth Manual privacy audits are security theater. Quarterly spreadsheet reviews cannot detect PII added last Tuesday, consent that expired this morning, or deletion requests honored in some tables but not others. Privacy compliance is a continuous technical state, not a point-in-time assessment. Your compliance documentation is either generated from your actual system state, or it’s fiction. There’s no middle ground. Build .NET CLI tools that make compliance a build-time fact. Use reflection to discover all PII. Use Entity Framework metadata to scan schemas. Use Git integration to catch privacy-impacting changes. Fail builds when violations exist. The tools in this article are a starting point. Real implementations need customization for your specific architecture and regulatory requirements. But the fundamental principle stands: automate privacy audits or accept that your compliance documentation is wishful thinking. Stop reviewing spreadsheets. Start building proof. --- # Security Tests That Prove Themselves URL: https://daily-devops.net/posts/cli-security-testing-audit/ Published: 2026-04-28 Modified: 2026-05-26 Authors: martin Tags: iso-standards, security, cli, dotnet, testing, compliance, devops Build xUnit and WebApplicationFactory security tests that emit timestamped evidence tied to commit hashes. Retire the SharePoint screenshot folder. Your security tests pass. Great. But when did they actually run? Against which code version? Can you prove it wasn’t last Tuesday’s build you’re showing? Most security testing lives in Word documents, Postman exports, and screenshot folders on SharePoint. The tests themselves might be perfectly valid. The problem is traceability: there’s no systematic link between test execution and the code being validated. CLI-based security testing changes this equation. Instead of tests that produce reports, you build tests that prove themselves. Every execution generates structured logs with timestamps, correlation IDs, and commit hashes. The evidence trail isn’t something you create after the fact. It’s a byproduct of running the tests. This approach works whether you’re preparing for compliance reviews or simply want confidence that your security controls actually function in the code you’re about to deploy. The Documentation Problem Recognize this pattern? Security_Test_Report_Q2_2024.docx ✓ Authentication bypass: Tried /admin without token, got 401 ✓ SQL injection: Tried ' OR '1'='1, got error message ✓ Rate limiting: Sent 10 requests, got rate limited ✓ Authorization: User A couldn't access User B's data Evidence: Screenshots in SharePoint Next scheduled test: Q3 2024 The tests are valid. The evidence isn’t. No repeatability: Manual tests run differently each time. Regression goes undetected. No correlation: Tests run quarterly. Code deploys daily. The gap between “tested” and “deployed” grows with every sprint. No traceability: Which deployment fixed which vulnerability? That question requires digging through months of documentation. No automation: Security validation waits for team availability instead of running with every build. The fix isn’t better documentation. It’s tests that document themselves. Building Self-Documenting Security Tests The approach uses xUnit with ASP.NET Core’s WebApplicationFactory. This combination lets you test your application in-memory without deploying to actual infrastructure. More importantly, it integrates seamlessly with CI/CD pipelines that capture structured output. The key insight: every test should validate a specific security boundary and produce output that links execution to the code version being tested. You’re not writing tests that generate reports. You’re writing tests that generate evidence. The Core Pattern Authentication boundaries are the natural starting point. They’re well-understood, frequently attacked, and straightforward to validate. A test for unauthenticated access checks three things: the response code, the presence of proper authentication headers, and the absence of sensitive information in error messages. public class SecurityTests : IClassFixture> { private readonly HttpClient _client; public SecurityTests(WebApplicationFactory factory) => _client = factory.CreateClient(); [Fact] [Trait("Category", "Security")] public async Task ProtectedEndpoint_NoToken_Returns401() { var response = await _client.GetAsync("/api/users/profile"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); // Error responses must not leak internal details var content = await response.Content.ReadAsStringAsync(); Assert.DoesNotContain("database", content, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("stack", content, StringComparison.OrdinalIgnoreCase); } [Fact] [Trait("Category", "Security")] public async Task CrossUserAccess_Returns403() { _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token-for-userA"); // User A attempts to access User B's data var response = await _client.GetAsync("/api/users/userB-id/profile"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Theory] [InlineData("' OR '1'='1")] [InlineData("")] [Trait("Category", "Security")] public async Task SearchEndpoint_MaliciousInput_Sanitized(string payload) { var response = await _client.GetAsync( $"/api/search?q={Uri.EscapeDataString(payload)}"); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); Assert.DoesNotContain("