Add filtering feature into CrowdsecAgent plugin

ci-el10
Yadd 3 months ago
parent 4ba59fa89a
commit 8f75b1061a
  1. 7
      lemonldap-ng-portal/MANIFEST
  2. 101
      lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CrowdsecFilter.pm
  3. 59
      lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CrowdSecAgent.pm
  4. 18
      lemonldap-ng-portal/t/61-CrowdSecAgent.t
  5. 1
      lemonldap-ng-portal/t/crowdsec-filters/bad.url.re
  6. 1
      lemonldap-ng-portal/t/crowdsec-filters/bad.url.txt
  7. 1
      lemonldap-ng-portal/t/crowdsec-filters/url1/bad.txt

@ -70,6 +70,7 @@ lib/Lemonldap/NG/Portal/Lib/Captcha.pm
lib/Lemonldap/NG/Portal/Lib/CAS.pm
lib/Lemonldap/NG/Portal/Lib/Choice.pm
lib/Lemonldap/NG/Portal/Lib/Code2F.pm
lib/Lemonldap/NG/Portal/Lib/CrowdsecFilter.pm
lib/Lemonldap/NG/Portal/Lib/CustomModule.pm
lib/Lemonldap/NG/Portal/Lib/DBI.pm
lib/Lemonldap/NG/Portal/Lib/LazyLoadedConfiguration.pm
@ -768,6 +769,7 @@ t/32-OIDC-JWT-type-header.t
t/32-OIDC-login_hint.t
t/32-OIDC-Logout-from-RP-bypass-confirm.t
t/32-OIDC-Logout-redirect-uri-not-allowed.t
t/32-OIDC-Logout-unauth.t
t/32-OIDC-Macro.t
t/32-OIDC-Metadata.t
t/32-OIDC-Native-SSO.t
@ -891,9 +893,9 @@ t/61-AdaptativeAuthenticationLevel.t
t/61-BruteForceProtection-with-Incremental-lockTimes-and-TOTP.t
t/61-BruteForceProtection-with-Incremental-lockTimes.t
t/61-BruteForceProtection.t
t/61-CrowdSec-ban.t
t/61-CrowdSec-warn.t
t/61-CrowdSec.t
t/61-CrowdSecAgent.t
t/61-ForceAuthn.t
t/61-GrantSession.t
t/61-LocationDetect.t
@ -1019,6 +1021,9 @@ t/CaptchaOldApi.pm
t/cas-lib.pm
t/CasHookPlugin.pm
t/ChoiceHookPlugin.pm
t/crowdsec-filters/bad.url.re
t/crowdsec-filters/bad.url.txt
t/crowdsec-filters/url1/bad.txt
t/Custom.pm
t/CustomMenu.pm
t/DbiCustomHash.pm

@ -0,0 +1,101 @@
package Lemonldap::NG::Portal::Lib::CrowdsecFilter;
use strict;
use Mouse::Role;
use Regexp::Assemble;
use constant knownCat => (qw(url));
use constant knownSuffixes => (qw(re txt));
# Initialization functions for CrowdsecFilter feature
sub initializeFilters {
my ($self) = @_;
my $filters = $self->parseFilters( $self->conf->{crowdsecFilters} );
if ( $filters and %$filters ) {
foreach my $cat ( keys %$filters ) {
my $re = Regexp::Assemble->new;
eval {
$re->add( map { qr/(?i)$_/ } @{ $filters->{$cat}->{re} } )
if $filters->{$cat}->{re};
$re->add( map { qr/(?i)\Q$_\E/ } @{ $filters->{$cat}->{txt} } )
if @{ $filters->{$cat}->{txt} };
};
if ($@) {
$self->logger->error("Unable to parst category $cat: $@");
}
else {
$self->filters->{$cat} = $re->re;
$self->logger->debug("RE $cat: $re");
}
}
}
}
sub parseFilters {
my ( $self, $dirname, $res, $cat ) = @_;
$self->logger->debug("Crowdsec filters, parsing $dirname");
$res //= {};
my $fh;
unless ( opendir $fh, $dirname ) {
$self->logger->error("Unable to read directory $dirname: $!");
return $res;
}
my @files = grep /\w/, readdir $fh;
closedir $fh;
LOOP: foreach my $file (@files) {
# Sub-directories fixes the category
my $path = "$dirname/$file";
if ( -d $path ) {
if ($cat) {
$self->parseFilters( $path, $res, $cat );
}
elsif ( my ($t) = grep { $file =~ m/^$_/ } knownCat ) {
$self->parseFilters( $path, $res, $t );
}
else {
$self->logger->error("Unknwon category for directory $path");
}
next LOOP;
}
$file =~ s/\.([^\.]+)$//;
my $type = $1;
unless ( $type and grep { $_ eq $type } knownSuffixes ) {
$self->logger->error("Bad suffix for $path, skipping");
next LOOP;
}
my $lcat = $cat;
unless ($lcat) {
$file =~ s/\.([^\.]+)$//;
$lcat = $1;
unless ($lcat) {
$self->logger->error("Malformed file $path (missing category)");
next LOOP;
}
unless ( grep { $_ eq $lcat } knownCat ) {
$self->logger->error("Unknown category $lcat for $path");
next LOOP;
}
}
unless ( open $fh, '<', $path ) {
$self->logger->error("Unable to read file $path: $!");
next LOOP;
}
$self->logger->debug(
"Crowdsec filters, adding content of $path into category $lcat, type $type"
);
my $c = 0;
foreach (<$fh>) {
next if /^\s*#/;
next unless /\w/;
s/[\r\n]//g;
push @{ $res->{$lcat}->{$type} }, $_;
$c++;
}
$self->logger->debug(" -> $c lines added");
}
return $res;
}
1;

@ -11,6 +11,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
PE_OK
PE_OPENID_BADID
PE_SAML_SIGNATURE_ERROR
PE_SENDRESPONSE
PE_USERNOTFOUND
);
@ -28,9 +29,18 @@ with 'Lemonldap::NG::Common::CrowdSec';
has rule => ( is => 'rw', default => sub { 0 } );
has filters => ( is => 'rw', default => sub { {} } );
# Entrypoint
use constant aroundSub =>
{ getUser => 'sendIpAlerts', authenticate => 'sendIpAlerts' };
use constant aroundSub => {
# Filter function (if crowdsecFilters is set)
controlUrl => 'controlUrl',
# Generate alerts for bad credentials
getUser => 'sendIpAlerts',
authenticate => 'sendIpAlerts'
};
sub init {
my ($self) = @_;
@ -44,6 +54,18 @@ sub init {
}
$self->rule(
$self->p->buildRule( $self->conf->{crowdsecAgent}, 'crowdsecAgent' ) );
if ( $self->conf->{crowdsecFilters} ) {
if ( -d $self->conf->{crowdsecFilters} ) {
with 'Lemonldap::NG::Portal::Lib::CrowdsecFilter';
$self->initializeFilters;
}
else {
$self->logger->error(
'Crowdsec filter directory not found, ignoring');
}
}
return 1;
}
@ -76,4 +98,37 @@ sub sendIpAlerts {
return $ret;
}
sub controlUrl {
my ( $self, $sub, $req ) = @_;
my $ret = $sub->($req);
return $ret if $ret;
if ( !$self->rule->( $req, $req->sessionInfo ) ) {
$self->logger->debug('Crowdsec-agent disabled for this env');
return $ret;
}
return $ret unless $self->filters;
if ( $self->filters->{url}
and $req->env->{REQUEST_URI} =~ $self->filters->{url} )
{
my $msg = 'Bad URI detected: ' . $req->env->{REQUEST_URI};
$self->alert( $req->address, $msg, { reason => "LLNG bad URL" } )
? $self->auditLog(
$req,
code => 200,
message => "Alert sent to Crowdsec: $msg",
ip => $req->address,
uri => $req->env->{REQUEST_URI},
matchingPart => $&,
)
: $self->logger->error(
"Unable to send alert to Crowdsec (was '$msg' for "
. $req->address );
$req->response( [ 404, [], ['Not found'] ] );
return PE_SENDRESPONSE;
}
return $ret;
}
1;

@ -80,6 +80,7 @@ my $client = LLNG::Manager::Test->new( {
crowdsecMachineId => 'llng',
crowdsecPassword => 'llngpwd',
crowdsecAgent => 1,
crowdsecFilters => 't/crowdsec-filters',
}
}
);
@ -122,6 +123,23 @@ subtest 'Report unknown user to Crowdsec', sub {
ok( !$lastAlertTypeIsBan, 'Alert type is "alert"' );
};
our %badUrls = (
'Filter in a sub-directory named url1' => '/bb/.htaccess',
'Filter type re in main directory' => '/aa/phpmyadmin',
'Filter type txt in main directory' => '/config.php',
);
subtest 'Report bad urls to Crowdsec', sub {
my $prevBan = $ban;
foreach my $subtest ( sort keys %badUrls ) {
my $url = $badUrls{$subtest};
subtest $subtest => sub {
ok( $res = $client->_get($url), "Test bad url $url" );
ok( $res->[0] == 404, '404 not found' );
ok( $ban == ++$prevBan, "Bad url detected" );
};
}
};
clean_sessions();
done_testing();

@ -0,0 +1 @@
^.*/php(?:my|ldap|pg)admin.*$
Loading…
Cancel
Save