extra stuff

This commit is contained in:
Anthony Stirling 2025-06-16 23:03:04 +01:00
parent a5aed57d9f
commit 561003f9af
6 changed files with 464 additions and 167 deletions

View File

@ -8,13 +8,16 @@ import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import stirling.software.proprietary.config.AuditConfigurationProperties; import stirling.software.proprietary.config.AuditConfigurationProperties;
import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.service.AuditService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.IntStream;
/** /**
* Aspect for processing {@link Audited} annotations. * Aspect for processing {@link Audited} annotations.
@ -34,14 +37,23 @@ public class AuditAspect {
Method method = signature.getMethod(); Method method = signature.getMethod();
Audited auditedAnnotation = method.getAnnotation(Audited.class); Audited auditedAnnotation = method.getAnnotation(Audited.class);
// Skip if this audit level is not enabled // Use unified check to determine if we should audit
if (!auditConfig.isLevelEnabled(auditedAnnotation.level())) { if (!AuditUtils.shouldAudit(method, auditConfig)) {
return joinPoint.proceed(); return joinPoint.proceed();
} }
Map<String, Object> auditData = new HashMap<>(); // Use AuditUtils to create the base audit data
auditData.put("className", joinPoint.getTarget().getClass().getName()); Map<String, Object> auditData = AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
auditData.put("methodName", method.getName());
// Add HTTP information if we're in a web context
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest req = attrs.getRequest();
String path = req.getRequestURI();
String httpMethod = req.getMethod();
AuditUtils.addHttpData(auditData, httpMethod, path, auditedAnnotation.level());
AuditUtils.addFileData(auditData, joinPoint, auditedAnnotation.level());
}
// Add arguments if requested and if at VERBOSE level, or if specifically requested // Add arguments if requested and if at VERBOSE level, or if specifically requested
boolean includeArgs = auditedAnnotation.includeArgs() && boolean includeArgs = auditedAnnotation.includeArgs() &&
@ -49,18 +61,11 @@ public class AuditAspect {
auditConfig.getAuditLevel() == AuditLevel.VERBOSE); auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
if (includeArgs) { if (includeArgs) {
Object[] args = joinPoint.getArgs(); AuditUtils.addMethodArguments(auditData, joinPoint, AuditLevel.VERBOSE);
String[] parameterNames = signature.getParameterNames();
if (args != null && parameterNames != null) {
IntStream.range(0, args.length)
.forEach(i -> {
String paramName = i < parameterNames.length ? parameterNames[i] : "arg" + i;
auditData.put("arg_" + paramName, args[i]);
});
}
} }
// Record start time for latency calculation
long startTime = System.currentTimeMillis();
Object result; Object result;
try { try {
// Execute the method // Execute the method
@ -88,17 +93,36 @@ public class AuditAspect {
// Re-throw the exception // Re-throw the exception
throw ex; throw ex;
} finally { } finally {
// Create the audit entry with the specified level // Add timing information - use isHttpRequest=false to ensure we get timing for non-HTTP methods
// Determine which type of event identifier to use (enum or string) HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
AuditEventType eventType = auditedAnnotation.type(); boolean isHttpRequest = attrs != null;
String typeString = auditedAnnotation.typeString(); AuditUtils.addTimingData(auditData, startTime, resp, auditedAnnotation.level(), isHttpRequest);
if (eventType != AuditEventType.HTTP_REQUEST || !StringUtils.isNotEmpty(typeString)) { // Resolve the event type based on annotation and context
// Use the enum type (preferred) String httpMethod = null;
auditService.audit(eventType, auditData, auditedAnnotation.level()); String path = null;
} else { if (attrs != null) {
HttpServletRequest req = attrs.getRequest();
httpMethod = req.getMethod();
path = req.getRequestURI();
}
AuditEventType eventType = AuditUtils.resolveEventType(
method,
joinPoint.getTarget().getClass(),
path,
httpMethod,
auditedAnnotation
);
// Check if we should use string type instead
String typeString = auditedAnnotation.typeString();
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
// Use the string type (for backward compatibility) // Use the string type (for backward compatibility)
auditService.audit(typeString, auditData, auditedAnnotation.level()); auditService.audit(typeString, auditData, auditedAnnotation.level());
} else {
// Use the enum type (preferred)
auditService.audit(eventType, auditData, auditedAnnotation.level());
} }
} }
} }

View File

@ -0,0 +1,318 @@
package stirling.software.proprietary.audit;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.util.RequestUriUtils;
import stirling.software.proprietary.config.AuditConfigurationProperties;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Shared utilities for audit aspects to ensure consistent behavior
* across different audit mechanisms.
*/
@Slf4j
public class AuditUtils {
/**
* Create a standard audit data map with common attributes based on the current audit level
*
* @param joinPoint The AspectJ join point
* @param auditLevel The current audit level
* @return A map with standard audit data
*/
public static Map<String, Object> createBaseAuditData(ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
Map<String, Object> data = new HashMap<>();
// Common data for all levels
data.put("timestamp", Instant.now().toString());
// Add principal if available
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getName() != null) {
data.put("principal", auth.getName());
} else {
data.put("principal", "system");
}
// Add class name and method name only at VERBOSE level
if (auditLevel.includes(AuditLevel.VERBOSE)) {
data.put("className", joinPoint.getTarget().getClass().getName());
data.put("methodName", ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
}
return data;
}
/**
* Add HTTP-specific information to the audit data if available
*
* @param data The existing audit data map
* @param httpMethod The HTTP method (GET, POST, etc.)
* @param path The request path
* @param auditLevel The current audit level
*/
public static void addHttpData(Map<String, Object> data, String httpMethod, String path, AuditLevel auditLevel) {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return;
}
HttpServletRequest req = attrs.getRequest();
HttpServletResponse resp = attrs.getResponse();
// BASIC level HTTP data
data.put("httpMethod", httpMethod);
data.put("path", path);
// STANDARD level HTTP data
if (auditLevel.includes(AuditLevel.STANDARD)) {
data.put("clientIp", req.getRemoteAddr());
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
data.put("requestId", MDC.get("requestId"));
// Form data for POST/PUT/PATCH
if (("POST".equalsIgnoreCase(httpMethod) ||
"PUT".equalsIgnoreCase(httpMethod) ||
"PATCH".equalsIgnoreCase(httpMethod)) && req.getContentType() != null) {
String contentType = req.getContentType();
if (contentType.contains("application/x-www-form-urlencoded") ||
contentType.contains("multipart/form-data")) {
Map<String, String[]> params = new HashMap<>(req.getParameterMap());
// Remove CSRF token from logged parameters
params.remove("_csrf");
if (!params.isEmpty()) {
data.put("formParams", params);
}
}
}
}
}
/**
* Add file information to the audit data if available
*
* @param data The existing audit data map
* @param joinPoint The AspectJ join point
* @param auditLevel The current audit level
*/
public static void addFileData(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
if (auditLevel.includes(AuditLevel.STANDARD)) {
List<MultipartFile> files = Arrays.stream(joinPoint.getArgs())
.filter(a -> a instanceof MultipartFile)
.map(a -> (MultipartFile)a)
.collect(Collectors.toList());
if (!files.isEmpty()) {
List<Map<String,Object>> fileInfos = files.stream().map(f -> {
Map<String,Object> m = new HashMap<>();
m.put("name", f.getOriginalFilename());
m.put("size", f.getSize());
m.put("type", f.getContentType());
return m;
}).collect(Collectors.toList());
data.put("files", fileInfos);
}
}
}
/**
* Add method arguments to the audit data
*
* @param data The existing audit data map
* @param joinPoint The AspectJ join point
* @param auditLevel The current audit level
*/
public static void addMethodArguments(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
if (auditLevel.includes(AuditLevel.VERBOSE)) {
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
String[] names = sig.getParameterNames();
Object[] vals = joinPoint.getArgs();
if (names != null && vals != null) {
IntStream.range(0, names.length)
.forEach(i -> data.put("arg_" + names[i], vals[i]));
}
}
}
/**
* Determine if a method should be audited based on config and annotation
*
* @param method The method to check
* @param auditConfig The audit configuration
* @return true if the method should be audited
*/
public static boolean shouldAudit(Method method, AuditConfigurationProperties auditConfig) {
// First check if audit is globally enabled
if (!auditConfig.isEnabled()) {
return false;
}
// Check for annotation override
Audited auditedAnnotation = method.getAnnotation(Audited.class);
if (auditedAnnotation != null) {
// Method has @Audited - check if the specific level is enabled
return auditConfig.isLevelEnabled(auditedAnnotation.level());
}
// No annotation - use global level for controllers
return auditConfig.isLevelEnabled(AuditLevel.BASIC);
}
/**
* Add timing and response status data to the audit record
*
* @param data The audit data to add to
* @param startTime The start time in milliseconds
* @param response The HTTP response (may be null for non-HTTP methods)
* @param level The current audit level
* @param isHttpRequest Whether this is an HTTP request (controller) or a regular method call
*/
public static void addTimingData(Map<String, Object> data, long startTime, HttpServletResponse response, AuditLevel level, boolean isHttpRequest) {
if (level.includes(AuditLevel.STANDARD)) {
// For HTTP requests, let ControllerAuditAspect handle timing separately
// For non-HTTP methods, add execution time here
if (!isHttpRequest) {
data.put("latencyMs", System.currentTimeMillis() - startTime);
}
// Add HTTP status code if available
if (response != null) {
data.put("statusCode", response.getStatus());
}
}
}
/**
* Resolve the event type to use for auditing, considering annotations and context
*
* @param method The method being audited
* @param controller The controller class
* @param path The request path (may be null for non-HTTP methods)
* @param httpMethod The HTTP method (may be null for non-HTTP methods)
* @param annotation The @Audited annotation (may be null)
* @return The resolved event type (never null)
*/
public static AuditEventType resolveEventType(Method method, Class<?> controller, String path, String httpMethod, Audited annotation) {
// First check if we have an explicit annotation
if (annotation != null && annotation.type() != AuditEventType.HTTP_REQUEST) {
return annotation.type();
}
// For HTTP methods, infer based on controller and path
if (httpMethod != null) {
String cls = controller.getSimpleName().toLowerCase();
String pkg = controller.getPackage().getName().toLowerCase();
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|| path.startsWith("/user") || path.startsWith("/login")) {
return AuditEventType.USER_PROFILE_UPDATE;
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
return AuditEventType.SETTINGS_CHANGED;
} else if (cls.contains("file") || path.startsWith("/file")
|| path.matches("(?i).*/(upload|download)/.*")) {
return AuditEventType.FILE_OPERATION;
}
}
// Default for non-HTTP methods or when no specific match
return AuditEventType.PDF_PROCESS;
}
/**
* Determine the appropriate audit level to use
*
* @param method The method to check
* @param defaultLevel The default level to use if no annotation present
* @param auditConfig The audit configuration
* @return The audit level to use
*/
public static AuditLevel getEffectiveAuditLevel(Method method, AuditLevel defaultLevel, AuditConfigurationProperties auditConfig) {
Audited auditedAnnotation = method.getAnnotation(Audited.class);
if (auditedAnnotation != null) {
// Method has @Audited - use its level
return auditedAnnotation.level();
}
// Use default level (typically from global config)
return defaultLevel;
}
/**
* Determine the appropriate audit event type to use
*
* @param method The method being audited
* @param controller The controller class
* @param path The request path
* @param httpMethod The HTTP method
* @return The determined audit event type
*/
public static AuditEventType determineAuditEventType(Method method, Class<?> controller, String path, String httpMethod) {
// First check for explicit annotation
Audited auditedAnnotation = method.getAnnotation(Audited.class);
if (auditedAnnotation != null) {
return auditedAnnotation.type();
}
// Otherwise infer from controller and path
String cls = controller.getSimpleName().toLowerCase();
String pkg = controller.getPackage().getName().toLowerCase();
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|| path.startsWith("/user") || path.startsWith("/login")) {
return AuditEventType.USER_PROFILE_UPDATE;
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
return AuditEventType.SETTINGS_CHANGED;
} else if (cls.contains("file") || path.startsWith("/file")
|| path.matches("(?i).*/(upload|download)/.*")) {
return AuditEventType.FILE_OPERATION;
} else {
return AuditEventType.PDF_PROCESS;
}
}
/**
* Get the current HTTP request if available
*
* @return The current request or null if not in a request context
*/
public static HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attrs != null ? attrs.getRequest() : null;
}
/**
* Check if a GET request is for a static resource
*
* @param request The HTTP request
* @return true if this is a static resource request
*/
public static boolean isStaticResourceRequest(HttpServletRequest request) {
return request != null && !RequestUriUtils.isTrackableResource(
request.getContextPath(), request.getRequestURI());
}
}

View File

@ -2,12 +2,11 @@ package stirling.software.proprietary.audit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -17,23 +16,16 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.common.util.RequestUriUtils;
import stirling.software.proprietary.config.AuditConfigurationProperties; import stirling.software.proprietary.config.AuditConfigurationProperties;
import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.service.AuditService;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/** /**
* Aspect for automatically auditing controller methods with web mappings * Aspect for automatically auditing controller methods with web mappings
@ -51,10 +43,7 @@ public class ControllerAuditAspect {
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))") @Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable { public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
log.info("HELLOOOOOOOOOOOOOOOO");
return auditController(jp, "GET"); return auditController(jp, "GET");
} }
/** /**
* Intercept all methods with GetMapping annotation * Intercept all methods with GetMapping annotation
@ -99,23 +88,28 @@ public class ControllerAuditAspect {
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable { private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
MethodSignature sig = (MethodSignature) joinPoint.getSignature(); MethodSignature sig = (MethodSignature) joinPoint.getSignature();
Method method = sig.getMethod(); Method method = sig.getMethod();
AuditLevel level = auditConfig.getAuditLevel();
// OFF below BASIC? // Use unified check to determine if we should audit
if (!auditConfig.isLevelEnabled(AuditLevel.BASIC)) { if (!AuditUtils.shouldAudit(method, auditConfig)) {
return joinPoint.proceed(); return joinPoint.proceed();
} }
// // Opt-out // Check if method is explicitly annotated with @Audited
// if (method.isAnnotationPresent(Audited.class)) { Audited auditedAnnotation = method.getAnnotation(Audited.class);
// return joinPoint.proceed(); AuditLevel level = auditConfig.getAuditLevel();
// }
// If @Audited annotation is present, respect its level setting
if (auditedAnnotation != null) {
// Use the level from annotation if it's stricter than global level
level = auditedAnnotation.level();
}
String path = getRequestPath(method, httpMethod); String path = getRequestPath(method, httpMethod);
// Skip static GET resources // Skip static GET resources
if ("GET".equals(httpMethod)) { if ("GET".equals(httpMethod)) {
HttpServletRequest maybe = getCurrentRequest(); HttpServletRequest maybe = AuditUtils.getCurrentRequest();
if (maybe != null && !RequestUriUtils.isTrackableResource(maybe.getContextPath(), maybe.getRequestURI())) { if (maybe != null && AuditUtils.isStaticResourceRequest(maybe)) {
return joinPoint.proceed(); return joinPoint.proceed();
} }
} }
@ -125,64 +119,19 @@ public class ControllerAuditAspect {
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null; HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Map<String, Object> data = new HashMap<>();
// BASIC // Use AuditUtils to create the base audit data
if (level.includes(AuditLevel.BASIC)) { Map<String, Object> data = AuditUtils.createBaseAuditData(joinPoint, level);
data.put("timestamp", Instant.now().toString());
data.put("principal", SecurityContextHolder.getContext().getAuthentication().getName());
data.put("path", path);
data.put("httpMethod", httpMethod);
}
// STANDARD // Add HTTP-specific information
if (level.includes(AuditLevel.STANDARD) && req != null) { AuditUtils.addHttpData(data, httpMethod, path, level);
data.put("clientIp", req.getRemoteAddr());
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
data.put("requestId", MDC.get("requestId"));
if ("POST".equalsIgnoreCase(httpMethod) // Add file information if present
|| "PUT".equalsIgnoreCase(httpMethod) AuditUtils.addFileData(data, joinPoint, level);
|| "PATCH".equalsIgnoreCase(httpMethod)) {
String ct = req.getContentType();
if (ct != null && (
ct.contains("application/x-www-form-urlencoded") ||
ct.contains("multipart/form-data")
)) {
Map<String,String[]> params = req.getParameterMap();
if (!params.isEmpty()) {
data.put("formParams", params);
}
}
List<MultipartFile> files = Arrays.stream(joinPoint.getArgs()) // Add method arguments if at VERBOSE level
.filter(a -> a instanceof MultipartFile)
.map(a -> (MultipartFile)a)
.collect(Collectors.toList());
if (!files.isEmpty()) {
List<Map<String,Object>> fileInfos = files.stream().map(f -> {
Map<String,Object> m = new HashMap<>();
m.put("name", f.getOriginalFilename());
m.put("size", f.getSize());
m.put("type", f.getContentType());
return m;
}).collect(Collectors.toList());
data.put("files", fileInfos);
}
}
}
// VERBOSE args
if (level.includes(AuditLevel.VERBOSE)) { if (level.includes(AuditLevel.VERBOSE)) {
String[] names = sig.getParameterNames(); AuditUtils.addMethodArguments(data, joinPoint, level);
Object[] vals = joinPoint.getArgs();
if (names != null && vals != null) {
IntStream.range(0, names.length).forEach(i -> data.put("arg_" + names[i], vals[i]));
}
} }
Object result = null; Object result = null;
@ -195,37 +144,45 @@ public class ControllerAuditAspect {
data.put("errorMessage", ex.getMessage()); data.put("errorMessage", ex.getMessage());
throw ex; throw ex;
} finally { } finally {
// finalize STANDARD // Handle timing directly for HTTP requests
if (level.includes(AuditLevel.STANDARD)) { if (level.includes(AuditLevel.STANDARD)) {
data.put("latencyMs", System.currentTimeMillis() - start); data.put("latencyMs", System.currentTimeMillis() - start);
if (resp != null) data.put("statusCode", resp.getStatus()); if (resp != null) data.put("statusCode", resp.getStatus());
} }
// finalize VERBOSE result
// Call AuditUtils but with isHttpRequest=true to skip additional timing
AuditUtils.addTimingData(data, start, resp, level, true);
// Add result for VERBOSE level
if (level.includes(AuditLevel.VERBOSE) && result != null) { if (level.includes(AuditLevel.VERBOSE) && result != null) {
data.put("result", result.toString()); data.put("result", result.toString());
} }
AuditEventType type = determineAuditEventType(joinPoint.getTarget().getClass(), path, httpMethod);
auditService.audit(type, data, level); // Resolve the event type using the unified method
AuditEventType eventType = AuditUtils.resolveEventType(
method,
joinPoint.getTarget().getClass(),
path,
httpMethod,
auditedAnnotation
);
// Check if we should use string type instead (for backward compatibility)
if (auditedAnnotation != null) {
String typeString = auditedAnnotation.typeString();
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
auditService.audit(typeString, data, level);
return result;
}
}
// Use the enum type
auditService.audit(eventType, data, level);
} }
return result; return result;
} }
private AuditEventType determineAuditEventType(Class<?> controller, String path, String httpMethod) { // Using AuditUtils.determineAuditEventType instead
String cls = controller.getSimpleName().toLowerCase();
String pkg = controller.getPackage().getName().toLowerCase();
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|| path.startsWith("/user") || path.startsWith("/login")) {
return AuditEventType.USER_PROFILE_UPDATE;
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
return AuditEventType.SETTINGS_CHANGED;
} else if (cls.contains("file") || path.startsWith("/file")
|| path.matches("(?i).*/(upload|download)/.*")) {
return AuditEventType.FILE_OPERATION;
} else {
return AuditEventType.PDF_PROCESS;
}
}
private String getRequestPath(Method method, String httpMethod) { private String getRequestPath(Method method, String httpMethod) {
String base = ""; String base = "";
@ -248,8 +205,5 @@ public class ControllerAuditAspect {
return base + mp; return base + mp;
} }
private HttpServletRequest getCurrentRequest() { // Using AuditUtils.getCurrentRequest instead
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return a != null ? a.getRequest() : null;
}
} }

View File

@ -3,8 +3,6 @@ package stirling.software.proprietary.config;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -24,19 +22,16 @@ public class AuditConfigurationProperties {
private final boolean enabled; private final boolean enabled;
private final int level; private final int level;
private final int retentionDays; private final int retentionDays;
private final String licenseType;
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
public AuditConfigurationProperties(ApplicationProperties applicationProperties, @Qualifier("license") String licenseType) {
ApplicationProperties.Premium.ProFeatures.Audit auditConfig = ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
applicationProperties.getPremium().getProFeatures().getAudit(); applicationProperties.getPremium().getProFeatures().getAudit();
// Read values directly from configuration
this.enabled = auditConfig.isEnabled(); this.enabled = auditConfig.isEnabled();
this.level = auditConfig.getLevel(); this.level = auditConfig.getLevel();
this.retentionDays = auditConfig.getRetentionDays(); this.retentionDays = auditConfig.getRetentionDays();
this.licenseType = licenseType;
log.info("Initialized audit configuration: enabled={}, level={}, retentionDays={}", log.debug("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
this.enabled, this.level, this.retentionDays); this.enabled, this.level, this.retentionDays);
} }

View File

@ -81,8 +81,8 @@ public class AuditDashboardController {
@GetMapping("/data") @GetMapping("/data")
@ResponseBody @ResponseBody
public Map<String, Object> getAuditData( public Map<String, Object> getAuditData(
@RequestParam(value = "page", defaultValue = "0") Long page, @RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") Long size, @RequestParam(value = "size", defaultValue = "30") int size,
@RequestParam(value = "type", required = false) String type, @RequestParam(value = "type", required = false) String type,
@RequestParam(value = "principal", required = false) String principal, @RequestParam(value = "principal", required = false) String principal,
@RequestParam(value = "startDate", required = false) @RequestParam(value = "startDate", required = false)
@ -90,12 +90,11 @@ public class AuditDashboardController {
@RequestParam(value = "endDate", required = false) @RequestParam(value = "endDate", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
log.info("Raw query string: {}", request.getQueryString());
Pageable pageable = PageRequest.of(page.intValue(), size.intValue(), Sort.by("timestamp").descending()); Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
Page<PersistentAuditEvent> events; Page<PersistentAuditEvent> events;
String mode = "unknown"; String mode;
if (type != null && principal != null && startDate != null && endDate != null) { if (type != null && principal != null && startDate != null && endDate != null) {
mode = "principal + type + startDate + endDate"; mode = "principal + type + startDate + endDate";
@ -133,13 +132,6 @@ public class AuditDashboardController {
// Logging // Logging
List<PersistentAuditEvent> content = events.getContent(); List<PersistentAuditEvent> content = events.getContent();
Long firstId = content.isEmpty() ? null : content.get(0).getId();
Long lastId = content.isEmpty() ? null : content.get(content.size() - 1).getId();
log.info("Audit request: page={} size={} mode='{}' → result page={} totalElements={} totalPages={} contentSize={}",
page, size, mode, events.getNumber(), events.getTotalElements(), events.getTotalPages(), content.size());
log.info("Audit content ID range: firstId={} lastId={} (descending timestamp)", firstId, lastId);
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("content", content); response.put("content", content);

View File

@ -6,7 +6,14 @@ import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
/** Redacts any map values whose keys match common secret/token patterns. */ /** Redacts any map values whose keys match common secret/token patterns. */
@Slf4j
public final class SecretMasker { public final class SecretMasker {
private static final Pattern SENSITIVE = private static final Pattern SENSITIVE =
@ -14,21 +21,34 @@ public final class SecretMasker {
private SecretMasker() {} private SecretMasker() {}
public static Object deepMask(Object value) { public static Map<String,Object> mask(Map<String,Object> in) {
if (in == null) return null;
return in.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> deepMaskValue(e.getKey(), e.getValue())
));
}
private static Object deepMask(Object value) {
if (value instanceof Map<?,?> m) { if (value instanceof Map<?,?> m) {
return m.entrySet().stream().collect(Collectors.toMap( return m.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getKey,
e -> deepMaskValue((String)e.getKey(), e.getValue()) e -> deepMaskValue((String)e.getKey(), e.getValue())
)); ));
} else if (value instanceof List<?> list) { } else if (value instanceof List<?> list) {
return list.stream() return list.stream()
.map(SecretMasker::deepMask) .map(SecretMasker::deepMask).toList();
.collect(Collectors.toList());
} else { } else {
return value; return value;
} }
} }
private static Object deepMaskValue(String key, Object value) { private static Object deepMaskValue(String key, Object value) {
if (key != null && SENSITIVE.matcher(key).find()) { if (key != null && SENSITIVE.matcher(key).find()) {
return "***REDACTED***"; return "***REDACTED***";
@ -36,12 +56,6 @@ public final class SecretMasker {
return deepMask(value); return deepMask(value);
} }
public static Map<String,Object> mask(Map<String,Object> in) {
if (in == null) return null;
return in.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> deepMaskValue(e.getKey(), e.getValue())
));
}
} }