Skip to content

feat(privacy): Privacy & Secret Protection Engine - #19

Merged
jusso-dev merged 1 commit into
mainfrom
feat/privacy-secret-protection-engine
Jul 29, 2026
Merged

feat(privacy): Privacy & Secret Protection Engine#19
jusso-dev merged 1 commit into
mainfrom
feat/privacy-secret-protection-engine

Conversation

@jusso-dev

@jusso-dev jusso-dev commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Modular Privacy & Secret Protection Engine in Bower.Redaction (PrivacyEngine, ISensitiveDetector, policy actions, metadata).
  • Australian regulated identifiers with checksums (TFN, Medicare, IHI, ABN, ACN), plus CRN/passport/licence/DVA.
  • Financial (Luhn cards, BSB, IBAN, SWIFT, PayID), secrets (AWS/Azure/GCP/JWT/API keys/K8s/DB/env), crypto PEM material, optional classification markings.
  • Configurable actions: Allow / Remove / Replace / Mask / SHA-256 / HMAC / Encrypt / AlertOnly.
  • Event privacy metadata (detected kinds + actions only; never original values).
  • Unit tests, micro-benchmarks, docs (docs/privacy/privacy-secret-engine.md).

Test plan

  • dotnet restore Bower.sln --locked-mode
  • dotnet build Bower.sln --configuration Release
  • dotnet test Bower.sln --configuration Release (88 tests)
  • dotnet format Bower.sln --verify-no-changes
  • dotnet run --project tests/Bower.Benchmarks -c Release
  • CI green on PR

Summary by CodeRabbit

  • New Features
    • Added a configurable privacy and secret-protection engine for JSON and text.
    • Detects Australian identifiers, financial data, personal information, credentials, API keys, tokens, cryptographic material, and security markings.
    • Supports removal, masking, replacement, hashing, encryption, alerting, detector opt-in, and privacy metadata.
    • Added checksum validation to reduce false positives.
  • Documentation
    • Added comprehensive privacy-engine guidance and updated architecture documentation.
  • Tests
    • Added coverage for detection, validation, redaction policies, metadata, and compatibility.
  • Chores
    • Added a throughput benchmark project.

Modular deterministic detectors for AU identifiers, financial data,
credentials and crypto material with configurable policy actions and
privacy metadata. Extends Bower.Redaction; no AI at runtime.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a deterministic, policy-driven privacy engine with modular detectors for regulated identifiers, financial data, identity fields, secrets, crypto material, and security markings. JSON/text redaction, privacy metadata, compatibility facades, tests, documentation, and benchmarks are included.

Changes

Privacy engine and detector stack

Layer / File(s) Summary
Privacy contracts, policy, and transformations
src/Bower.Redaction/Privacy/*, src/Bower.Redaction/Validation/*
Defines detector contracts, stable IDs, checksum validators, privacy actions, configurable policies, metadata, and masking, hashing, HMAC, and encryption transformations.
PrivacyEngine traversal and compatibility integration
src/Bower.Redaction/Privacy/PrivacyEngine.cs, src/Bower.Redaction/JsonEventRedactor.cs, src/Bower.Redaction/SensitiveDataDetector.cs, src/Bower.Contracts/SecurityEventEnvelope.cs, src/Bower.Collector/Program.cs
Adds JSON/text traversal, match resolution, policy application, metadata generation, compatibility facades, envelope privacy context, and DI construction.
Built-in detector modules and catalog
src/Bower.Redaction/Detectors/*, src/Bower.Redaction/Privacy/DetectorCatalog.cs
Adds Australian, financial, identity, secret, crypto, field-name, and security-marking detectors with validation and classification behavior.
Validation, benchmarks, and repository integration
tests/Bower.UnitTests/*, tests/Bower.Benchmarks/*, Bower.sln, docs/*, AGENTS.md, src/Bower.Redaction/AGENTS.md
Adds unit tests, a throughput benchmark project, solution wiring, repository guidance, and privacy-engine documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • Bower#6 — Implements the configurable privacy engine, detector modules, redaction actions, and policies described by the issue.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: a new privacy and secret protection engine.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/privacy-secret-protection-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jusso-dev
jusso-dev merged commit 94b34c4 into main Jul 29, 2026
7 of 8 checks passed
@jusso-dev
jusso-dev deleted the feat/privacy-secret-protection-engine branch July 29, 2026 11:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (4)
src/Bower.Redaction/Detectors/RegexDetectorBase.cs (1)

29-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant string re-materialization per detector.

The caller (PrivacyEngine.ApplyDetectorsToValue) already holds the original string and only converts it to a ReadOnlySpan<char> before calling Detect. Here, text.ToString() immediately reallocates an identical string — repeated once per detector (~30 detectors) for every scanned field. Regex.EnumerateMatches(ReadOnlySpan<char>) (available since .NET 7; this repo already relies on .NET 9 APIs elsewhere, e.g. Convert.ToHexStringLower in PolicyApplicator.cs) can iterate matches directly over the span without allocating, using ValueMatch.Index/.Length instead of Match.Index/.Length.

♻️ Proposed refactor using EnumerateMatches
     public virtual void Detect(ReadOnlySpan<char> text, string path, ICollection<DetectionMatch> matches)
     {
-        // Regex operates on string; single allocation only when content present.
         if (text.IsEmpty)
         {
             return;
         }
 
-        string s = text.ToString();
-        foreach (Match match in Pattern.Matches(s))
+        foreach (ValueMatch match in Pattern.EnumerateMatches(text))
         {
-            if (!match.Success)
-            {
-                continue;
-            }
-
-            ReadOnlySpan<char> span = s.AsSpan(match.Index, match.Length);
+            ReadOnlySpan<char> span = text.Slice(match.Index, match.Length);
             bool validated = !RequiresValidation || Validate(span);
             if (RequiresValidation && !validated)
             {
                 continue;
             }
 
             matches.Add(new DetectionMatch(
                 Id,
                 Category,
                 match.Index,
                 match.Length,
                 validated,
                 SubKind(span)));
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/RegexDetectorBase.cs` around lines 29 - 30,
Update the detector loop in RegexDetectorBase.Detect to call
Pattern.EnumerateMatches directly with the existing ReadOnlySpan<char> instead
of materializing text.ToString(). Replace Match usage with the returned
ValueMatch type and preserve the current index and length handling through
ValueMatch.Index and ValueMatch.Length.
src/Bower.Redaction/SensitiveDataDetector.cs (1)

26-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting that maskInPlace is now ignored.

The parameter is discarded (_ = maskInPlace;) purely for backward compatibility. A one-line XML doc note on the parameter would help existing callers understand redaction is now always policy-driven regardless of this flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/SensitiveDataDetector.cs` around lines 26 - 39, Update
the XML documentation for SensitiveScanResult.ScanAndRedact to note that the
maskInPlace parameter is retained for backward compatibility but ignored, since
redaction is always policy-driven. Keep the existing _ = maskInPlace; behavior
unchanged.
src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs (1)

6-234: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add match timeouts to [GeneratedRegex] detectors that process untrusted payloads. ApiKeyDetector in SecretDetectors.cs already sets a TimeSpan.FromMilliseconds(50) timeout on its hand-rolled Regex instances, showing the team is aware regex on untrusted event content needs a timeout guard — but every [GeneratedRegex]-based detector across the catalog omits matchTimeoutMilliseconds. GeneratedRegexAttribute supports a (pattern, options, matchTimeoutMilliseconds) constructor overload, so this is a low-effort, drop-in fix.

  • src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs#L6-L234: add matchTimeoutMilliseconds to AwsRegex, AzureRegex, EntraRegex, GcpRegex, JwtRegex, OAuthRegex, K8sRegex, DockerRegex, DbRegex (line 219 combines [^;]+; with unbounded .*, the riskiest pattern in this file), and EnvRegex.
  • src/Bower.Redaction/Detectors/Financial/FinancialDetectors.cs#L101-L102: add a timeout to CardRegex, which repeats (?:\d[ -]*?){13,19} over attacker-controlled text.
  • src/Bower.Redaction/Detectors/Crypto/CryptographicMaterialDetector.cs#L53-L56: add a timeout to PemRegex, whose lazy [\s\S]+? spans across 4 alternatives on unbounded input.
♻️ Example fix pattern
-    [GeneratedRegex(
-        @"(?:Server|Data Source|Host)=[^;]+;.*(?:Password|Pwd)=[^;]+|jdbc:[a-z0-9]+:\/\/[^\s]+|postgres(?:ql)?:\/\/[^\s]+|mysql:\/\/[^\s]+|mongodb(?:\+srv)?:\/\/[^\s]+|Driver=\{[^}]+\}.*Pwd=",
-        RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
+    [GeneratedRegex(
+        @"(?:Server|Data Source|Host)=[^;]+;.*(?:Password|Pwd)=[^;]+|jdbc:[a-z0-9]+:\/\/[^\s]+|postgres(?:ql)?:\/\/[^\s]+|mysql:\/\/[^\s]+|mongodb(?:\+srv)?:\/\/[^\s]+|Driver=\{[^}]+\}.*Pwd=",
+        RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 100)]
     private static partial Regex DbRegex();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs` around lines 6 -
234, Add a consistent finite match timeout to every affected generated regex:
AwsRegex, AzureRegex, EntraRegex, GcpRegex, JwtRegex, OAuthRegex, K8sRegex,
DockerRegex, DbRegex, and EnvRegex in
src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs (lines 6-234),
CardRegex in src/Bower.Redaction/Detectors/Financial/FinancialDetectors.cs
(lines 101-102), and PemRegex in
src/Bower.Redaction/Detectors/Crypto/CryptographicMaterialDetector.cs (lines
53-56). Use the GeneratedRegexAttribute overload with matchTimeoutMilliseconds,
preserving each existing pattern and options.
src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs (1)

7-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make TfnDetector invoke the overridden validation hook consistently.

RegexDetectorBase.Detect() calls Validate() when RequiresValidation is true, but TfnDetector replaces that default path with its own Detect(), creating a separate duplicate checksum check and leaving Validate() unreachable. Either keep the base scanning path and return the captured digit span for validation, or remove the Validate() override entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs`
around lines 7 - 57, Update TfnDetector to use RegexDetectorBase.Detect() so its
RequiresValidation path invokes the existing Validate() override, removing the
duplicate custom Detect() checksum logic. Ensure the detector still validates
the captured digit group rather than the optional context text, using the base
class’s supported capture-span behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/privacy/privacy-secret-engine.md`:
- Around line 3-14: Establish a single redaction boundary across both
documentation sites: in docs/privacy/privacy-secret-engine.md lines 3-14, revise
the lifecycle text and diagram to place the Privacy & Secret Engine before typed
parsing; in docs/architecture/overview.md lines 10-13, clarify that typed
parsing creates typed event contracts only after raw JSON redaction.
- Around line 137-140: Update the detector extension steps to require every new
detector to implement ISensitiveDetector; describe IFieldNameDetector only as an
optional additional interface for detectors needing field-name matching.
Preserve the existing registration options through
DetectorCatalog.CreateDefaultValueDetectors() or PrivacyEngine constructor
injection.

In `@src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs`:
- Around line 59-89: Update CrnDetector.Detect so CRN regex matches are not
marked Validated: true, since no checksum validation is performed; set the match
result to the unvalidated state while preserving the existing format matching,
span selection, and early-return behavior.

In `@src/Bower.Redaction/Detectors/RegexDetectorBase.cs`:
- Around line 21-52: The Detect method’s Pattern.Matches(s) call allows
unbounded regex execution on event text. Update the regex matching used by
RegexDetectorBase to enforce a bounded per-detector or shared timeout, or apply
RegexOptions.NonBacktracking where supported, while preserving existing match
validation and DetectionMatch creation behavior.

In `@src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs`:
- Around line 47-57: Resolve the generic JWT overlap between EntraTokenDetector
and JwtDetector without relying on detector array order. Update the detector
matching or PrivacyEngine overlap resolution to apply a stable detector/policy
priority, or make EntraRegex exclusive to Entra-specific tokens while preserving
device_code detection. Ensure identical start and length matches always produce
deterministic actions.

In `@src/Bower.Redaction/Privacy/PrivacyEngine.cs`:
- Around line 122-147: Update the field-name match branch in WalkObject so Allow
and AlertOnly findings continue into ApplyDetectorsToValue, WalkObject, or
WalkArray for nested scanning instead of unconditionally continuing. For other
actions, route the property value through the existing PolicyApplicator
transformation flow used by normal value matches, including string values,
rather than always removing the property, while preserving accurate action
metadata and existing traversal behavior.
- Around line 171-191: Update WalkArray to detect JsonArray elements and
recursively call WalkArray with the nested array, its itemPath, and the existing
findings collection before processing object or scalar values. Add an
adversarial test covering sensitive data inside nested arrays, including nested
objects where applicable, and verify all detectors redact the values before
persistence.

In `@src/Bower.Redaction/Privacy/PrivacyPolicy.cs`:
- Line 80: Update the default policy mapping for DetectorIds.Tfn to use
PrivacyAction.Mask or Remove instead of PrivacyAction.Sha256, while preserving
HMAC only for configurations with a mandatory managed key. Add coverage
verifying CreateDefault() never produces a TFN SHA-256 digest, and ensure
redaction failures are treated as security failures.
- Around line 14-35: Update the PrivacyPolicy collection properties, including
DetectorActions, DisabledDetectors, OptInDetectors, and EnabledOptInDetectors,
to defensively copy incoming dictionaries/sets into policy-owned immutable or
read-only collections using ordinal comparison. Ensure later mutations to
caller-owned collections cannot affect detector evaluation or actions, and add
an adversarial test that mutates each source collection after policy/engine
construction and verifies scan behavior remains unchanged.

---

Nitpick comments:
In `@src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs`:
- Around line 7-57: Update TfnDetector to use RegexDetectorBase.Detect() so its
RequiresValidation path invokes the existing Validate() override, removing the
duplicate custom Detect() checksum logic. Ensure the detector still validates
the captured digit group rather than the optional context text, using the base
class’s supported capture-span behavior.

In `@src/Bower.Redaction/Detectors/RegexDetectorBase.cs`:
- Around line 29-30: Update the detector loop in RegexDetectorBase.Detect to
call Pattern.EnumerateMatches directly with the existing ReadOnlySpan<char>
instead of materializing text.ToString(). Replace Match usage with the returned
ValueMatch type and preserve the current index and length handling through
ValueMatch.Index and ValueMatch.Length.

In `@src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs`:
- Around line 6-234: Add a consistent finite match timeout to every affected
generated regex: AwsRegex, AzureRegex, EntraRegex, GcpRegex, JwtRegex,
OAuthRegex, K8sRegex, DockerRegex, DbRegex, and EnvRegex in
src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs (lines 6-234),
CardRegex in src/Bower.Redaction/Detectors/Financial/FinancialDetectors.cs
(lines 101-102), and PemRegex in
src/Bower.Redaction/Detectors/Crypto/CryptographicMaterialDetector.cs (lines
53-56). Use the GeneratedRegexAttribute overload with matchTimeoutMilliseconds,
preserving each existing pattern and options.

In `@src/Bower.Redaction/SensitiveDataDetector.cs`:
- Around line 26-39: Update the XML documentation for
SensitiveScanResult.ScanAndRedact to note that the maskInPlace parameter is
retained for backward compatibility but ignored, since redaction is always
policy-driven. Keep the existing _ = maskInPlace; behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83c179ad-5835-4d55-9cf2-eea199858640

📥 Commits

Reviewing files that changed from the base of the PR and between 68ee0ed and 489e511.

📒 Files selected for processing (32)
  • AGENTS.md
  • Bower.sln
  • docs/architecture/overview.md
  • docs/privacy/privacy-secret-engine.md
  • src/Bower.Collector/Program.cs
  • src/Bower.Contracts/SecurityEventEnvelope.cs
  • src/Bower.Redaction/AGENTS.md
  • src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs
  • src/Bower.Redaction/Detectors/Classification/SecurityMarkingDetector.cs
  • src/Bower.Redaction/Detectors/Crypto/CryptographicMaterialDetector.cs
  • src/Bower.Redaction/Detectors/FieldNameSecretDetector.cs
  • src/Bower.Redaction/Detectors/Financial/FinancialDetectors.cs
  • src/Bower.Redaction/Detectors/Identity/IdentityDetectors.cs
  • src/Bower.Redaction/Detectors/RegexDetectorBase.cs
  • src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs
  • src/Bower.Redaction/JsonEventRedactor.cs
  • src/Bower.Redaction/Privacy/DetectionMatch.cs
  • src/Bower.Redaction/Privacy/DetectorCatalog.cs
  • src/Bower.Redaction/Privacy/DetectorIds.cs
  • src/Bower.Redaction/Privacy/ISensitiveDetector.cs
  • src/Bower.Redaction/Privacy/PolicyApplicator.cs
  • src/Bower.Redaction/Privacy/PrivacyAction.cs
  • src/Bower.Redaction/Privacy/PrivacyEngine.cs
  • src/Bower.Redaction/Privacy/PrivacyMetadata.cs
  • src/Bower.Redaction/Privacy/PrivacyPolicy.cs
  • src/Bower.Redaction/SensitiveDataDetector.cs
  • src/Bower.Redaction/Validation/ChecksumAlgorithms.cs
  • tests/Bower.Benchmarks/Bower.Benchmarks.csproj
  • tests/Bower.Benchmarks/Program.cs
  • tests/Bower.Benchmarks/packages.lock.json
  • tests/Bower.UnitTests/PrivacyEngineTests.cs
  • tests/Bower.UnitTests/SensitiveDataDetectorTests.cs

Comment on lines +3 to +14
Bower's privacy engine inspects every event **after parse and before
persistence / normalisation**. It is a core control for preventing accidental
leakage of regulated Australian identifiers, credentials and cryptographic
material into downstream SIEM platforms.

```
Raw Event → Parser → Privacy & Secret Engine → Normalisation → Output
├── Detect
├── Validate
├── Classify
└── Apply Policy
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Define one redaction boundary across the architecture docs.

The privacy guide places the engine after parsing, while the architecture guide places it before typed parsing. This ambiguity can lead integrations to parse sensitive raw data before the control runs.

  • docs/privacy/privacy-secret-engine.md#L3-L14: change the lifecycle text and diagram to place redaction before typed parsing.
  • docs/architecture/overview.md#L10-L13: clarify that “typed parsing” means creation of typed event contracts after raw JSON redaction.

Based on learnings, behavior changes require documentation updates.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 8-8: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 2 files
  • docs/privacy/privacy-secret-engine.md#L3-L14 (this comment)
  • docs/architecture/overview.md#L10-L13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/privacy/privacy-secret-engine.md` around lines 3 - 14, Establish a
single redaction boundary across both documentation sites: in
docs/privacy/privacy-secret-engine.md lines 3-14, revise the lifecycle text and
diagram to place the Privacy & Secret Engine before typed parsing; in
docs/architecture/overview.md lines 10-13, clarify that typed parsing creates
typed event contracts only after raw JSON redaction.

Source: Learnings

Comment on lines +137 to +140
1. Implement `ISensitiveDetector` (value scan) and/or `IFieldNameDetector`.
2. Pass detectors into `PrivacyEngine` constructor **or** extend
`DetectorCatalog.CreateDefaultValueDetectors()`.
3. Set per-id actions / disable flags on `PrivacyPolicy`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Require ISensitiveDetector for every detector extension.

and/or IFieldNameDetector permits a field-name-only implementation. Require ISensitiveDetector, with IFieldNameDetector as an additional interface where needed.

Proposed documentation fix
-1. Implement `ISensitiveDetector` (value scan) and/or `IFieldNameDetector`.
+1. Implement `ISensitiveDetector`; additionally implement `IFieldNameDetector`
+   when the detector needs field-name matching.

As per coding guidelines, “New detectors must implement ISensitiveDetector and register through DetectorCatalog or constructor injection.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. Implement `ISensitiveDetector` (value scan) and/or `IFieldNameDetector`.
2. Pass detectors into `PrivacyEngine` constructor **or** extend
`DetectorCatalog.CreateDefaultValueDetectors()`.
3. Set per-id actions / disable flags on `PrivacyPolicy`.
1. Implement `ISensitiveDetector`; additionally implement `IFieldNameDetector`
when the detector needs field-name matching.
2. Pass detectors into `PrivacyEngine` constructor **or** extend
`DetectorCatalog.CreateDefaultValueDetectors()`.
3. Set per-id actions / disable flags on `PrivacyPolicy`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/privacy/privacy-secret-engine.md` around lines 137 - 140, Update the
detector extension steps to require every new detector to implement
ISensitiveDetector; describe IFieldNameDetector only as an optional additional
interface for detectors needing field-name matching. Preserve the existing
registration options through DetectorCatalog.CreateDefaultValueDetectors() or
PrivacyEngine constructor injection.

Source: Coding guidelines

Comment on lines +59 to +89
public sealed partial class CrnDetector : RegexDetectorBase
{
public override string Id => DetectorIds.Crn;
public override string Category => DetectorCategories.Australian;
protected override Regex Pattern => CrnRegex();

[GeneratedRegex(
@"\b(?:CRN|Customer\s*Reference\s*Number|Centrelink)?[:\s#-]*(\d{3}[\s-]?\d{3}[\s-]?\d{3}[A-Za-z])\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CrnRegex();

public override void Detect(ReadOnlySpan<char> text, string path, ICollection<DetectionMatch> matches)
{
if (text.IsEmpty)
{
return;
}

string s = text.ToString();
foreach (Match match in Pattern.Matches(s))
{
Group g = match.Groups.Count > 1 ? match.Groups[1] : match.Groups[0];
if (!g.Success)
{
continue;
}

matches.Add(new DetectionMatch(Id, Category, g.Index, g.Length, Validated: true));
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

CrnDetector marks matches Validated: true with no checksum check.

Unlike TfnDetector, MedicareDetector, AbnDetector, AcnDetector, and IhiDetector in this same file — which all gate Validated: true behind a ChecksumAlgorithms.IsValidXxx(...) call — CrnDetector.Detect() unconditionally sets Validated: true (line 86) purely from a regex format match. The CRN's trailing letter is a checksum, but per Services Australia/Centrelink documentation it's generated by an internal, undocumented algorithm, so it's plausible no IsValidCrn exists — meaning this "validated" match is actually just a format guess. Downstream consumers of DetectionMatch.Validated (confidence scoring, policy decisions) will be misled into treating an unverified heuristic match as checksum-confirmed.

🐛 Proposed fix
-            matches.Add(new DetectionMatch(Id, Category, g.Index, g.Length, Validated: true));
+            matches.Add(new DetectionMatch(Id, Category, g.Index, g.Length, Validated: false));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public sealed partial class CrnDetector : RegexDetectorBase
{
public override string Id => DetectorIds.Crn;
public override string Category => DetectorCategories.Australian;
protected override Regex Pattern => CrnRegex();
[GeneratedRegex(
@"\b(?:CRN|Customer\s*Reference\s*Number|Centrelink)?[:\s#-]*(\d{3}[\s-]?\d{3}[\s-]?\d{3}[A-Za-z])\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CrnRegex();
public override void Detect(ReadOnlySpan<char> text, string path, ICollection<DetectionMatch> matches)
{
if (text.IsEmpty)
{
return;
}
string s = text.ToString();
foreach (Match match in Pattern.Matches(s))
{
Group g = match.Groups.Count > 1 ? match.Groups[1] : match.Groups[0];
if (!g.Success)
{
continue;
}
matches.Add(new DetectionMatch(Id, Category, g.Index, g.Length, Validated: true));
}
}
}
public sealed partial class CrnDetector : RegexDetectorBase
{
public override string Id => DetectorIds.Crn;
public override string Category => DetectorCategories.Australian;
protected override Regex Pattern => CrnRegex();
[GeneratedRegex(
@"\b(?:CRN|Customer\s*Reference\s*Number|Centrelink)?[:\s#-]*(\d{3}[\s-]?\d{3}[\s-]?\d{3}[A-Za-z])\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CrnRegex();
public override void Detect(ReadOnlySpan<char> text, string path, ICollection<DetectionMatch> matches)
{
if (text.IsEmpty)
{
return;
}
string s = text.ToString();
foreach (Match match in Pattern.Matches(s))
{
Group g = match.Groups.Count > 1 ? match.Groups[1] : match.Groups[0];
if (!g.Success)
{
continue;
}
matches.Add(new DetectionMatch(Id, Category, g.Index, g.Length, Validated: false));
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/Australian/AustralianIdentifierDetectors.cs`
around lines 59 - 89, Update CrnDetector.Detect so CRN regex matches are not
marked Validated: true, since no checksum validation is performed; set the match
result to the unvalidated state while preserving the existing format matching,
span selection, and early-return behavior.

Comment on lines +21 to +52
public virtual void Detect(ReadOnlySpan<char> text, string path, ICollection<DetectionMatch> matches)
{
// Regex operates on string; single allocation only when content present.
if (text.IsEmpty)
{
return;
}

string s = text.ToString();
foreach (Match match in Pattern.Matches(s))
{
if (!match.Success)
{
continue;
}

ReadOnlySpan<char> span = s.AsSpan(match.Index, match.Length);
bool validated = !RequiresValidation || Validate(span);
if (RequiresValidation && !validated)
{
continue;
}

matches.Add(new DetectionMatch(
Id,
Category,
match.Index,
match.Length,
validated,
SubKind(span)));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)(Program\.cs|PolicyApplicator\.cs|RegexDetectorBase\.cs|.*Detector.*\.cs|.*Redaction.*\.cs|).*\.(cs|csproj|json|yaml|yml)$' | sed -n '1,200p'

echo
echo "== RegexDetectorBase.cs =="
if [ -f src/Bower.Redaction/Detectors/RegexDetectorBase.cs ]; then
  cat -n src/Bower.Redaction/Detectors/RegexDetectorBase.cs
fi

echo
echo "== related base/detector constructors and Pattern declaration =="
rg -n "class RegexDetectorBase|Record|Record<|protected .*Pattern|Regex\(|Pattern\." src/Bower.Redaction/Detectors -g '*.cs' || true

echo
echo "== Program.cs relevant HTTP endpoint snippets =="
for f in $(git ls-files | rg 'Program\.cs$'); do
  echo "--- $f"
  cat -n "$f" | sed -n '1,240p'
done

echo
echo "== payload cap references =="
rg -n "1048576|1 MB|content-length|Length|Max|events|v1/events|Json" src -g '*.cs' | sed -n '1,240p'

Repository: jusso-dev/Bower

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files =="
git ls-files | sed -n '1,220p'

echo
echo "== RegexDetectorBase if present =="
if git ls-files | grep -qx 'src/Bower.Redaction/Detectors/RegexDetectorBase.cs'; then
  wc -l src/Bower.Redaction/Detectors/RegexDetectorBase.cs
  cat -n src/Bower.Redaction/Detectors/RegexDetectorBase.cs
fi

Repository: jusso-dev/Bower

Length of output: 10986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== global.json target SDK =="
cat -n global.json | sed -n '1,120p'

echo
echo "== Bower.Redaction project SDK packages =="
cat -n src/Bower.Redaction/Bower.Redaction.csproj | sed -n '1,160p'

echo
echo "== JsonEventRedactor payload cap implementation =="
if [ -f src/Bower.Redaction/JsonEventRedactor.cs ]; then
  ast-grep outline src/Bower.Redaction/JsonEventRedactor.cs --view expanded
  cat -n src/Bower.Redaction/JsonEventRedactor.cs | sed -n '1,260p'
fi

echo
echo "== Redaction scanning entry paths =="
rg -n "JsonEventRedactor|MaximumPayloadBytes|SetJsonOptions|Detect\\(|Redact|DetectAllMatch|PrivacyEngine" src/Bower.Redaction tests/Bower.UnitTests -g '*.cs' | sed -n '1,240p'

Repository: jusso-dev/Bower

Length of output: 20623


🌐 Web query:

.NET Regex Matching Timeout Regular expression matching times out Regex.IsMatch overload matchTimeout documentation

💡 Result:

In.NET, the matchTimeout parameter in Regex methods is a critical tool used to prevent "catastrophic backtracking," a performance issue where complex regular expressions take an excessive amount of time to process specific input strings [1][2]. When a matching operation exceeds the specified timeout interval, the engine throws a RegexMatchTimeoutException [1][3]. Recommended Overload The recommended static method for performing a pattern match with a timeout is Regex.IsMatch(String, String, RegexOptions, TimeSpan) [1][4]. This overload allows you to explicitly define a TimeSpan limit for the operation [1][5]. How Timeouts Work 1. Implementation: The matchTimeout parameter overrides any default timeout values defined for the application domain [1][6]. 2. Default Behavior: If no timeout is explicitly set, the default is Regex.InfiniteMatchTimeout, meaning the operation will not time out [5][7]. 3. Global Setting: You can set a default timeout for all Regex operations within an application domain by using AppDomain.SetData with the key "REGEX_DEFAULT_MATCH_TIMEOUT" and providing a TimeSpan value [5][7]. 4. Mechanics: The regex engine performs periodic checks to see if the timeout interval has elapsed. If it has, it throws a RegexMatchTimeoutException during the next check [5][7]. This ensures the engine does not spend more than a predictable amount of time (often O(n) relative to input length) on any single operation [2]. Best Practices * Use Timeouts: Always set a timeout when processing untrusted input to mitigate potential Regex Denial-of-Service (ReDoS) attacks [8][2]. * Handle Exceptions: If a RegexMatchTimeoutException occurs, it often indicates either excessive backtracking or a timeout value that is too low for the current system load [3][9]. The engine maintains its state after the exception, so you may choose to retry or abandon the operation [3][9]. * Non-Backtracking: In modern.NET (especially.NET 7 and later), consider using the RegexOptions.NonBacktracking flag, which uses an automata-based engine that guarantees linear-time performance and avoids backtracking issues entirely [8][2]. For further details, refer to the official documentation on the Regex.IsMatch method and Best Practices for Regular Expressions [1][8].

Citations:


Avoid unbounded regex execution on event text.

Pattern.Matches(s) runs with no matchTimeout, and event payload text is scanned before redaction in /v1/events. A crafted backtrack-heavy value can hang a request-processing thread even under the 1 MB payload cap; add per-detector or shared bounded timeouts, e.g. matchTimeoutMilliseconds, EnumerateMatches(..., timeout), or RegexOptions.NonBacktracking where supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/RegexDetectorBase.cs` around lines 21 - 52, The
Detect method’s Pattern.Matches(s) call allows unbounded regex execution on
event text. Update the regex matching used by RegexDetectorBase to enforce a
bounded per-detector or shared timeout, or apply RegexOptions.NonBacktracking
where supported, while preserving existing match validation and DetectionMatch
creation behavior.

Source: Path instructions

Comment on lines +47 to +57
public sealed partial class EntraTokenDetector : RegexDetectorBase
{
public override string Id => DetectorIds.Entra;
public override string Category => DetectorCategories.Secrets;
protected override Regex Pattern => EntraRegex();

[GeneratedRegex(
@"\b(?:eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b|device_code\s*[:=]\s*[A-Za-z0-9\-_]{10,}",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex EntraRegex();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm how PrivacyEngine resolves overlapping matches from different detector IDs
# at the same span, and whether outcome is deterministic regardless of detector order.
rg -n "Overlap|Resolve|OrderBy" src/Bower.Redaction/Privacy/PrivacyEngine.cs

Repository: jusso-dev/Bower

Length of output: 665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SecretDetectors relevant regions =="
sed -n '1,130p' src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs | cat -n

echo
echo "== PrivacyEngine overlap resolution region =="
sed -n '180,260p' src/Bower.Redaction/Privacy/PrivacyEngine.cs | cat -n

echo
echo "== Deterministic match-selection probe from source snippets =="
python3 - <<'PY'
import re
from pathlib import Path

secret = Path('src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs').read_text()
ent = re.search(r'EntraRegex\(\)\)\s*\n\s*private static partial Regex EntraRegex\(\)', secret).group(0)
jwt = re.search(r'JwtRegex\(\)\)\s*\n\s*private static partial Regex JwtRegex\(\)', secret).group(0)

def extract(s):
    m=re.search(r'GeneratedRegex\(\s*@?"([^"\n]+)"', s|'.')
    return re.sub(r'\s*\\n[^\n,]+', '', m.group(1)) if m else None

print('Entra pattern:', extract(ent))
print('Jwt pattern:', extract(jwt))

# Simple static regex containment check: a token matching A+B should match both if each pattern contains the other.
patterns = {
  'EntraTokenDetector': extract(ent),
  'JwtDetector': extract(jwt)
}
for name_a, pat_a in patterns.items():
    for name_b, pat_b in patterns.items():
        if name_a==name_b: continue
        # If the token payload of pattern B is captured by A's regex, same span occurs.
        try:
            rgx=re.compile(pat_a, re.IGNORECASE|re.CULTUREINVARIANT|re.ASCII)
            # Extract JWT token string from B pattern if it appears as fixed substring or simple class concat.
            print(f'Contains {name_b} literal string in {name_a}: {pat_b in pat_a}')
        except Exception as e:
            print(f'Compiled check failed for {name_a}: {type(e).__name__}: {e}')
PY

Repository: jusso-dev/Bower

Length of output: 9006


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PrivacyEngine overlap selection tie-break and action mapping =="
sed -n '200,260p' src/Bower.Redaction/Privacy/PrivacyEngine.cs | cat -n

echo
echo "== detector order construction =="
rg -n "new \[\]|Add\\(|FindAll|GetAll|IEnumerable<ISensitiveDetector>|detectors\\s*:" src/Bower.Redaction src -g '*.cs' | head -80

echo
echo "== pattern overlap simulation =="
python3 - <<'PY'
import re
patterns = {
    'EntraTokenDetector': r'\b(?:eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b|device_code\s*[:=]\s*[A-Za-z0-9\-_]{10,}',
    'JwtDetector': r'\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b'
}
samples = [
    'eyJpayload.header.sig',
    'eyJeyJeyJ.header.sig',
    'x device_code abcabcabc1234567890'
]
for s in samples:
    print(s)
    for name, pat in patterns.items():
        m = re.search(pat, s, flags=re.I|re.CIL|re.CULTUREINVARIANT)
        print(' ', name, s[m.start():m.end()] if m else 'no match')
    # Same-length token ordering examples
    token = 'eyJeyJeyJ.eyJeyJeyJ.eyJeyJeyJ'
    print('same_len_token', re.search(r'\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b', token).start(),
          re.search(r'\b(?:eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b|device_code\s*[:=]\s*[A-Za-z0-9\-_]{10,}', token).start())
PY

Repository: jusso-dev/Bower

Length of output: 12644


Resolve the JWT overlap deterministically

EntraTokenDetector and JwtDetector both match generic eyJ...\.eyJ...\.<signature> tokens, and PrivacyEngine only breaks same-length same-start overlaps by detector array order. Use a stable tie-breaker such as detector/policy priority, or make one detector’s pattern exclusive to avoid order-dependent actions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Detectors/Secrets/SecretDetectors.cs` around lines 47 -
57, Resolve the generic JWT overlap between EntraTokenDetector and JwtDetector
without relying on detector array order. Update the detector matching or
PrivacyEngine overlap resolution to apply a stable detector/policy priority, or
make EntraRegex exclusive to Entra-specific tokens while preserving device_code
detection. Ensure identical start and length matches always produce
deterministic actions.

Source: Coding guidelines

Comment on lines +122 to +147
private void WalkObject(JsonObject value, string parentPath, List<AppliedFinding> findings)
{
foreach ((string propertyName, JsonNode? child) in value.ToArray())
{
string path = $"{parentPath}.{propertyName}";
string normalized = NormalizeFieldName(propertyName);

if (policy.IsDetectorEnabled(fieldNameDetector.Id) &&
fieldNameDetector.MatchesFieldName(normalized))
{
PrivacyAction action = policy.ResolveAction(fieldNameDetector.Id);
findings.Add(new AppliedFinding(
fieldNameDetector.Id,
fieldNameDetector.Category,
path,
action,
Validated: true,
SubKind: null));

if (action is not (PrivacyAction.Allow or PrivacyAction.AlertOnly))
{
value.Remove(propertyName);
}

continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Field-name-secret match unconditionally skips value scanning, and collapses every non-allow action to full removal.

Two related problems in this block:

  1. continue at Line 146 fires regardless of the resolved action. When action is Allow or AlertOnly (field-name match kept, e.g. an admin override for a name-matched-but-benign field), the code never calls ApplyDetectorsToValue/WalkObject/WalkArray on that field's value. Any real credit card, TFN, JWT, etc. nested inside an allow-listed field name is never independently scanned by the other detectors, even though those detectors are otherwise supposed to be "independently enableable" per policy.
  2. When action is anything other than Allow/AlertOnly (Replace, Mask, Sha256, Hmac, Encrypt), Line 143 always fully deletes the property instead of routing through PolicyApplicator (as normal value matches do at Line 248). The metadata built from this finding (via PolicyApplicator.ActionLabel) will report e.g. "Masked" or "SHA256" even though the actual effect was a full removal — the privacy metadata no longer accurately reflects what happened to the data.

At minimum, for string-valued fields, the applicator's actual transformation should be applied instead of an unconditional remove; and for Allow/AlertOnly, the value should still be recursed into so other detectors can flag/act on nested sensitive content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Privacy/PrivacyEngine.cs` around lines 122 - 147, Update
the field-name match branch in WalkObject so Allow and AlertOnly findings
continue into ApplyDetectorsToValue, WalkObject, or WalkArray for nested
scanning instead of unconditionally continuing. For other actions, route the
property value through the existing PolicyApplicator transformation flow used by
normal value matches, including string values, rather than always removing the
property, while preserving accurate action metadata and existing traversal
behavior.

Source: Path instructions

Comment on lines +171 to +191
private void WalkArray(JsonArray array, string path, List<AppliedFinding> findings)
{
for (int index = 0; index < array.Count; index++)
{
string itemPath = $"{path}[{index}]";
if (array[index] is JsonObject nested)
{
WalkObject(nested, itemPath, findings);
}
else if (array[index] is JsonValue arrayValue &&
arrayValue.TryGetValue(out string? arrayText) &&
arrayText is not null)
{
string redacted = ApplyDetectorsToValue(arrayText, itemPath, findings);
if (redacted != arrayText)
{
array[index] = redacted;
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Nested JSON arrays are never traversed — sensitive data inside them escapes redaction.

WalkArray handles JsonObject and string JsonValue elements, but has no branch for array[index] is JsonArray. A payload like {"a": [["user@example.com"]]} or {"a": [[{"cardNumber": "4111..."}]]} will silently skip all detectors for the inner array, so credit card numbers, TFNs, JWTs, etc. nested one level deeper than expected are persisted unredacted. Given this engine's stated contract ("Operates after parse / before persistence") and the guideline that redaction failures are security failures, this is a real leak path, not just an edge case.

🔒 Proposed fix: recurse into nested arrays
     private void WalkArray(JsonArray array, string path, List<AppliedFinding> findings)
     {
         for (int index = 0; index < array.Count; index++)
         {
             string itemPath = $"{path}[{index}]";
             if (array[index] is JsonObject nested)
             {
                 WalkObject(nested, itemPath, findings);
             }
+            else if (array[index] is JsonArray nestedArray)
+            {
+                WalkArray(nestedArray, itemPath, findings);
+            }
             else if (array[index] is JsonValue arrayValue &&
                      arrayValue.TryGetValue(out string? arrayText) &&
                      arrayText is not null)
             {
                 string redacted = ApplyDetectorsToValue(arrayText, itemPath, findings);
                 if (redacted != arrayText)
                 {
                     array[index] = redacted;
                 }
             }
         }
     }

Given the path-instruction that queue/redaction changes require crash/recovery or adversarial tests, please add an adversarial test with nested arrays to guard against regressions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void WalkArray(JsonArray array, string path, List<AppliedFinding> findings)
{
for (int index = 0; index < array.Count; index++)
{
string itemPath = $"{path}[{index}]";
if (array[index] is JsonObject nested)
{
WalkObject(nested, itemPath, findings);
}
else if (array[index] is JsonValue arrayValue &&
arrayValue.TryGetValue(out string? arrayText) &&
arrayText is not null)
{
string redacted = ApplyDetectorsToValue(arrayText, itemPath, findings);
if (redacted != arrayText)
{
array[index] = redacted;
}
}
}
}
private void WalkArray(JsonArray array, string path, List<AppliedFinding> findings)
{
for (int index = 0; index < array.Count; index++)
{
string itemPath = $"{path}[{index}]";
if (array[index] is JsonObject nested)
{
WalkObject(nested, itemPath, findings);
}
else if (array[index] is JsonArray nestedArray)
{
WalkArray(nestedArray, itemPath, findings);
}
else if (array[index] is JsonValue arrayValue &&
arrayValue.TryGetValue(out string? arrayText) &&
arrayText is not null)
{
string redacted = ApplyDetectorsToValue(arrayText, itemPath, findings);
if (redacted != arrayText)
{
array[index] = redacted;
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Privacy/PrivacyEngine.cs` around lines 171 - 191, Update
WalkArray to detect JsonArray elements and recursively call WalkArray with the
nested array, its itemPath, and the existing findings collection before
processing object or scalar values. Add an adversarial test covering sensitive
data inside nested arrays, including nested objects where applicable, and verify
all detectors redact the values before persistence.

Source: Path instructions

Comment on lines +14 to +35
public IReadOnlyDictionary<string, PrivacyAction> DetectorActions { get; init; } =
new Dictionary<string, PrivacyAction>(StringComparer.Ordinal);

/// <summary>Disabled detector ids (case-sensitive stable ids).</summary>
public IReadOnlySet<string> DisabledDetectors { get; init; } =
new HashSet<string>(StringComparer.Ordinal);

/// <summary>Optional detectors that are off unless explicitly enabled.</summary>
public IReadOnlySet<string> OptInDetectors { get; init; } =
new HashSet<string>(StringComparer.Ordinal)
{
DetectorIds.IpAddress,
DetectorIds.Hostname,
DetectorIds.Username,
DetectorIds.Address,
DetectorIds.Gps,
DetectorIds.SecurityMarking
};

/// <summary>Explicitly enable opt-in detectors.</summary>
public IReadOnlySet<string> EnabledOptInDetectors { get; init; } =
new HashSet<string>(StringComparer.Ordinal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Defensively copy policy collections.

These IReadOnly* properties retain caller-owned mutable dictionaries/sets. Mutating a source collection after engine construction can enable a detector or change a secret action to Allow during later scans. Copy inputs into policy-owned immutable/read-only backing collections, and add an adversarial mutation test.

As per coding guidelines, “Keep evaluation deterministic” and “Redact sensitive data before persistence; treat any redaction failure as a security failure.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Privacy/PrivacyPolicy.cs` around lines 14 - 35, Update
the PrivacyPolicy collection properties, including DetectorActions,
DisabledDetectors, OptInDetectors, and EnabledOptInDetectors, to defensively
copy incoming dictionaries/sets into policy-owned immutable or read-only
collections using ordinal comparison. Ensure later mutations to caller-owned
collections cannot affect detector evaluation or actions, and add an adversarial
test that mutates each source collection after policy/engine construction and
verifies scan behavior remains unchanged.

Source: Coding guidelines

DetectorActions = new Dictionary<string, PrivacyAction>(StringComparer.Ordinal)
{
[DetectorIds.FieldNameSecret] = PrivacyAction.Remove,
[DetectorIds.Tfn] = PrivacyAction.Sha256,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not SHA-256 TFNs by default.

A TFN has a small, structured input space, so an unkeyed sha256: value persisted in telemetry is practical to enumerate offline. Default to Mask or Remove; use HMAC only when a mandatory managed key is available. Add a test that CreateDefault() never emits a TFN SHA-256 digest.

As per coding guidelines, “Redact sensitive data before persistence; treat any redaction failure as a security failure.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bower.Redaction/Privacy/PrivacyPolicy.cs` at line 80, Update the default
policy mapping for DetectorIds.Tfn to use PrivacyAction.Mask or Remove instead
of PrivacyAction.Sha256, while preserving HMAC only for configurations with a
mandatory managed key. Add coverage verifying CreateDefault() never produces a
TFN SHA-256 digest, and ensure redaction failures are treated as security
failures.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant