-
Notifications
You must be signed in to change notification settings - Fork 4
Harden Universal Guard PII scrubber config normalization #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -111,7 +111,7 @@ export class PiiScrubber { | |||||||||||||||
| * @param {boolean} cfg.logDetections - Whether to log PII detections | ||||||||||||||||
| */ | ||||||||||||||||
| constructor(cfg) { | ||||||||||||||||
| this._cfg = cfg; | ||||||||||||||||
| this._cfg = this._normalizeConfig(cfg); | ||||||||||||||||
| this._detections = 0; | ||||||||||||||||
| this._byType = {}; | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -184,6 +184,19 @@ export class PiiScrubber { | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| updateConfig(cfg) { | ||||||||||||||||
| this._cfg = cfg; | ||||||||||||||||
| this._cfg = this._normalizeConfig(cfg); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| _normalizeConfig(cfg) { | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: High The new Code Suggestion: _normalizeConfig(cfg) {
const next = (cfg && typeof cfg === 'object') ? { ...cfg } : {};
// Normalize only the known properties, preserving any others.
return {
...next,
enabled: Boolean(next.enabled),
action: next.action === 'flag' ? 'flag' : 'redact',
patterns: Array.isArray(next.patterns)
? next.patterns.filter(p => typeof p === 'string' && p.trim().length > 0)
: [],
logDetections: Boolean(next.logDetections),
};
} |
||||||||||||||||
| const next = (cfg && typeof cfg === 'object') ? { ...cfg } : {}; | ||||||||||||||||
| return { | ||||||||||||||||
| ...next, | ||||||||||||||||
| enabled: Boolean(next.enabled), | ||||||||||||||||
| action: next.action === 'flag' ? 'flag' : 'redact', | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: High The logic for the Code Suggestion: const normalizedAction = String(next.action).toLowerCase();
action: normalizedAction === 'flag' ? 'flag' : 'redact',
// Optional: Log if original value was not 'redact' or 'flag'
if (normalizedAction !== 'redact' && normalizedAction !== 'flag') {
console.warn(`PiiScrubber: Unrecognized action "${next.action}". Defaulting to "redact".`);
} |
||||||||||||||||
| patterns: Array.isArray(next.patterns) | ||||||||||||||||
| ? next.patterns.filter(p => typeof p === 'string' && p.trim().length > 0) | ||||||||||||||||
|
||||||||||||||||
| ? next.patterns.filter(p => typeof p === 'string' && p.trim().length > 0) | |
| ? [...new Set( | |
| next.patterns | |
| .filter(p => typeof p === 'string') | |
| .map(p => p.trim()) | |
| .filter(p => p.length > 0) | |
| )] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 | Confidence: High
The normalization logic for patterns silently degrades invalid input (non-array, or array containing non-strings/empty strings) to an empty array []. This aligns with the PR's goal of making the scrubber non-fatal. However, this creates a silent failure mode where a configuration error (e.g., a typo like paterns) or a UI/API bug that sends a malformed payload results in the PII scrubber being completely disabled without any warning or log. For a security-critical component, silently turning off protection is risky. The scrubber should at least log a warning when patterns is normalized to an empty array, especially if the scrubber is enabled. The logDetections flag is for logging actual PII findings, not configuration errors.
Code Suggestion:
patterns: Array.isArray(next.patterns)
? next.patterns.filter(p => typeof p === 'string' && p.trim().length > 0)
: (() => {
if (next.enabled && next.patterns !== undefined) {
console.warn(`PiiScrubber: Invalid 'patterns' config (${typeof next.patterns}). Defaulting to empty list.`);
}
return [];
})(),| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||
| import { describe, expect, test } from '@jest/globals'; | ||||||
| import { PiiScrubber } from '../src/plugins/universal-guard/pii-scrubber.js'; | ||||||
|
|
||||||
| describe('PiiScrubber', () => { | ||||||
| test('handles null patterns config without throwing', () => { | ||||||
| const scrubber = new PiiScrubber({ | ||||||
| enabled: true, | ||||||
| action: 'redact', | ||||||
| patterns: null, | ||||||
| logDetections: false, | ||||||
| }); | ||||||
|
|
||||||
| expect(() => scrubber.scrub([{ role: 'user', content: 'email me at x@y.com' }])).not.toThrow(); | ||||||
| expect(scrubber.scrub([{ role: 'user', content: 'email me at x@y.com' }]).detections).toEqual([]); | ||||||
| }); | ||||||
|
|
||||||
| test('continues detecting after updateConfig receives invalid patterns', () => { | ||||||
|
||||||
| test('continues detecting after updateConfig receives invalid patterns', () => { | |
| test('degrades safely to no detection after updateConfig receives invalid patterns', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 | Confidence: High
The new unit tests are valuable for locking in the non-throwing behavior. However, they do not test the updateConfig method in isolation. The test 'continues detecting after updateConfig receives invalid patterns' validates the outcome of a subsequent scrub call but does not assert that the updateConfig call itself succeeds without throwing. While the implementation makes this likely, a direct test that scrubber.updateConfig(invalidPayload) does not throw would be more precise and would catch regressions where _normalizeConfig might be removed or altered. Furthermore, the test suite lacks coverage for updateConfig with a null or undefined argument, which the _normalizeConfig method is designed to handle (it defaults to an empty object).
Code Suggestion:
test('updateConfig does not throw on invalid input', () => {
const scrubber = new PiiScrubber({ enabled: true, patterns: [] });
expect(() => scrubber.updateConfig({ patterns: 'not-an-array' })).not.toThrow();
expect(() => scrubber.updateConfig(null)).not.toThrow();
expect(() => scrubber.updateConfig(undefined)).not.toThrow();
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 | Confidence: High
The constructor now delegates to
_normalizeConfig. This is a positive change that centralizes validation logic. However, there is a potential state inconsistency risk on re-initialization. The_detectionsand_byTypecounters are reset to zero in the constructor, but they are not reset whenupdateConfigis called. This is correct behavior, as the detection statistics are about the scrubber's runtime activity, not its configuration. The PR does not change this, but it's worth noting that if the scrubber were ever re-instantiated with a new config (instead of usingupdateConfig), the stats would reset. This is fine, but it highlights thatupdateConfigis for dynamic reconfiguration, while creating a new instance is for a fresh state. The architecture is sound.