@@ -27,6 +27,10 @@ const domainPartArb: fc.Arbitrary<string> = fc.stringMatching(
2727const digitArb : fc . Arbitrary < string > = fc . integer ( { min : 0 , max : 9 } ) . map (
2828 String ,
2929) ;
30+ const luhnValidCardDigitsArb : fc . Arbitrary < readonly string [ ] > = fc . array (
31+ digitArb ,
32+ { minLength : 15 , maxLength : 15 } ,
33+ ) . map ( appendLuhnCheckDigit ) ;
3034
3135test ( "EMAIL_ADDRESS_PATTERN" , ( ) => {
3236 const { pattern, replacement } = EMAIL_ADDRESS_PATTERN ;
@@ -101,34 +105,87 @@ test("EMAIL_ADDRESS_PATTERN redacts generated email addresses", () => {
101105test ( "CREDIT_CARD_NUMBER_PATTERN" , ( ) => {
102106 const { pattern, replacement } = CREDIT_CARD_NUMBER_PATTERN ;
103107
104- // Test valid credit card numbers with dashes
105- assert . match ( "1234-5678-9012-3456" , pattern ) ; // Regular 16-digit card
106- pattern . lastIndex = 0 ;
107- assert . match ( "1234-5678-901234" , pattern ) ; // American Express format
108- pattern . lastIndex = 0 ;
108+ const redact = ( value : string ) : string =>
109+ typeof replacement === "string"
110+ ? value . replaceAll ( pattern , replacement )
111+ : value . replaceAll ( pattern , replacement ) ;
112+
113+ const validNumbers = [
114+ "4222222222222" ,
115+ "4222 2222 2222 2" ,
116+ "30569309025904" ,
117+ "3056 9309 025904" ,
118+ "3056-9309-025904" ,
119+ "3056-930902-5904" ,
120+ "1354 12345 678911" ,
121+ "1354-12345-678911" ,
122+ "378282246310005" ,
123+ "3782 822463 10005" ,
124+ "3782-822463-10005" ,
125+ "4111111111111111" ,
126+ "4111 1111 1111 1111" ,
127+ "4111 1111 1111 1111" ,
128+ "4111-1111-1111-1111" ,
129+ "5500005555555559" ,
130+ "4000000000000000006" ,
131+ "4000 0000 0000 0000 006" ,
132+ "4000-0000-0000-0000-006" ,
133+ ] ;
134+
135+ for ( const number of validNumbers ) {
136+ assert . strictEqual (
137+ redact ( `Card: ${ number } ` ) ,
138+ "Card: XXXX-XXXX-XXXX-XXXX" ,
139+ ) ;
140+ }
109141
110- // Test replacements
111142 assert . strictEqual (
112- "Card: 1234-5678-9012-3456" . replaceAll ( pattern , replacement as string ) ,
113- "Card: XXXX-XXXX-XXXX-XXXX" ,
143+ redact ( "Cards: 4111111111111111 and 3782-822463-10005" ) ,
144+ "Cards: XXXX-XXXX-XXXX-XXXX and XXXX-XXXX-XXXX-XXXX" ,
114145 ) ;
115146 assert . strictEqual (
116- "AmEx: 1234-5678-901234" . replaceAll ( pattern , replacement as string ) ,
117- "AmEx : XXXX-XXXX-XXXX-XXXX" ,
147+ redact ( "Payment: 4111-1111-1111-1111-12-28" ) ,
148+ "Payment : XXXX-XXXX-XXXX-XXXX-12-28 " ,
118149 ) ;
119150 assert . strictEqual (
120- "Cards: 1234-5678-9012-3456 and 1234-5678-901234" . replaceAll (
121- pattern ,
122- replacement as string ,
123- ) ,
124- "Cards: XXXX-XXXX-XXXX-XXXX and XXXX-XXXX-XXXX-XXXX" ,
151+ redact ( "Payment: 4111 1111 1111 1111 12/28" ) ,
152+ "Payment: XXXX-XXXX-XXXX-XXXX 12/28" ,
153+ ) ;
154+ assert . strictEqual (
155+ redact ( "Order 0001 4111-1111-1111-1111" ) ,
156+ "Order 0001 XXXX-XXXX-XXXX-XXXX" ,
125157 ) ;
158+ assert . strictEqual (
159+ redact ( "card_4111-1111-1111-1111_token" ) ,
160+ "card_XXXX-XXXX-XXXX-XXXX_token" ,
161+ ) ;
162+ assert . strictEqual (
163+ redact ( "Ref 1234 5678 4111 1111 1111 1111" ) ,
164+ "Ref 1234 5678 XXXX-XXXX-XXXX-XXXX" ,
165+ ) ;
166+ assert . strictEqual (
167+ redact ( "Cards: 4111 1111 1111 1111 5500 0055 5555 5559" ) ,
168+ "Cards: XXXX-XXXX-XXXX-XXXX XXXX-XXXX-XXXX-XXXX" ,
169+ ) ;
170+
171+ const invalidNumbers = [
172+ "123456789012" ,
173+ "12345678901234" ,
174+ "1234-5678-901234" ,
175+ "4111111111111112" ,
176+ "4111 1111 1111 1112" ,
177+ "12345678901234567890" ,
178+ ] ;
179+
180+ for ( const number of invalidNumbers ) {
181+ assert . strictEqual ( redact ( `Number: ${ number } ` ) , `Number: ${ number } ` ) ;
182+ }
126183} ) ;
127184
128- test ( "CREDIT_CARD_NUMBER_PATTERN redacts generated dashed card numbers " , ( ) => {
185+ test ( "CREDIT_CARD_NUMBER_PATTERN redacts generated Luhn-valid cards " , ( ) => {
129186 fc . assert (
130187 fc . property (
131- fc . array ( digitArb , { minLength : 16 , maxLength : 16 } ) ,
188+ luhnValidCardDigitsArb ,
132189 ( digits ) => {
133190 const card = `${ digits . slice ( 0 , 4 ) . join ( "" ) } -${
134191 digits . slice ( 4 , 8 ) . join ( "" )
@@ -279,10 +336,10 @@ test("redactByPattern(TextFormatter)", () => {
279336 level : "info" ,
280337 category : [ "test" ] ,
281338 message : [
282- "Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456 , ssn = 123-45-6789" ,
339+ "Sensitive info: email = user@example.com, cc = 4111-1111-1111-1111 , ssn = 123-45-6789" ,
283340 ] ,
284341 rawMessage :
285- "Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456 , ssn = 123-45-6789" ,
342+ "Sensitive info: email = user@example.com, cc = 4111-1111-1111-1111 , ssn = 123-45-6789" ,
286343 timestamp : Date . now ( ) ,
287344 properties : { } ,
288345 } ;
@@ -383,7 +440,7 @@ test("redactByPattern(ConsoleFormatter)", async () => {
383440 {
384441 name : "John Doe" ,
385442 email : "john@example.com" ,
386- creditCard : "1234-5678-9012-3456 " ,
443+ creditCard : "4111-1111-1111-1111 " ,
387444 } ,
388445 ] ,
389446 rawMessage : "User data: [object Object]" ,
@@ -431,8 +488,8 @@ test("redactByPattern(ConsoleFormatter)", async () => {
431488 } ,
432489 payment : {
433490 cards : [
434- "1234-5678-9012-3456 " ,
435- "8765-4321-8765-4321 " ,
491+ "4111-1111-1111-1111 " ,
492+ "5500-0055-5555-5559 " ,
436493 ] ,
437494 } ,
438495 documents : {
@@ -736,3 +793,16 @@ function record(): LogRecord {
736793 properties : { } ,
737794 } ;
738795}
796+
797+ function appendLuhnCheckDigit ( digits : readonly string [ ] ) : readonly string [ ] {
798+ let checksum = 0 ;
799+ for ( let i = 0 ; i < digits . length ; i ++ ) {
800+ let digit = Number ( digits [ i ] ) ;
801+ if ( i % 2 === 0 ) {
802+ digit *= 2 ;
803+ if ( digit > 9 ) digit -= 9 ;
804+ }
805+ checksum += digit ;
806+ }
807+ return [ ...digits , String ( ( 10 - checksum % 10 ) % 10 ) ] ;
808+ }
0 commit comments