--- /dev/null
+package System::Introspector::Sudoers;
+use Moo;
+
+has sudoers_file => (
+ is => 'ro',
+ default => sub { '/etc/sudoers' },
+);
+
+has hostname => (
+ is => 'ro',
+ default => sub { scalar `hostname` },
+);
+
+sub gather {
+ my ($self) = @_;
+ my %file = $self->_gather_files($self->sudoers_file);
+ return \%file;
+}
+
+sub _gather_files {
+ my ($self, $file) = @_;
+ open my $fh, '<', $file
+ or return $file => { error => "Unable to read: $!" };
+ my @lines = <$fh>;
+ my %file = ($file => { body => join '', @lines });
+ for my $line (@lines) {
+ chomp $line;
+ if ($line =~ m{^#include\s+(.+)$}) {
+ my $inc_file = $self->_insert_hostname($1);
+ %file = (%file, $self->_gather_files($inc_file));
+ }
+ elsif ($line =~ m{^#includedir\s+(.+)$}) {
+ my $inc_dir = $self->_insert_hostname($1);
+ %file = (%file, $self->_gather_from_dir($inc_dir));
+ }
+ }
+ return %file;
+}
+
+sub _gather_from_dir {
+ my ($self, $dir) = @_;
+ opendir(my $dh, $dir);
+ return $dir => { error => "Unable to read dir $dir: $!" }
+ unless $dh;
+ my %file;
+ while (my $file = readdir $dh) {
+ next if $file =~ m{\.} or $file =~ m{~$};
+ %file = (%file, $self->_gather_files("$dir/$file"));
+ }
+ return %file;
+}
+
+sub _insert_hostname {
+ my ($self, $value) = @_;
+ my $hostname = $self->hostname;
+ $value =~ s{\%h}{$hostname}g;
+ return $value;
+}
+
+1;
--- /dev/null
+use strictures 1;
+use Test::More;
+use FindBin;
+
+use System::Introspector::Sudoers;
+
+my $dir = "$FindBin::Bin/sudoers-data";
+
+system("mkdir $dir");
+system("mkdir $dir/bar_host");
+
+my $start = write_file('sudoers',
+ 'foo bar',
+ 'baz qux',
+ "#include $dir/foo_%h",
+ "#includedir $dir/bar_%h",
+);
+
+my $foo_file = write_file('foo_host',
+ 'in foo',
+);
+my $bar_file = write_file("bar_host/baz",
+ 'in bar file',
+);
+
+my $probe = System::Introspector::Sudoers->new(
+ sudoers_file => $start,
+ hostname => 'host',
+);
+
+ok((my $data = $probe->gather), 'received data');
+
+my $inc = "#include $dir/foo_\%h\n#includedir $dir/bar_\%h\n";
+is_deeply $data, {
+ $start => { body => "foo bar\nbaz qux\n$inc" },
+ $foo_file => { body => "in foo\n" },
+ $bar_file => { body => "in bar file\n" },
+}, 'found files';
+
+system("rm $_") for $start, $foo_file, $bar_file;
+system("rmdir $dir/bar_host");
+system("rmdir $dir");
+done_testing;
+
+sub write_file {
+ my ($file, @lines) = @_;
+ my $path = "$FindBin::Bin/sudoers-data/$file";
+ open my $fh, '>', $path or die "Unable to write $path: $!\n";
+ print $fh map "$_\n", @lines;
+ return $path;
+}