Filter Strategies
A filter strategy controls what happens to a detected PII token. Each identifier type supports a Strategies list; the first strategy whose condition evaluates to true is applied. If the list is empty, the default REDACT strategy is used.
Every strategy is available on every identifier type, including the dictionary-backed ones
(surname, firstName, city, county, state, hospital) and the custom dictionaries
list, and behaves the same on each.
Strategy names are matched without regard to case, so MASK, mask and Mask all select the same strategy. The names below are the canonical form and what the redaction policy schema declares; write them uppercase in a new policy.
A date filter raises on a name that is not one of the strategies below, rather than falling back to
REDACT, so a policy asking for a year or an interval cannot silently get a destroyed date instead.
Every other filter type still falls back to REDACT.
Available Strategies
| Strategy | Constant | Description |
|---|---|---|
REDACT |
AbstractFilterStrategy.Redact |
Replace the token with a formatted redaction label |
RANDOM_REPLACE |
AbstractFilterStrategy.RandomReplace |
Replace with a realistic, type-appropriate fake value |
STATIC_REPLACE |
AbstractFilterStrategy.StaticReplace |
Replace with a fixed string |
CRYPTO_REPLACE |
AbstractFilterStrategy.CryptoReplace |
Replace with AES-GCM encrypted ciphertext |
FPE_ENCRYPT_REPLACE |
AbstractFilterStrategy.FpeEncryptReplace |
Format-preserving encryption (FF3-1) |
HASH_SHA256_REPLACE |
AbstractFilterStrategy.HashSha256Replace |
Replace with the SHA-256 hex digest |
LAST_4 |
AbstractFilterStrategy.Last4 |
Keep only the last 4 characters |
MASK |
AbstractFilterStrategy.Mask |
Overwrite characters with a mask character |
ABBREVIATE |
AbstractFilterStrategy.Abbreviate |
Reduce the token to the initials of its words |
MAP_REPLACE |
AbstractFilterStrategy.MapReplace |
Replace from a lookup table, then a generator, then a fallback strategy |
SAME |
AbstractFilterStrategy.Same |
Leave the token unchanged (mark as detected but not replaced) |
TRUNCATE |
AbstractFilterStrategy.Truncate |
Keep only the first character |
SHIFT |
AbstractFilterStrategy.Shift |
Shift a detected date by a configurable offset (date filters only). SHIFT_DATE is accepted as an alias |
TRUNCATE_TO_YEAR |
AbstractFilterStrategy.TruncateToYear |
Replace a detected date with its year (date filters only) |
RELATIVE |
AbstractFilterStrategy.Relative |
Replace a detected date with a readable interval from today (date filters only) |
Strategy Details
REDACT
Replaces the token with a formatted label. The redactionFormat string may contain:
%t— replaced with the filter type name (e.g.ssn,email-address)%l— replaced with the token's classification label (if any)
Default format: {{{REDACTED-%t}}}
new SsnFilterStrategy
{
Strategy = "REDACT",
RedactionFormat = "[REMOVED-%t]"
}
{ "strategy": "REDACT", "redactionFormat": "[REMOVED-%t]" }
RANDOM_REPLACE
Replaces the token with a realistic, type-appropriate fake value generated by the anonymization service for the filter type (for example, a fake SSN-shaped value for an SSN, a fake date for a date). When no anonymization service is wired in (for strategies constructed directly, outside FilterService), it falls back to a random GUID.
new SsnFilterStrategy
{
Strategy = "RANDOM_REPLACE"
}
Replacement scope — replacementScope controls whether a token's replacement is reused:
replacementScope |
Behaviour |
|---|---|
"DOCUMENT" (default) |
Each occurrence is anonymized independently. |
"CONTEXT" |
A token's replacement is reused across the context, so the same input value always maps to the same fake value (referential integrity). |
new SsnFilterStrategy
{
Strategy = "RANDOM_REPLACE",
ReplacementScope = "CONTEXT" // reuse the same fake value for the same SSN
}
Choosing the fake values — by default RANDOM_REPLACE generates realistic values. You can override this:
| Property | JSON key | Description |
|---|---|---|
anonymizationCandidates |
anonymizationCandidates |
When non-empty, replacements are drawn from this explicit list of values instead of being generated. |
anonymizationMethod |
anonymizationMethod |
The generation method when no candidates are supplied (defaults to realistic generation). |
new FirstNameFilterStrategy
{
Strategy = "RANDOM_REPLACE",
AnonymizationCandidates = new List<string> { "Alex", "Jordan", "Riley" }
}
See Context Service for details on referential integrity with CONTEXT scope.
STATIC_REPLACE
Replaces the token with a fixed string supplied in staticReplacement. Falls back to REDACT format if staticReplacement is empty.
new EmailAddressFilterStrategy
{
Strategy = "STATIC_REPLACE",
StaticReplacement = "user@redacted.invalid"
}
{ "strategy": "STATIC_REPLACE", "staticReplacement": "user@redacted.invalid" }
CRYPTO_REPLACE
Encrypts the token with AES-GCM and replaces it with the ciphertext wrapped in double braces, e.g. {{<base64>}} (the Base64 payload is nonce || ciphertext || tag). Requires a Crypto block on the Policy whose key is a hex-encoded 16, 24, or 32-byte AES key. Falls back to REDACT if the policy has no Crypto configuration.
var policy = new Policy
{
Name = "encrypted",
Crypto = new Crypto
{
// Hex-encoded AES key (32 hex chars = 16 bytes, 48 = 24, 64 = 32).
Key = Convert.ToHexString(aesKey)
},
Identifiers = new Identifiers
{
Ssn = new Ssn
{
Strategies = new List<SsnFilterStrategy>
{
new SsnFilterStrategy { Strategy = "CRYPTO_REPLACE" }
}
}
}
};
The key may also be supplied as an
env:NAMEreference, which is resolved from the environment variableNAMEat filter time.
FPE_ENCRYPT_REPLACE
Format-preserving encryption using the FF3-1 cipher. Requires an Fpe block on the Policy with a hex-encoded key and a tweak. The tweak is required (FF3-1 needs a 56- or 64-bit tweak); the strategy falls back to REDACT when the Fpe block, key, or tweak is missing.
var policy = new Policy
{
Name = "fpe-policy",
Fpe = new Fpe
{
Key = "EF4359D8D580AA4F7F036D6F04FC6A94", // hex-encoded key
Tweak = "D8E7920AFA330A73" // hex-encoded tweak (required)
},
Identifiers = new Identifiers
{
CreditCard = new CreditCard
{
Strategies = new List<CreditCardFilterStrategy>
{
new CreditCardFilterStrategy { Strategy = "FPE_ENCRYPT_REPLACE" }
}
}
}
};
Like the AES key, the FPE key and tweak may be supplied as
env:NAMEreferences.
HASH_SHA256_REPLACE
Replaces the token with its lower-case SHA-256 hex digest. Optionally appends a random salt before hashing when salt: true is set.
new EmailAddressFilterStrategy
{
Strategy = "HASH_SHA256_REPLACE",
Salt = true // prepend random salt before hashing
}
{ "strategy": "HASH_SHA256_REPLACE", "salt": true }
LAST_4
Keeps the last four characters of the token and discards the rest. If the token is shorter than four characters, the full token is returned.
new CreditCardFilterStrategy { Strategy = "LAST_4" }
Output example: 1234 (from 4111-1111-1111-1234).
MASK
Replaces characters with a mask character (default *). Use maskLength to control how many characters are written:
maskLength value |
Behaviour |
|---|---|
"same" (default) |
Mask has the same length as the original token |
Integer string, e.g. "6" |
Mask has exactly that many characters (capped at token length) |
new SsnFilterStrategy
{
Strategy = "MASK",
MaskCharacter = "#",
MaskLength = "same"
}
{ "strategy": "MASK", "maskCharacter": "#", "maskLength": "6" }
SAME
Marks the token as detected but leaves the text unchanged. Useful when you want spans and metadata without altering the output.
new PhoneNumberFilterStrategy { Strategy = "SAME" }
ABBREVIATE
Replaces the token with the uppercase initial of each whitespace-separated word.
new FirstNameFilterStrategy { Strategy = "ABBREVIATE" }
Output example: JS (from John Smith).
MAP_REPLACE
Replaces a detected value using a lookup table, resolving each token in this order:
- Lookup table. If the token is a key in the table, its mapped value is used.
- Generator. If the token is not in the table and a
generatoris configured, the generator produces a replacement. The value is rejected (and the strategy falls through to the fallback) if the generator fails or times out, returns a blank value, returns the original token again (case-insensitively), or produces a value that itself contains detectable PII (each generated value is re-scanned through the filter pipeline to confirm the generator did not reintroduce sensitive data). - Fallback strategy.
fallbackStrategy(defaultREDACT) is applied. A detected value is never left in the clear.
The table is built from inline mappings and/or tab-separated mappingFiles (one key<TAB>value pair per row), merged once when the filter is built. Inline mappings override entries loaded from files; among files, a later file overrides an earlier one for a duplicate key. caseSensitive (default false) controls whether keys and tokens are matched case-insensitively.
new SurnameFilterStrategy
{
Strategy = "MAP_REPLACE",
Mappings = new Dictionary<string, string> { ["Smith"] = "Jones" },
MappingFiles = new List<string> { "/etc/phileas/surnames.tsv" },
CaseSensitive = false,
Generator = "local", // name of a generator in the policy's generators block
FallbackStrategy = "REDACT"
}
Generated values are routed through the same context-scoped cache as RANDOM_REPLACE: when replacementScope is CONTEXT, a repeated token in the same context reuses its first replacement and the generator is not called again. With replacementScope DOCUMENT (the default), each occurrence is generated independently.
Generators
A generator is declared once in the policy's top-level generators block and referenced by name from a MAP_REPLACE strategy's generator property. Generators target a local model endpoint inside your deployment boundary so detected values are not sent to a third party. The ollama type calls a local Ollama-compatible /api/generate endpoint.
{
"generators": {
"local": {
"type": "ollama",
"endpoint": "http://localhost:11434",
"model": "llama3.1",
"prompt": "Rewrite {{token}} as a different but structurally similar value. Return only the value.",
"timeoutMs": 2000
}
}
}
The prompt template supports the {{token}} placeholder (the detected value) and {{label}} (its entity label). timeoutMs is required so a generator can never block the pipeline: on timeout the strategy applies its fallbackStrategy. A generator name that does not resolve to a defined generator is ignored, and the strategy uses its fallback.
TRUNCATE
Keeps only the first character of the token.
new EmailAddressFilterStrategy { Strategy = "TRUNCATE" }
SHIFT
Date filters only. Applies to
DateFilterStrategy; ignored by all other filter types.
Shifts a detected date forward or backward by a configurable number of days, months, and/or years while preserving the original date format. All three offsets default to 0 and can be combined freely. Negative values shift the date into the past.
The strategy is named SHIFT in the redaction policy schema and by the PhiSQL compiler. SHIFT_DATE, the name this port used previously, is still accepted so existing policies keep working.
Supported date formats
| Example | Format |
|---|---|
1/15/1990 |
Numeric month first |
15/01/1990 |
Numeric day first, also with - and . |
1990-01-15 |
Numeric year first (ISO 8601), also with / and . |
January 15, 1990 |
Full month name |
Jan 15, 1990 |
Abbreviated month name |
15 January 1990 |
Day first, full month name |
15-Jan-1990 |
Day first, abbreviated month name, also with /, . and a space |
Every form the date filter detects can be shifted; see Supported Identifiers for the full detection table. A shifted date is written back in the form it was read in, so the ordering, the separator and a two-digit year are all preserved.
If a detected token cannot be parsed as a date, which a numeric date that is not a real calendar date
will not be, SHIFT falls back to REDACT rather than returning the token. A detected date is never
left in the document.
Properties
| Property | JSON key | Type | Default | Description |
|---|---|---|---|---|
ShiftDays |
shiftDays |
int |
0 |
Days to add (negative to subtract) |
ShiftMonths |
shiftMonths |
int |
0 |
Months to add (negative to subtract) |
ShiftYears |
shiftYears |
int |
0 |
Years to add (negative to subtract) |
ShiftRandom |
shiftRandom |
bool |
false |
Shift by a random amount instead of the configured offsets: one to twenty-nine days forward, one to eleven months forward, and one or two years back |
FutureDates |
futureDates |
bool |
false |
Whether a shifted date may land in the future. When false, a shift that would move a past date beyond today is applied in the opposite direction instead |
C# example
using Phileas.Policy.Filters;
using Phileas.Policy.Filters.Strategies;
var policy = new Policy
{
Name = "date-shift-policy",
Identifiers = new Identifiers
{
Date = new Date
{
Strategies = new List<DateFilterStrategy>
{
new DateFilterStrategy
{
Strategy = "SHIFT",
ShiftYears = -1,
ShiftDays = 14
}
}
}
}
};
JSON policy example
{
"name": "date-shift-policy",
"identifiers": {
"date": {
"dateFilterStrategies": [{
"strategy": "SHIFT",
"shiftYears": -1,
"shiftDays": 14
}]
}
}
}
TRUNCATE_TO_YEAR
Date filters only. Applies to
DateFilterStrategy; ignored by all other filter types.
Replaces a detected date with its year alone, so 01/15/1990 becomes 1990. A detected token that
cannot be parsed as a date falls back to REDACT.
{
"identifiers": {
"date": {
"dateFilterStrategies": [{ "strategy": "TRUNCATE_TO_YEAR" }]
}
}
}
RELATIVE
Date filters only. Applies to
DateFilterStrategy; ignored by all other filter types.
Replaces a detected date with a readable interval from today, such as 3 months ago. Once a year has
passed the years are included: 2 years 1 months ago. The wording, including the plural on a single
month, matches the Java filter so the two ports produce the same output. An interval rounds up to the
next month from the fifteenth day onward.
A date ahead of today is governed by futureDates, which is off by default:
futureDates |
A date four months ahead |
|---|---|
false (default) |
redacted |
true |
in 4 months |
A detected token that cannot be parsed as a date falls back to REDACT.
Properties
| Property | JSON key | Type | Default | Description |
|---|---|---|---|---|
FutureDates |
futureDates |
bool |
false |
Whether a date ahead of today is phrased as an interval rather than redacted |
{
"identifiers": {
"date": {
"dateFilterStrategies": [{ "strategy": "RELATIVE", "futureDates": true }]
}
}
}
futureDatesis read by bothRELATIVEandSHIFT, and means the same thing in each: whether a replacement may land ahead of today. UnderRELATIVEit decides between an interval and redaction; underSHIFTit decides whether a shift that would move a past date beyond today is applied in the opposite direction instead.
Example transformation, with today taken as 15 September 2026:
| Input | Output |
|---|---|
DOB: January 15, 1990 |
DOB: 36 years 8 months ago |
Admitted: 3/1/2024 |
Admitted: 2 years 6 months ago |
Salting
Any strategy can optionally append a random 16-byte Base64 salt to the token before processing by setting salt: true. The generated salt is included in the Span.Salt field of the result so it can be recorded for auditing or reproduction.
new SsnFilterStrategy
{
Strategy = "HASH_SHA256_REPLACE",
Salt = true
}
Redaction Bar Color
Any strategy can set an optional color that controls the color of the bar drawn over the spans it redacts when the output is a PDF or image. It overrides the policy-wide config.pdf.redactionColor for those spans; when unset, the policy-wide color (default black) applies. color has no effect on text redaction.
new SsnFilterStrategy
{
Strategy = "REDACT",
Color = "red"
}
Accepted values are a named color (black, white, red, orange, yellow, green, blue, gray) or a 6-digit hex string matching ^#[0-9A-Fa-f]{6}$ (for example #ff8800). An unrecognized or malformed value renders as black, so a detected span is never left un-redacted. Because color overrides the policy-wide color, a malformed strategy color renders black rather than falling back to config.pdf.redactionColor.
Combined with strategy conditions, this colors spans by detection confidence or any other condition field:
"creditCardFilterStrategies": [
{ "strategy": "REDACT", "color": "green", "condition": "confidence >= 0.9" },
{ "strategy": "REDACT", "color": "orange", "condition": "confidence < 0.9" }
]
Strategy Conditions
Strategies can include a condition property that controls when they are applied. When multiple strategies are defined, phileas-dotnet evaluates their conditions in order and applies the first strategy whose condition evaluates to true.
new EmailAddressFilterStrategy
{
Strategy = "MASK",
Condition = "confidence > 0.8 and context == \"internal\""
}
Supported condition fields:
- confidence - Detection confidence (0.0 to 1.0)
- context - Context name passed to FilterService.Filter()
- token - The detected text value
- type - Classification type (e.g., "PER", "LOC")
- population - Census population of a detected ZIP code
Supported operators:
- Comparison: ==, !=, >, <, >=, <=, is, is not
- String: startswith
- Logical: and
See Filter Conditions for detailed examples and usage patterns.
Configuring Strategies Per Identifier
Each identifier type has a corresponding strategy class (e.g. SsnFilterStrategy, EmailAddressFilterStrategy). Set the Strategies list on the identifier:
var policy = new Policy
{
Name = "multi-strategy",
Identifiers = new Identifiers
{
Ssn = new Ssn
{
Strategies = new List<SsnFilterStrategy>
{
new SsnFilterStrategy { Strategy = "MASK" }
}
},
EmailAddress = new EmailAddress
{
Strategies = new List<EmailAddressFilterStrategy>
{
new EmailAddressFilterStrategy { Strategy = "HASH_SHA256_REPLACE" }
}
},
PhoneNumber = new PhoneNumber
{
Strategies = new List<PhoneNumberFilterStrategy>
{
new PhoneNumberFilterStrategy { Strategy = "LAST_4" }
}
}
}
};