Andy Baugh 3 gadi atpakaļ
vecāks
revīzija
94882d45e6
3 mainītis faili ar 105 papildinājumiem un 45 dzēšanām
  1. 2 0
      .gitignore
  2. 84 30
      ldapscanner
  3. 19 15
      ldapscanner.cfg.dist

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
+.current_diff.json
+.last.json
 ldapscanner.cfg
 *.swp
 *.swo

+ 84 - 30
ldapscanner

@@ -6,47 +6,101 @@ use warnings;
 use Config::Tiny  ();
 use File::Slurper ();
 use Net::LDAP     ();
+use JSON::XS      ();
 
 die "No config file!" if !-f 'ldapscanner.cfg';
 my $cfg     = Config::Tiny->read('ldapscanner.cfg');
 my @servers = split( ',', $cfg->{'server'}{'hosts'} );
-my $proto   = $cfg->{'server'}{'ldaps'} ? 'ldaps' : 'ldap';
-my $port    = $cfg->{'server'}{'port'};
-
-foreach my $server (@servers) {
-    _log( "debug", "Connecting to $proto://$server:$port..." );
-    my $conn = Net::LDAP->new(
-        "$proto://$server:$port",
-        'debug'   => $cfg->{'prefs'}{'loglevel'} eq 'debug',
-        'onerror' => 'die',
-        'async'   => 0,
-    ) or die $@;
-    _log( "debug", "Binding anonymously..." );
-    my $result = $conn->bind( 'anonymous' => 1 );
-    $result->code and die $result->error;
-
-    # Login if you wanna do that
-    if( scalar( grep { $cfg->{'creds'}{$_} } qw{id_field id_value pass} ) eq 3 ) {
-        #$conn->unbind;
-        _log( "debug", "Now binding as $cfg->{'creds'}{'id_value'}..." );
-        #$bind_args = "dn=$result->dn";
-        #$result = $conn->bind($bind_args);
+my $proto = 'ldap';
+my %extra_args = (
+    'onerror' => ( $cfg->{'prefs'}{'loglevel'} eq 'warn' ) ? 'warn' : 'die',
+    'debug'   => $cfg->{'prefs'}{'loglevel'} eq 'debug',
+    'port'    => $cfg->{'server'}{'port'},
+    'timeout' => $cfg->{'server'}{'timeout'},
+);
+if( $cfg->{'server'}{'ldaps'} ) {
+    $proto = 'ldaps';
+    $extra_args{'verify'} = 'no';
+}
+$extra_args{'scheme'} = $proto;
+
+_log( "info", "Connecting..." );
+my $conn = Net::LDAP->new( \@servers, %extra_args ) or die $@;
+
+my @bind_args;
+my $user = 'anonymous';
+if( $cfg->{'creds'}{'dn'} && $cfg->{'creds'}{'pass'} ) {
+    push @bind_args, $cfg->{'creds'}{'dn'}, 'password', $cfg->{'creds'}{'pass'};
+    $user = "DN: $cfg->{'creds'}{'dn'}";
+}
+_log( "info", "Binding as $user..." );
+my $result = $conn->bind(@bind_args);
+$result->code and die $result->error;
+
+_log( "info", "Beginning search..." );
+$result = $conn->search(
+    base   => $cfg->{'search'}{'base'},
+    filter => $cfg->{'search'}{'filter'},
+);
+$result->code and die $result->error;
+
+my $old_entries = {};
+if( -f '.last.json' ) {
+    _log( "info", "Stored run data found, will compare against previous run." );
+    my $raw = File::Slurper::read_text('.last.json');
+    $old_entries = JSON::XS->new->decode($raw);
+} else {
+    _log( "info", "No run data found. Storing current data and exiting." );
+}
+
+my $new_entries = {};
+foreach my $entry ($result->entries) {
+    my $hr = {};
+    foreach my $attr ($entry->attributes( 'nooptions' => 1 )) {
+        $hr->{$attr} = $entry->get_value( $attr, 'nooptions' => 1 );
     }
+    $new_entries->{$entry->dn} = $hr;
+}
+_log( "info", "Found " . scalar(keys(%$new_entries)) . " records total" );
 
-    $result = $conn->search(
-        base   => $cfg->{'search'}{'base'},
-        filter => $cfg->{'search'}{'filter'},
-    );
-    $result->code and die $result->error;
-    foreach my $entry ($result->entries) {
-        $entry->dump;
+my $diff = {
+    'added'   => { map { $_ => $new_entries->{$_} } grep { !exists($old_entries->{$_}) } keys(%$new_entries) },
+    'removed' => { map { $_ => $old_entries->{$_} } grep { !exists($new_entries->{$_}) } keys(%$old_entries) },
+};
+
+File::Slurper::write_text( '.current_diff.json', JSON::XS->new->pretty->encode($diff) );
+File::Slurper::write_text( '.last.json', JSON::XS->new->pretty->encode($new_entries) );
+
+my @skip = split( ',', $cfg->{'display'}{'ignore_attrs'} );
+_log( "warn", "Added records  : " . scalar(keys(%{$diff->{'added'}})) );
+_print_records( $diff->{'added'}, @skip );
+_log( "warn", "Removed records: " . scalar(keys(%{$diff->{'removed'}})) );
+_print_records( $diff->{'removed'}, @skip );
+
+sub _print_records {
+    my ( $hr, @skip ) = @_;
+    foreach my $key ( keys(%$hr) ) {
+        _log( "warn", "------------------------------------------------" );
+        _log( "warn", "DN: " . $key );
+        foreach my $attr ( keys(%{$hr->{$key}}) ) {
+            next if grep { $_ eq $attr } @skip;
+            _log( "warn", "  $attr: $hr->{$key}{$attr}" );
+        }
     }
-    last if $result;
+    return;
 }
 
 sub _log {
     my ( $level, $msg ) = @_;
-    print "$msg\n" if $level eq $cfg->{'prefs'}{'loglevel'};
+    my %levels = (
+        'warn'  => 1,
+        'error' => 2,
+        'info'  => 3,
+        'debug' => 4,
+    );
+    my $loglevel = $levels{$cfg->{'prefs'}{'loglevel'}};
+    die "Bad log level" if !$loglevel;
+    print "$msg\n" if $levels{$level} <= $loglevel;
     return;
 }
 

+ 19 - 15
ldapscanner.cfg.dist

@@ -4,30 +4,34 @@
 hosts=ldap.host,another.host
 port=636
 ldaps=1
+timeout=60
 
 [search]
-# Put in what records you wanna find (and diff over time) here
+# Put in what records you wanna find (and diff over time) here.
+# Surround your filter with parens, especially if you need to
+# do searches on multiple things, a la
+# (&(objectClass=capedAvenger)(o=LegionOfDoom))
 base=ou=superheroes,dc=marvel,dc=comics
-filter=(&(objectClass=capedAvenger))
+filter=(objectClass=capedAvenger)
+ignore_attrs=bigBinaryBlobField,crapIDontCareAbout
 
 [creds]
 # If you need to auth with the server in order to get the
 # records you need, then enter the details below.
 # If not, don't.
-# id_field is what is distinct about your LDAP record
-# given the search filter, id_value is the value of the field
-# If the base & filter are the same, just enter it in
-# again here.
-base=ou=superheroes,dc=marvel,dc=comics
-filter=(&(objectClass=capedAvenger))
-id_field=codename
-id_value=Lobo
+dn=heroName=Lobo,ou=superheroes,dc=marvel,dc=comics
 pass=S4NT4DR00LZ
 
+[display]
+# Put anything you don't want displayed in your cron email here.
+# Separated by commas, just like hosts
+# Binary blob fields like jpegPhoto get annoying quick.
+ignore_attrs=jpegPhoto
+
 [prefs]
 # Loglevels:
-#  info:  only the diff is printed
-#  error: die on errors
-#  warn:  warn on errors
-#  debug: die on errors, also print debug info
-loglevel=info
+#  warn:  warn on errors, print diff.
+#  error: die on errors, print diff. Recommended for crons.
+#  info:  die on errors, print informational messages
+#  debug: die on errors, also print debug & info messages
+loglevel=error