Skip to content

feat(privacy-filter): capture-group replacement in redact - #665

Open
TimeToBuildBob wants to merge 7 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/privacy-redact-captures
Open

feat(privacy-filter): capture-group replacement in redact#665
TimeToBuildBob wants to merge 7 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/privacy-redact-captures

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #659: redact currently replaces the whole field with a static string. Awatcher filters let you extract capturing groups and put them in the replacement ($1, $name). This adds the same, opt-in via the replacement template.

Capture substitution runs only when the pattern has capturing groups and replacement contains a real capture template ($1, $name, ${name}) whose every $ ref names a group that exists and participates in every match. $0 / ${0} (whole-match identity), dangling refs, empty ${}, unmatched alternation/optional groups, and mixed malformed templates ($1$, $1$&) stay whole-field. Static replacements keep whole-field redaction even if the pattern has groups — so existing stored rules like (token) + REDACTED do not switch to replace_all and leak unmatched text.

Example (same as the awatcher README):

  • pattern: ● (.*)
  • replacement: $1
  • "● file.rs - Visual Studio Code""file.rs - Visual Studio Code"

Also works for URL path stripping: https://([^/]+)/.* + https://$1/ keeps the host.

Test plan

  • cargo test -p aw-datastore --lib privacy_filter (30 passed)
  • Save a redact rule with a capturing group and $1 / $name replacement in the webui
  • Confirm a no-capture / static-replacement rule still replaces the whole field

Redact previously replaced the whole field with a static string.
When the pattern has capturing groups, treat replacement as a regex
template ($1, $name) like awatcher filters. No capturing groups keeps
the existing whole-field behavior.

Related to ActivityWatch#659.
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds opt-in capture-group substitution for privacy-filter redaction while retaining whole-field replacement for static or invalid templates.

  • Parses numeric and named capture references in replacement templates.
  • Requires referenced captures to participate in every match before using partial replacement.
  • Adds regression coverage for static replacements, invalid references, group zero, and unmatched captures.

Confidence Score: 3/5

The PR is not yet safe to merge because malformed mixed capture templates can leave sensitive event-field content unredacted.

The replacement parser can accept an earlier valid capture after encountering a dangling or unsupported dollar form, causing replace_all to preserve unmatched sensitive content at the datastore privacy boundary.

Files Needing Attention: aw-datastore/src/privacy_filter.rs

Security Review

A mixed template containing a valid capture plus a dangling or unsupported dollar reference can still enable partial replacement, leaving unmatched sensitive field content in events persisted through the privacy boundary.

Important Files Changed

Filename Overview
aw-datastore/src/privacy_filter.rs Adds capture-aware redaction and extensive tests, but mixed malformed templates can bypass the intended fail-closed behavior.

Reviews (4): Last reviewed commit: "fix(privacy-filter): reject $0 and unmat..." | Re-trigger Greptile

Comment thread aw-datastore/src/privacy_filter.rs Outdated
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.18310% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.81%. Comparing base (656f3c9) to head (0de5164).
⚠️ Report is 93 commits behind head on master.

Files with missing lines Patch % Lines
aw-datastore/src/privacy_filter.rs 97.18% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #665      +/-   ##
==========================================
+ Coverage   70.81%   78.81%   +8.00%     
==========================================
  Files          51       66      +15     
  Lines        2916     5524    +2608     
==========================================
+ Hits         2065     4354    +2289     
- Misses        851     1170     +319     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…capture template

Capture-group replacement was keyed only on captures_len > 1, so an existing
stored rule like `(token)` + `REDACTED` switched from whole-field redaction
to replace_all and leaked unmatched text (`token=abc token=def` became
`REDACTED=abc REDACTED=def`).

Opt in only when the replacement contains a capture template (`$1`, `$name`).
Static replacements keep whole-field behavior.

Addresses Greptile P1 on ActivityWatch#665.
replacement_is_capture_template now requires every $ reference to name a
group that exists on the compiled regex. A replacement like `REDACTED $5`
on a 1-group pattern would otherwise take replace_all, expand $5 to "",
and leak unmatched field text.

Addresses in-band P1 on ActivityWatch#665.
The regex crate only interpolates $N / $name / ${name}. $& /$` /$' are
Perl-only; treating them as valid refs would take replace_all and leak
unmatched field text. Fall through to whole-field redaction instead.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-datastore/src/privacy_filter.rs
`${}` is a named ref with an empty name, not `$0`. The regex crate
expands it to "" and would leak unmatched field text via replace_all.

Addresses Greptile P1 on ActivityWatch#665.

Git-Session-Id: 589e6fad-191b-5e97-93c6-70c22c1f5d4c
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-datastore/src/privacy_filter.rs Outdated
Comment thread aw-datastore/src/privacy_filter.rs Outdated
$0 / ${0} is the whole match; replace_all would persist the match and
unmatched field text. Alternation groups that exist on the regex but
do not participate in a match expand to empty and leak leftover text.

Stay whole-field unless every referenced group is present in every match.

Addresses Greptile P1 on ActivityWatch#665.

Git-Session-Id: d3267f9a-1817-52b6-92cd-86e006957db2
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-datastore/src/privacy_filter.rs
A valid $1 plus a dangling or unsupported dollar form ($1$, $1$&) used
to skip the suffix and still enable replace_all, leaking unmatched
field text. Any unparsed $ now keeps the rule whole-field. $1$$ (group
plus literal dollar) still substitutes.

Git-Session-Id: 1c0d8fb8-0b35-5596-9027-cd416fbe048a
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Greptile convergence adjudication

Hit the re-review cap. Not triggering another Greptile review.

Fixed this session

  • P1 Malformed templates enable partial redaction ($1$, $1$&): the parser skipped a dangling/unsupported $ after accepting $1, which still enabled replace_all and leaked unmatched field text (token=abctoken=). 0de5164 fail-closes any unparsed $. $1$$ (group + literal dollar) still substitutes.
  • Local verification: cargo test -p aw-datastore --lib privacy_filter — 30 passed.

Earlier rounds (already on the branch)

  • Capture substitution is opt-in via $1/$name, not “pattern has groups”
  • Empty ${}, $0/${0}, dangling $N, unmatched alternation groups, and Perl $&/$``/$'` all stay whole-field

Remaining

  • None blocking that I can reproduce. The 3/5 score is stale (last review was 621008b). Not chasing a 5/5 — four rounds each produced a new P1 at the same fail-closed boundary.

CI

  • 621008b: all green
  • 0de5164: format green; build/clippy/coverage still in flight (not watching)

Domain risk

Privacy-filter is a datastore boundary. Maintainer should sanity-check: (1) static REDACTED on a capturing pattern stays whole-field, (2) $1=REDACTED still replace_all, (3) malformed $1$ / $1$& stay whole-field. Existing stored rules without $N in the replacement are unchanged.

Convergence

round_convergence.status=new_blocking, stable_rounds=0 (required 2). Four rounds, four distinct P1 keys, each a new blocking finding. Cap hit. Stopping re-triggers.

Merge-ready does not mean auto-merge — maintainer judgment.

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