diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 145205874..89aefafca 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -674,10 +674,9 @@ async def m029_create_audit_table(db): ip_address TEXT, user_id TEXT, path TEXT, - route_path TEXT, request_type TEXT, request_method TEXT, - query_string TEXT, + request_details TEXT, response_code TEXT, duration REAL NOT NULL, delete_at TIMESTAMP, diff --git a/lnbits/core/models/audit.py b/lnbits/core/models/audit.py index f361bd557..2211e3e29 100644 --- a/lnbits/core/models/audit.py +++ b/lnbits/core/models/audit.py @@ -14,10 +14,9 @@ class AuditEntry(BaseModel): ip_address: Optional[str] = None user_id: Optional[str] = None path: Optional[str] = None - route_path: Optional[str] = None request_type: Optional[str] = None request_method: Optional[str] = None - query_string: Optional[str] = None + request_details: Optional[str] = None response_code: Optional[str] = None duration: float delete_at: Optional[datetime] = None @@ -34,7 +33,6 @@ class AuditFilters(FilterModel): "ip_address", "user_id", "path", - "route_path", "request_method", "response_code", ] @@ -46,6 +44,5 @@ class AuditFilters(FilterModel): ip_address: Optional[str] = None user_id: Optional[str] = None path: Optional[str] = None - route_path: Optional[str] = None request_method: Optional[str] = None response_code: Optional[str] = None diff --git a/lnbits/core/templates/admin/_tab_audit.html b/lnbits/core/templates/admin/_tab_audit.html index 8da25d0fd..30bfc7d8d 100644 --- a/lnbits/core/templates/admin/_tab_audit.html +++ b/lnbits/core/templates/admin/_tab_audit.html @@ -2,8 +2,8 @@
Audit
-
-
+
+
-
+
+ +
+
+ + + + + + Record Request Body + Warning: the request body can have large size. Use it with + caution. + + +
+
Record IP Address - If disabled audit entries will not record the IP - address + Save the client IP address. + + +
+
+ + + + + + Record Path Parameters + Recommended. + + +
+
+ + + + + + Record Query Parameters + Recommended.
- +

Include HTTP Methods

@@ -65,7 +119,7 @@ @keydown.enter="addIncludeResponseCode" type="text" label="HTTP Response code (regex)" - hint="List of HTTP codes to be included (regex match). Empty lists means all." + hint="List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*" > @@ -83,7 +137,7 @@
- +

Include Paths

diff --git a/lnbits/core/templates/audit/index.html b/lnbits/core/templates/audit/index.html index d1a5ec650..4d3d3cee5 100644 --- a/lnbits/core/templates/audit/index.html +++ b/lnbits/core/templates/audit/index.html @@ -17,7 +17,7 @@ - -
+
+ + Request Details + + + + + + +
+
Response: start_time = datetime.now(timezone.utc) + request_details = await self._request_details(request) response: Optional[Response] = None + try: response = await call_next(request) assert response return response finally: duration = (datetime.now(timezone.utc) - start_time).total_seconds() - await self._log_audit(request, response, duration) + await self._log_audit(request, response, duration, request_details) async def _log_audit( - self, request: Request, response: Optional[Response], duration: float + self, + request: Request, + response: Optional[Response], + duration: float, + request_details: Optional[str], ): try: http_method = request.scope.get("method", None) path = request.scope.get("path", None) response_code = str(response.status_code) if response else None - if not settings.is_http_request_auditable(http_method, path, response_code): + if not settings.audit_http_request(http_method, path, response_code): return None ip_address = ( request.client.host @@ -161,11 +168,10 @@ class AuditMiddleware(BaseHTTPMiddleware): data = AuditEntry( ip_address=ip_address, user_id=request.scope.get("user_id", None), - path=path, - route_path=getattr(request.scope.get("route", {}), "path", None), + path=getattr(request.scope.get("route", {}), "path", None), request_type=request.scope.get("type", None), request_method=http_method, - query_string=request.scope.get("query_string", None), + request_details=request_details, response_code=response_code, duration=duration, ) @@ -173,6 +179,30 @@ class AuditMiddleware(BaseHTTPMiddleware): except Exception as ex: logger.warning(ex) + async def _request_details(self, request: Request) -> Optional[str]: + if not settings.audit_http_request_details(): + return None + + try: + http_method = request.scope.get("method", None) + path = request.scope.get("path", None) + + if not settings.audit_http_request(http_method, path): + return None + + details: dict = {} + if settings.lnbits_audit_log_path_params: + details["path_params"] = request.path_params + if settings.lnbits_audit_log_query_params: + details["query_params"] = dict(request.query_params) + if settings.lnbits_audit_log_request_body: + _body = await request.body() + details["body"] = _body.decode("utf-8") + return json.dumps(details) + except Exception as e: + logger.warning(e) + return None + def add_ratelimit_middleware(app: FastAPI): core_app_extra.register_new_ratelimiter() diff --git a/lnbits/settings.py b/lnbits/settings.py index b2d0adc7e..d7efb5b8b 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -517,6 +517,9 @@ class AuditSettings(LNbitsSettings): lnbits_audit_retention_days: int = Field(default=7) lnbits_audit_log_ip_address: bool = Field(default=False) + lnbits_audit_log_path_params: bool = Field(default=True) + lnbits_audit_log_query_params: bool = Field(default=True) + lnbits_audit_log_request_body: bool = Field(default=False) # List of paths to be included (regex match). Empty list means all. lnbits_audit_include_paths: list[str] = Field(default=[".*api/v1/.*"]) @@ -534,31 +537,32 @@ class AuditSettings(LNbitsSettings): # List of HTTP codes to be included (regex match). Empty lists means all. lnbits_audit_http_response_codes: list[str] = Field(default=[]) - def is_http_request_auditable( + def audit_http_request_details(self) -> bool: + return ( + self.lnbits_audit_log_path_params + or self.lnbits_audit_log_query_params + or self.lnbits_audit_log_request_body + ) + + def audit_http_request( self, - http_method: Optional[str], - path: Optional[str], - http_response_code: Optional[str], + http_method: Optional[str] = None, + path: Optional[str] = None, + http_response_code: Optional[str] = None, ) -> bool: if not self.lnbits_audit_enabled: return False if len(self.lnbits_audit_http_methods) != 0: - if not http_method or http_method not in self.lnbits_audit_http_methods: + if not http_method: + return False + if http_method not in self.lnbits_audit_http_methods: return False if not self._is_http_request_path_auditable(path): return False - if len(self.lnbits_audit_http_response_codes) != 0: - is_response_code_included = True - if not http_response_code: - return False - for response_code in self.lnbits_audit_http_response_codes: - if _re_fullmatch_safe(response_code, http_response_code): - is_response_code_included = True - break - if not is_response_code_included: - return False + if not self._is_http_response_code_auditable(http_response_code): + return False return True @@ -577,6 +581,22 @@ class AuditSettings(LNbitsSettings): return False + def _is_http_response_code_auditable( + self, http_response_code: Optional[str] + ) -> bool: + if not http_response_code: + # No response code means only request filters should apply + return True + + if len(self.lnbits_audit_http_response_codes) == 0: + return True + + for response_code in self.lnbits_audit_http_response_codes: + if _re_fullmatch_safe(response_code, http_response_code): + return True + + return False + class EditableSettings( UsersSettings, diff --git a/lnbits/static/js/admin.js b/lnbits/static/js/admin.js index 8de19a52b..a67a93aa0 100644 --- a/lnbits/static/js/admin.js +++ b/lnbits/static/js/admin.js @@ -112,7 +112,10 @@ window.app = Vue.createApp({ } const paths = this.formData.lnbits_audit_include_paths if (!paths.includes(this.formAddIncludePath)) { - paths.push(this.formAddIncludePath) + this.formData.lnbits_audit_include_paths = [ + ...paths, + this.formAddIncludePath + ] } this.formAddIncludePath = '' }, @@ -126,7 +129,10 @@ window.app = Vue.createApp({ } const paths = this.formData.lnbits_audit_exclude_paths if (!paths.includes(this.formAddExcludePath)) { - paths.push(this.formAddExcludePath) + this.formData.lnbits_audit_exclude_paths = [ + ...paths, + this.formAddExcludePath + ] } this.formAddExcludePath = '' }, @@ -139,9 +145,12 @@ window.app = Vue.createApp({ if (!this.formAddIncludeResponseCode) { return } - const paths = this.formData.lnbits_audit_http_response_codes - if (!paths.includes(this.formAddIncludeResponseCode)) { - paths.push(this.formAddIncludeResponseCode) + const codes = this.formData.lnbits_audit_http_response_codes + if (!codes.includes(this.formAddIncludeResponseCode)) { + this.formData.lnbits_audit_http_response_codes = [ + ...codes, + this.formAddIncludeResponseCode + ] } this.formAddIncludeResponseCode = '' }, diff --git a/lnbits/static/js/audit.js b/lnbits/static/js/audit.js index d66496ce7..0f330f885 100644 --- a/lnbits/static/js/audit.js +++ b/lnbits/static/js/audit.js @@ -10,14 +10,13 @@ window.app = Vue.createApp({ request_type: '', request_method: '', response_code: '', - path: '', - route_path: '' + path: '' }, auditTable: { columns: [ { name: 'created_at', - align: 'left', + align: 'center', label: 'Date', field: 'created_at', sortable: true @@ -29,7 +28,13 @@ window.app = Vue.createApp({ field: 'duration', sortable: true }, - + { + name: 'request_method', + align: 'left', + label: 'Method', + field: 'request_method', + sortable: false + }, { name: 'user_id', align: 'left', @@ -44,21 +49,6 @@ window.app = Vue.createApp({ field: 'ip_address', sortable: false }, - - { - name: 'request_type', - align: 'left', - label: 'Type', - field: 'request_type', - sortable: false - }, - { - name: 'request_method', - align: 'left', - label: 'Method', - field: 'request_method', - sortable: false - }, { name: 'response_code', align: 'left', @@ -72,22 +62,6 @@ window.app = Vue.createApp({ label: 'Path', field: 'path', sortable: false - }, - - { - name: 'route_path', - align: 'left', - label: 'Route Path', - field: 'route_path', - sortable: false - }, - - { - name: 'query_string', - align: 'left', - label: 'Query', - field: 'query_string', - sortable: false } ], pagination: { diff --git a/lnbits/templates/components.vue b/lnbits/templates/components.vue index 783834109..091399618 100644 --- a/lnbits/templates/components.vue +++ b/lnbits/templates/components.vue @@ -205,7 +205,7 @@