Repository: netbox-community/netbox
Pull request: 10653 log failed login attempts on INFO
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to opening a pull request. This helps avoid
    waste time and effort on a proposed change that we might not be able to
    accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WILL BE CLOSED AUTOMATICALLY.

    Please specify your assigned issue number on the line below.
-->
### Fixes: #10653 

<!--
    Please include a summary of the proposed changes below.
-->
@jeremystretch Not completely sure of what is the best way here.  The log was done for failed login attempts but at DEBUG level which would be fairly spammy to have on, changed to INFO level also included failed username - I don't think that would violate personal-identifiable-information security issues...  Probably up for discussion if this makes sense to change (from a logging standpoint if that is too much info for some people, and security if the username should be shown).

Problem statement:
Make it possible to log failed login attempts
### NetBox version

v3.2.9

### Feature type

New functionality

### Proposed functionality

I would like to have netbox log every login attempt that is made, however this doesn't seem possible, i currently have this logging config

```
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'netobx.*': {
        'handlers': ['console'],
        'level': 'INFO',
        }
    },
}
```
This config logs every time a user logs in successfully but not when a user fails to authenticate.

### Use case

This would make it possible to see if netbox is currently being bruteforced, this would be especially helpful to deployments in the cloud where it is accessible to the web.

### Database changes

N/A

### External dependencies

N/A

Hints from issue discussion:

Commit message: 10653 use signal to log failed login attempts

Patch to review:
diff --git a/netbox/users/apps.py b/netbox/users/apps.py
new file mode 100644
index 00000000000..6dff227d6d4
--- /dev/null
+++ b/netbox/users/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class HomeConfig(AppConfig):
+    name = 'users'
+
+    def ready(self):
+        import users.signals
diff --git a/netbox/users/signals.py b/netbox/users/signals.py
new file mode 100644
index 00000000000..ec7e113859a
--- /dev/null
+++ b/netbox/users/signals.py
@@ -0,0 +1,10 @@
+import logging
+from django.dispatch import receiver
+from django.contrib.auth.signals import user_login_failed
+
+
+@receiver(user_login_failed)
+def log_user_login_failed(sender, credentials, request, **kwargs):
+    logger = logging.getLogger('netbox.auth.login')
+    username = credentials.get("username", None)
+    logger.info(f"Failed login attempt for username: {username}")
diff --git a/netbox/users/views.py b/netbox/users/views.py
index 33ef3fadd85..c688d6b4f29 100644
--- a/netbox/users/views.py
+++ b/netbox/users/views.py
@@ -106,7 +106,7 @@ def post(self, request):
             return self.redirect_to_next(request, logger)
 
         else:
-            logger.debug("Login form validation failed")
+            logger.debug(f"Login form validation failed for username: {form['username'].value()}")
 
         return render(request, self.template_name, {
             'form': form,


Code review comments:
diff_hunk: @@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class HomeConfig(AppConfig):
path: netbox/users/apps.py
text: @user1:
To keep consistent with the other apps:
```suggestion
class UsersConfig(AppConfig):
```

diff_hunk: @@ -0,0 +1,10 @@
+import logging
+from django.dispatch import receiver
+from django.contrib.auth.signals import user_login_failed
+
+
+@receiver(user_login_failed)
+def log_user_login_failed(sender, credentials, request, **kwargs):
+    logger = logging.getLogger('netbox.auth.login')
+    username = credentials.get("username", None)
path: netbox/users/signals.py
text: @user1:
```suggestion
    username = credentials.get("username")
```