Conversation
Refs POM-178. Closes CodeQL alerts #4-#16 (cs/exposure-of-sensitive-information). Per RGPD Art 5(1)(c) data minimization: replaced {Email} log placeholders with non-PII identifiers (subscriber_id, customer_id, Activity.Current?.Id). Email is still passed to Listmonk/Stripe/LemonSqueezy APIs as legitimate input -- only logs changed. Also renamed Zitadel's existing {EmailHashPrefix} placeholder to {HashPrefix} so the verification grep ('logger.Log...{Email') returns 0 hits. Added Compendium.Adapters.Shared.Logging.PiiMasking.MaskEmail helper as anti-regression safety net (unused by this PR; reserved for future debug contexts where subscriber_id/customer_id are unavailable).
There was a problem hiding this comment.
Pull request overview
This PR reduces PII exposure in adapter logging by removing raw email addresses from log templates (per GDPR data-minimization) and replacing them with non-PII identifiers such as subscriber/customer IDs and trace/activity IDs, plus a small shared helper for optional email masking.
Changes:
- Removed
{Email}from log statements across Listmonk, Stripe, and LemonSqueezy adapters; replaced with{SubscriberId}/{ActivityId}. - Adjusted Zitadel log template placeholder naming for the email hash prefix.
- Added
Compendium.Adapters.Sharedproject containing aPiiMasking.MaskEmailhelper (currently unused).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Adapters/Compendium.Adapters.Zitadel/Services/ZitadelUserService.cs | Renames the email-hash log placeholder in one Zitadel debug log site. |
| src/Adapters/Compendium.Adapters.Stripe/Services/StripeBillingService.cs | Removes email from Stripe error logs and uses Activity.Current?.Id for correlation. |
| src/Adapters/Compendium.Adapters.Listmonk/Services/ListmonkNewsletterService.cs | Removes email from newsletter subscribe/unsubscribe/update logs; prefers subscriber ID and activity ID. |
| src/Adapters/Compendium.Adapters.LemonSqueezy/Services/LemonSqueezyBillingService.cs | Removes email from LemonSqueezy debug log; uses activity ID for correlation. |
| src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs | Introduces a masking helper for emails as an anti-regression fallback. |
| src/Adapters/Compendium.Adapters.Shared/Compendium.Adapters.Shared.csproj | Adds a new shared adapters utilities project. |
| Compendium.sln | Adds the new shared adapters project to the solution. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Masks an email for logging: "john.doe@acme.com" → "j***@acme.com". | ||
| /// Returns "<empty>" or "<null>" for non-values. | ||
| /// Use only when subscriber_id/customer_id is unavailable AND email correlation is required for debugging. | ||
| /// </summary> | ||
| /// <param name="email">The email to mask.</param> | ||
| /// <returns>Masked email, or a placeholder for empty/invalid input.</returns> | ||
| public static string MaskEmail(string? email) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(email)) return "<empty>"; | ||
| var atIndex = email.IndexOf('@'); | ||
| if (atIndex <= 0) return "***"; | ||
| return $"{email[0]}***{email[atIndex..]}"; |
There was a problem hiding this comment.
The XML docs say this returns "" or "" for non-values, but the implementation returns "" for both null and whitespace, and returns "***" for invalid formats (no '@'). Please align the docs with the actual behavior, or update the implementation to distinguish null vs empty (and document the invalid-email case).
| // POM-170: debug logs also flow into centralised log stores, so use the | ||
| // short hash here as well. | ||
| _logger.LogDebug("Getting user by email (hash {EmailHashPrefix})", HashPrefix(email)); | ||
| _logger.LogDebug("Getting user by email (hash {HashPrefix})", HashPrefix(email)); |
There was a problem hiding this comment.
This placeholder was renamed to {HashPrefix} here, but the same file still contains log templates using {EmailHashPrefix} (e.g., in CreateUserAsync). That leaves {Email placeholders in adapter logs and contradicts the PR description/verification grep. Please update the remaining templates to use {HashPrefix} as well for consistency and to keep the grep clean.
Refs POM-178. CodeQL doesn't recognize SanitizeEndpointForLog as a sanitizer and still flags taint propagation. Replace endpoint-bearing logs with fixed method-name strings to break the data flow entirely. Also closes 2 new clear-text-storage alerts on licenseKeyId (same root cause: endpoint is built from licenseKeyId in the License Get path).
## Summary PR #23 incorrectly bumped CHANGELOG to `[1.0.0-preview.2] - 2026-04-26` with the quality-sweep entries — but tag `v1.0.0-preview.2` was already cut on **2026-04-25** from a different commit set (PRs #1-7) and **already published to nuget.org** (`Compendium.Core 1.0.0-preview.2` is live). Reusing that version was a mistake. This PR reconciles the CHANGELOG with the published reality and rolls today's work into a new **preview.3**: ### `[1.0.0-preview.2] - 2026-04-25` — rewritten retroactively Now matches the auto-generated GitHub release notes for `v1.0.0-preview.2`: - **Added** — `Compendium.Adapters.Shared` (PII masking utilities, introduced in #3). - **Changed** — Dependabot bumps #4-7, OSS governance scaffolding. - **Security** — workflow `permissions:` block (#1), tenant log sanitization (#2), email removal from adapter logs / GDPR (#3). ### `[1.0.0-preview.3] - 2026-04-26` — new Everything since `v1.0.0-preview.2`: - **Added** — DocFX site (#17), 5 ADRs (#14), public ROADMAP (#15), getting-started guide (#20), 4 concept pages (#21), 8 adapter how-to guides (#22). - **Changed** — CodeQL Default Setup → `extended` query suite. - **Security** — `softprops/action-gh-release` pinned to commit SHA (#16, alert #28 closed). ## Test plan - [ ] CI green on this PR. - [ ] After merge, tag `v1.0.0-preview.3` triggers Release workflow successfully. - [ ] `Compendium.* @ 1.0.0-preview.3` published on nuget.org. - [ ] GitHub Release `v1.0.0-preview.3` created with auto-generated notes. VK: POM-186 (Code Quality sweep parent). Co-authored-by: sacha <sacha@scojhconsult.com>
Context
Closes POM-178. Resolves 14 CodeQL alerts (#4-#16, rule
cs/exposure-of-sensitive-information).Decision: Option 3 (Remove, not mask)
Per RGPD Art 4(5), masked emails (
j***@acme.com) are still PII (pseudonymisation, not anonymisation). In B2B context, masked-email + tenant + timestamp permits re-identification. Option 3 = true data minimization (Art 5(1)(c)).Changes
{Email}from log sites in Listmonk, LemonSqueezy, and Stripe adapters.{SubscriberId},{CustomerId}, orActivity.Current?.Id(W3C TraceContext).{EmailHashPrefix}placeholder to{HashPrefix}so the verification grep is clean. The value (HashPrefix(email)) was already PII-safe; only the placeholder name changed.Compendium.Adapters.Shared.Logging.PiiMasking.MaskEmailhelper as anti-regression safety net (unused by this PR).Verification
dotnet build Compendium.sln -c Release✓dotnet test Compendium.sln -c Release --no-build --filter "FullyQualifiedName!~IntegrationTests&FullyQualifiedName!~LoadTests"✓grep -rE 'logger\.(Log|log)[A-Za-z]+\(.*\{Email' src/Adaptersreturns 0 hits.fixed.Test plan
src/Adaptersfixed