Data-Censor-0.04/0000755000175000017500000000000015024746371013046 5ustar davidpdavidpData-Censor-0.04/META.yml0000644000175000017500000000137215024746371014322 0ustar davidpdavidp--- abstract: 'censor sensitive stuff in a data structure' author: - 'David Precious ' build_requires: Test::More: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 1 generated_by: 'ExtUtils::MakeMaker version 7.44, CPAN::Meta::Converter version 2.150010' license: unknown meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Data-Censor no_index: directory: - t - inc requires: Ref::Util: '0' perl: '5.006' resources: bugtracker: https://github.com/bigpresh/Data-Censor/issues homepage: https://github.com/bigpresh/Data-Censor repository: https://github.com/bigpresh/Data-Censor version: '0.04' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Data-Censor-0.04/Changes0000644000175000017500000000064415024746343014344 0ustar davidpdavidpRevision history for Data-Censor 0.04 2025-06-19 Deal with object instances, handle recursions better (thanks to @yanick in GH PR-2) 0.03 2018-10-04 Add old_password to default list of sensitive fields. 0.02 2014-03-12 Rename censored_data to clone_and_censor, make it work as class or object method, better docs. 0.01 2014-03-12 Initial release. Data-Censor-0.04/META.json0000644000175000017500000000233515024746371014472 0ustar davidpdavidp{ "abstract" : "censor sensitive stuff in a data structure", "author" : [ "David Precious " ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.44, CPAN::Meta::Converter version 2.150010", "license" : [ "unknown" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Data-Censor", "no_index" : { "directory" : [ "t", "inc" ] }, "prereqs" : { "build" : { "requires" : { "Test::More" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "runtime" : { "requires" : { "Ref::Util" : "0", "perl" : "5.006" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/bigpresh/Data-Censor/issues" }, "homepage" : "https://github.com/bigpresh/Data-Censor", "repository" : { "url" : "https://github.com/bigpresh/Data-Censor" } }, "version" : "0.04", "x_serialization_backend" : "JSON::PP version 4.04" } Data-Censor-0.04/README0000644000175000017500000001304112310052744013712 0ustar davidpdavidpNAME Data::Censor - censor sensitive stuff in a data structure VERSION Version 0.02 SYNOPSIS # OO way, letting you specify your own list of sensitive-looking fields, and # what they should be replaced by (all options here are optional) my $censor = Data::Censor->new( # Specify which fields to censor: sensitive_fields => [ qw(card_number password) ], # Specify text to replace their values with: replacement => '(Sensitive data hidden)', # Or specify callbacks for each field name which return the "censored" # value - in this case, masking a card number (PAN) to show only the # last four digits: replacement_callbacks => { card_number => sub { my $pan = shift; return "x" x (length($pan) - 4) . substr($pan, -4, 4); }, }, ); # Censor the data in-place (changes the data structure, returns the number # of keys censored) my $censor_count = $censor->censor(\%data); # Alternate non-OO interface, using default settings and returning a cloned # version of the data after censoring: my $censored_data = Data::Censor->clone_and_censor(\%data); new (CONSTRUCTOR) Accepts the following arguments: sensitive_fields Either an arrayref of sensitive fields, checked for equality, or a regex to test against each key to see if it's considered sensitive. replacement The string to replace each value with. Any censoring callback provided in `replacement_callbacks' which matches this key will take precedence over this straightforward value. replacement_callbacks A hashref of key => sub {...}, where each key is a column name to match, and the coderef takes the uncensored value and returns the censored value, letting you for instance mask a card number but leave the last 4 digits visible. If you provide both `replacement' and `replacement_callbacks', any callback defined which matches the key being considered takes precedence. =back METHODS censor Given a data structure (hashref), clones it and returns the cloned version after censoring potentially sensitive data within. clone_and_censor Clones the provided hashref (using Clone - will die if not installed), then censors the cloned data and returns it. Can be used both as a class or object method - the former for a quick way to use it without having to instantiate an object, the latter if you want to apply custom settings to the object before using it. # As a class method my $censored_data = Data::Censor->clone_and_censor($data); # or as an object method my $censor = Data::Censor->new( replacement => "SECRET!" ); my $censored_data = $censor->clone_and_censor($data); AUTHOR David Precious (BIGPRESH), `' This code was originally written for the Dancer project by myself; I've pulled it out into a seperate distribution as I was using it for code at work. SUPPORT You can find documentation for this module with the perldoc command. perldoc Data::Censor LICENSE AND COPYRIGHT Copyright 2014 David Precious. This program is free software; you can redistribute it and/or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: http://www.perlfoundation.org/artistic_license_2_0 Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Data-Censor-0.04/Makefile.PL0000644000175000017500000000200215024743632015007 0ustar davidpdavidpuse 5.006; use strict; use warnings FATAL => 'all'; use ExtUtils::MakeMaker; WriteMakefile( NAME => 'Data::Censor', AUTHOR => q{David Precious }, VERSION_FROM => 'lib/Data/Censor.pm', ABSTRACT_FROM => 'lib/Data/Censor.pm', LICENSE => 'Artistic_2_0', PL_FILES => {}, MIN_PERL_VERSION => 5.006, CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => 0, }, BUILD_REQUIRES => { 'Test::More' => 0, }, PREREQ_PM => { 'Ref::Util' => 0, #'ABC' => 1.6, #'Foo::Bar::Module' => 5.0401, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Data-Censor-*' }, META_MERGE => { resources => { repository => 'https://github.com/bigpresh/Data-Censor', bugtracker => 'https://github.com/bigpresh/Data-Censor/issues', homepage => 'https://github.com/bigpresh/Data-Censor', }, }, ); Data-Censor-0.04/t/0000755000175000017500000000000015024746370013310 5ustar davidpdavidpData-Censor-0.04/t/01-basic.t0000644000175000017500000000524015024743632014773 0ustar davidpdavidp#!perl -T use 5.006; use strict; use warnings FATAL => 'all'; use Test::More; use Data::Censor; plan tests => 13; diag( "Testing Data::Censor $Data::Censor::VERSION, Perl $], $^X" ); sub get_data { return { name => 'David Precious', email => 'davidp@preshweb.co.uk', password => 'supersecret', card => { pan => '4929000000006', cvv => '123', expiry => '03/16', }, }; } # Basic stuff. my $censor = Data::Censor->new; my $data = get_data(); my $count = $censor->censor($data); my $hidden = 'Hidden (looks potentially sensitive)'; is($count, 3, "Two items censored with default config"); is($data->{password}, $hidden, 'password field censored'); is($data->{email}, 'davidp@preshweb.co.uk', 'email field not censored'); is($data->{card}{pan}, $hidden, 'pan field censored (recursion works)'); is($data->{card}{expiry}, '03/16', 'expiry field not censored'); # Test replacement callback $censor = Data::Censor->new( replacement_callbacks => { pan => sub { my $pan = shift; return "x" x (length($pan) - 4) . substr($pan, -4, 4); }, }, ); $data = get_data(); $count = $censor->censor($data); is($data->{password}, $hidden, "password censored normally"); is ($data->{card}{pan}, 'xxxxxxxxx0006', "pan censored by callback"); # Test basic clone_and_censor call SKIP: { eval { require Clone }; skip "Clone not installed", 4 if $@; # Test using clone_and_censor as a class method my $clone_and_censor = Data::Censor->clone_and_censor(get_data()); is($clone_and_censor->{password}, $hidden, "clone_and_censor password censored (used as class method)"); is($clone_and_censor->{email}, 'davidp@preshweb.co.uk', "clone_and_censor email not censored (used as class method)"); # Test using clone_and_censor as an object method my $censor = Data::Censor->new( replacement => 'FOO' ); $clone_and_censor = $censor->clone_and_censor(get_data()); is ($clone_and_censor->{password}, 'FOO', "clone_and_censor password censored (used as object method)"); is ($clone_and_censor->{email}, 'davidp@preshweb.co.uk', "clone_and_censor email not censored (used as object method)"); } subtest 'objects as hashes' => sub { my $data = bless { password => 'hush', } => 'MyThing'; $censor->censor($data); isnt $data->{password} => 'hush', 'can process object instances'; }; subtest 'recursive' => sub { my $data = { foo => 1, }; my $y = { bar => 2, password => 'hush', data => $data, }; $data->{y} = $y; $censor->censor($data); isnt $data->{y}{password} => 'hush', 'password is hidden'; }; Data-Censor-0.04/t/00-load.t0000644000175000017500000000034612310026427014621 0ustar davidpdavidp#!perl -T use 5.006; use strict; use warnings FATAL => 'all'; use Test::More; plan tests => 1; BEGIN { use_ok( 'Data::Censor' ) || print "Bail out!\n"; } diag( "Testing Data::Censor $Data::Censor::VERSION, Perl $], $^X" ); Data-Censor-0.04/t/pod.t0000644000175000017500000000040112310026427014237 0ustar davidpdavidp#!perl -T use 5.006; use strict; use warnings FATAL => 'all'; use Test::More; # Ensure a recent version of Test::Pod my $min_tp = 1.22; eval "use Test::Pod $min_tp"; plan skip_all => "Test::Pod $min_tp required for testing POD" if $@; all_pod_files_ok(); Data-Censor-0.04/t/manifest.t0000644000175000017500000000050712310026427015272 0ustar davidpdavidp#!perl -T use 5.006; use strict; use warnings FATAL => 'all'; use Test::More; unless ( $ENV{RELEASE_TESTING} ) { plan( skip_all => "Author tests not required for installation" ); } my $min_tcm = 0.9; eval "use Test::CheckManifest $min_tcm"; plan skip_all => "Test::CheckManifest $min_tcm required" if $@; ok_manifest(); Data-Censor-0.04/MANIFEST0000644000175000017500000000044215024746371014177 0ustar davidpdavidpChanges lib/Data/Censor.pm Makefile.PL MANIFEST This list of files README t/00-load.t t/01-basic.t t/manifest.t t/pod.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) Data-Censor-0.04/lib/0000755000175000017500000000000015024746370013613 5ustar davidpdavidpData-Censor-0.04/lib/Data/0000755000175000017500000000000015024746370014464 5ustar davidpdavidpData-Censor-0.04/lib/Data/Censor.pm0000644000175000017500000001773015024746233016261 0ustar davidpdavidppackage Data::Censor; use 5.006; use strict; use warnings FATAL => 'all'; use Carp; use Ref::Util qw/ is_hashref /; =head1 NAME Data::Censor - censor sensitive stuff in a data structure =head1 VERSION Version 0.04 =cut our $VERSION = '0.04'; =head1 SYNOPSIS # OO way, letting you specify your own list of sensitive-looking fields, and # what they should be replaced by (all options here are optional) my $censor = Data::Censor->new( # Specify which fields to censor: sensitive_fields => [ qw(card_number password) ], # Specify text to replace their values with: replacement => '(Sensitive data hidden)', # Or specify callbacks for each field name which return the "censored" # value - in this case, masking a card number (PAN) to show only the # last four digits: replacement_callbacks => { card_number => sub { my $pan = shift; return "x" x (length($pan) - 4) . substr($pan, -4, 4); }, }, ); # Censor the data in-place (changes the data structure, returns the number # of keys censored) my $censor_count = $censor->censor(\%data); # Alternate non-OO interface, using default settings and returning a cloned # version of the data after censoring: my $censored_data = Data::Censor->clone_and_censor(\%data); =head1 new (CONSTRUCTOR) Accepts the following arguments: =over =item sensitive_fields Either an arrayref of sensitive fields, checked for equality, or a regex to test against each key to see if it's considered sensitive. =item replacement The string to replace each value with. Any censoring callback provided in C which matches this key will take precedence over this straightforward value. =item replacement_callbacks A hashref of key => sub {...}, where each key is a column name to match, and the coderef takes the uncensored value and returns the censored value, letting you for instance mask a card number but leave the last 4 digits visible. If you provide both C and C, any callback defined which matches the key being considered takes precedence. =back =cut sub new { my $class = shift; my %args = @_; my $self = bless {} => $class; if ( ref $args{sensitive_fields} eq 'Regexp' ) { $self->{censor_regex} = $args{sensitive_fields}; } elsif ( ref $args{sensitive_fields} eq 'ARRAY' ) { $self->{is_sensitive_field} = { map { $_ => 1 } @{ $args{sensitive_fields} } }; } else { $self->{is_sensitive_field} = { map { $_ => 1 } qw( pass password old_password secret private_key cardnum card_number pan cvv cvv2 ccv ) }; } if ( is_hashref $args{replacement_callbacks} ) { $self->{replacement_callbacks} = $args{replacement_callbacks}; } if ( exists $args{replacement} ) { $self->{replacement} = $args{replacement}; } else { $self->{replacement} = 'Hidden (looks potentially sensitive)'; } $self->{recurse_limit} = $args{recurse_limit} || 100; return $self; } =head1 METHODS =head2 censor Given a data structure (hashref), clones it and returns the cloned version after censoring potentially sensitive data within. =cut sub censor { my ( $self, $data, $recurse_count, $visited ) = @_; $recurse_count ||= 0; $visited ||= {}; no warnings 'recursion'; # we're checking ourselves. if ( $recurse_count++ > $self->{recurse_limit} ) { warn "Data exceeding $self->{recurse_limit} levels"; return; } croak('censor expects a hashref') unless is_hashref $data; my $censored = 0; for my $key ( keys %$data ) { if ( is_hashref $data->{$key} ) { $censored += $self->censor( $data->{$key}, $recurse_count, $visited ) unless $visited->{ $data->{$key} }++; next; } next unless ( $self->{is_sensitive_field} && $self->{is_sensitive_field}{ lc $key } ) or ( $self->{censor_regex} && $key =~ $self->{censor_regex} ); # OK, censor this if ( $self->{replacement_callbacks}{ lc $key } ) { $data->{$key} = $self->{replacement_callbacks}{ lc $key }->( $data->{$key} ); $censored++; } else { $data->{$key} = $self->{replacement}; $censored++; } } return $censored; } =head2 clone_and_censor Clones the provided hashref (using L - will die if not installed), then censors the cloned data and returns it. Can be used both as a class or object method - the former for a quick way to use it without having to instantiate an object, the latter if you want to apply custom settings to the object before using it. # As a class method my $censored_data = Data::Censor->clone_and_censor($data); # or as an object method my $censor = Data::Censor->new( replacement => "SECRET!" ); my $censored_data = $censor->clone_and_censor($data); =cut sub clone_and_censor { my $class = shift; my $data = shift; eval { require Clone; 1 } or die "Can't clone data without Clone installed"; my $cloned_data = Clone::clone($data); # if $class is a Data::Censor object, then we were called as an object method # rather than a class method - that's fine - otherwise, create a new # instance and use it: my $self = ref $class && $class->isa('Data::Censor') ? $class : $class->new; $self->censor($cloned_data); return $cloned_data; } =head1 AUTHOR David Precious (BIGPRESH), C<< >> This code was originally written for the L project by myself; I've pulled it out into a seperate distribution as I was using it for code at work. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Data::Censor =head1 LICENSE AND COPYRIGHT Copyright 2018 David Precious. This program is free software; you can redistribute it and/or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: L Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =cut 1; # End of Data::Censor