File Coverage

blib/lib/Data/Censor.pm
Criterion Covered Total %
statement 56 62 90.3
branch 18 24 75.0
condition 12 18 66.6
subroutine 9 9 100.0
pod 2 3 66.6
total 97 116 83.6


line stmt bran cond sub pod time code
1             package Data::Censor;
2              
3 2     2   236678 use 5.006;
  2         10  
4 2     2   14 use strict;
  2         4  
  2         107  
5 2     2   13 use warnings FATAL => 'all';
  2         19  
  2         146  
6 2     2   13 use Carp;
  2         18  
  2         202  
7              
8 2     2   1330 use Ref::Util qw/ is_hashref /;
  2         5931  
  2         971  
9              
10             =head1 NAME
11              
12             Data::Censor - censor sensitive stuff in a data structure
13              
14             =head1 VERSION
15              
16             Version 0.04
17              
18             =cut
19              
20             our $VERSION = '0.04';
21              
22              
23             =head1 SYNOPSIS
24              
25             # OO way, letting you specify your own list of sensitive-looking fields, and
26             # what they should be replaced by (all options here are optional)
27             my $censor = Data::Censor->new(
28             # Specify which fields to censor:
29             sensitive_fields => [ qw(card_number password) ],
30              
31             # Specify text to replace their values with:
32             replacement => '(Sensitive data hidden)',
33              
34             # Or specify callbacks for each field name which return the "censored"
35             # value - in this case, masking a card number (PAN) to show only the
36             # last four digits:
37             replacement_callbacks => {
38             card_number => sub {
39             my $pan = shift;
40             return "x" x (length($pan) - 4) . substr($pan, -4, 4);
41             },
42             },
43             );
44            
45             # Censor the data in-place (changes the data structure, returns the number
46             # of keys censored)
47             my $censor_count = $censor->censor(\%data);
48              
49             # Alternate non-OO interface, using default settings and returning a cloned
50             # version of the data after censoring:
51             my $censored_data = Data::Censor->clone_and_censor(\%data);
52              
53              
54             =head1 new (CONSTRUCTOR)
55              
56             Accepts the following arguments:
57              
58             =over
59              
60             =item sensitive_fields
61              
62             Either an arrayref of sensitive fields, checked for equality, or a regex to test
63             against each key to see if it's considered sensitive.
64              
65             =item replacement
66              
67             The string to replace each value with. Any censoring callback provided in
68             C which matches this key will take precedence over this
69             straightforward value.
70              
71             =item replacement_callbacks
72              
73             A hashref of key => sub {...}, where each key is a column name to match, and the
74             coderef takes the uncensored value and returns the censored value, letting you
75             for instance mask a card number but leave the last 4 digits visible.
76              
77             If you provide both C and C, any callback
78             defined which matches the key being considered takes precedence.
79              
80             =back
81              
82             =cut
83              
84             sub new {
85 4     4 0 234718 my $class = shift;
86 4         14 my %args = @_;
87              
88 4         29 my $self = bless {} => $class;
89              
90 4 50       20 if ( ref $args{sensitive_fields} eq 'Regexp' ) {
    50          
91 0         0 $self->{censor_regex} = $args{sensitive_fields};
92             } elsif ( ref $args{sensitive_fields} eq 'ARRAY' ) {
93             $self->{is_sensitive_field} = {
94 0         0 map { $_ => 1 } @{ $args{sensitive_fields} }
  0         0  
  0         0  
95             };
96             } else {
97             $self->{is_sensitive_field} = {
98 4         9 map { $_ => 1 } qw(
  44         108  
99             pass password old_password secret
100             private_key cardnum card_number pan
101             cvv cvv2 ccv
102             )
103             };
104             }
105              
106 4 100       29 if ( is_hashref $args{replacement_callbacks} ) {
107 1         4 $self->{replacement_callbacks} = $args{replacement_callbacks};
108             }
109 4 100       12 if ( exists $args{replacement} ) {
110 1         3 $self->{replacement} = $args{replacement};
111             } else {
112 3         8 $self->{replacement} = 'Hidden (looks potentially sensitive)';
113             }
114              
115 4   50     30 $self->{recurse_limit} = $args{recurse_limit} || 100;
116              
117 4         25 return $self;
118             }
119              
120             =head1 METHODS
121              
122             =head2 censor
123              
124             Given a data structure (hashref), clones it and returns the cloned version after
125             censoring potentially sensitive data within.
126              
127             =cut
128              
129             sub censor {
130 12     12 1 8732 my ( $self, $data, $recurse_count, $visited ) = @_;
131 12   100     47 $recurse_count ||= 0;
132 12   100     42 $visited ||= {};
133              
134 2     2   17 no warnings 'recursion'; # we're checking ourselves.
  2         13  
  2         1158  
135              
136 12 50       30 if ( $recurse_count++ > $self->{recurse_limit} ) {
137 0         0 warn "Data exceeding $self->{recurse_limit} levels";
138 0         0 return;
139             }
140              
141 12 50       26 croak('censor expects a hashref') unless is_hashref $data;
142              
143 12         43 my $censored = 0;
144 12         37 for my $key ( keys %$data ) {
145              
146 36 100       80 if ( is_hashref $data->{$key} ) {
147             $censored
148             += $self->censor( $data->{$key}, $recurse_count, $visited )
149 7 100       42 unless $visited->{ $data->{$key} }++;
150 7         22 next;
151             }
152              
153             next unless
154             ( $self->{is_sensitive_field}
155             && $self->{is_sensitive_field}{ lc $key } )
156 29 50 66     173 or ( $self->{censor_regex} && $key =~ $self->{censor_regex} );
      33        
      66        
157              
158             # OK, censor this
159 14 100       34 if ( $self->{replacement_callbacks}{ lc $key } ) {
160             $data->{$key} = $self->{replacement_callbacks}{ lc $key }->(
161 1         14 $data->{$key}
162             );
163 1         12 $censored++;
164             } else {
165 13         30 $data->{$key} = $self->{replacement};
166 13         30 $censored++;
167             }
168             }
169              
170 12         41 return $censored;
171             }
172              
173             =head2 clone_and_censor
174              
175             Clones the provided hashref (using L - will die if not installed), then
176             censors the cloned data and returns it.
177              
178             Can be used both as a class or object method - the former for a quick way to use
179             it without having to instantiate an object, the latter if you want to apply
180             custom settings to the object before using it.
181              
182             # As a class method
183             my $censored_data = Data::Censor->clone_and_censor($data);
184              
185             # or as an object method
186             my $censor = Data::Censor->new( replacement => "SECRET!" );
187             my $censored_data = $censor->clone_and_censor($data);
188              
189             =cut
190              
191             sub clone_and_censor {
192 2     2 1 4295 my $class = shift;
193 2         10 my $data = shift;
194              
195 2 50       5 eval { require Clone; 1 }
  2         13  
  2         8  
196             or die "Can't clone data without Clone installed";
197              
198 2         67 my $cloned_data = Clone::clone($data);
199              
200             # if $class is a Data::Censor object, then we were called as an object method
201             # rather than a class method - that's fine - otherwise, create a new
202             # instance and use it:
203 2 100 66     27 my $self = ref $class && $class->isa('Data::Censor')
204             ? $class
205             : $class->new;
206              
207 2         11 $self->censor($cloned_data);
208 2         13 return $cloned_data;
209             }
210              
211              
212             =head1 AUTHOR
213              
214             David Precious (BIGPRESH), C<< >>
215              
216             This code was originally written for the L project by myself; I've
217             pulled it out into a seperate distribution as I was using it for code at work.
218              
219              
220              
221             =head1 SUPPORT
222              
223             You can find documentation for this module with the perldoc command.
224              
225             perldoc Data::Censor
226              
227              
228             =head1 LICENSE AND COPYRIGHT
229              
230             Copyright 2018 David Precious.
231              
232             This program is free software; you can redistribute it and/or modify it
233             under the terms of the the Artistic License (2.0). You may obtain a
234             copy of the full license at:
235              
236             L
237              
238             Any use, modification, and distribution of the Standard or Modified
239             Versions is governed by this Artistic License. By using, modifying or
240             distributing the Package, you accept this license. Do not use, modify,
241             or distribute the Package, if you do not accept this license.
242              
243             If your Modified Version has been derived from a Modified Version made
244             by someone other than you, you are nevertheless required to ensure that
245             your Modified Version complies with the requirements of this license.
246              
247             This license does not grant you the right to use any trademark, service
248             mark, tradename, or logo of the Copyright Holder.
249              
250             This license includes the non-exclusive, worldwide, free-of-charge
251             patent license to make, have made, use, offer to sell, sell, import and
252             otherwise transfer the Package with respect to any patent claims
253             licensable by the Copyright Holder that are necessarily infringed by the
254             Package. If you institute patent litigation (including a cross-claim or
255             counterclaim) against any party alleging that the Package constitutes
256             direct or contributory patent infringement, then this Artistic License
257             to you shall terminate on the date that such litigation is filed.
258              
259             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
260             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
261             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
262             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
263             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
264             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
265             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
266             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
267              
268              
269             =cut
270              
271             1; # End of Data::Censor