001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.http.security.monitoring; 017 018import de.cuioss.http.security.core.UrlSecurityFailureType; 019 020import java.util.Map; 021import java.util.Optional; 022import java.util.concurrent.ConcurrentHashMap; 023import java.util.concurrent.atomic.AtomicLong; 024import java.util.stream.Collectors; 025 026/** 027 * Thread-safe counter for tracking security events by failure type. 028 * 029 * <p>This class provides a centralized mechanism for counting security violations, 030 * enabling monitoring, alerting, and security analytics. It uses atomic operations 031 * and concurrent collections to ensure thread safety without locks.</p> 032 * 033 * <h3>Design Principles</h3> 034 * <ul> 035 * <li><strong>Thread Safety</strong> - All operations are atomic and thread-safe</li> 036 * <li><strong>Lock-Free</strong> - Uses lock-free data structures for performance</li> 037 * <li><strong>Memory Efficient</strong> - Only allocates counters for observed failure types</li> 038 * <li><strong>Non-Blocking</strong> - All operations complete in constant time</li> 039 * </ul> 040 * 041 * <h3>Usage Examples</h3> 042 * <pre> 043 * // Create event counter 044 * SecurityEventCounter counter = new SecurityEventCounter(); 045 * 046 * // Increment counters for different failure types 047 * long pathTraversalCount = counter.increment(UrlSecurityFailureType.PATH_TRAVERSAL_DETECTED); 048 * long encodingCount = counter.increment(UrlSecurityFailureType.DOUBLE_ENCODING); 049 * 050 * // Query current counts 051 * long currentCount = counter.getCount(UrlSecurityFailureType.PATH_TRAVERSAL_DETECTED); 052 * 053 * // Get all counts for reporting 054 * Map<UrlSecurityFailureType, Long> allCounts = counter.getAllCounts(); 055 * 056 * // Reset all counters 057 * counter.reset(); 058 * </pre> 059 * 060 * <h3>Concurrent Access</h3> 061 * <p>This class is designed for high-concurrency environments where multiple threads 062 * may be simultaneously incrementing counters for different or the same failure types. 063 * All operations are atomic and consistent.</p> 064 * 065 * <h3>Memory Characteristics</h3> 066 * <p>Counters are created lazily - only failure types that have been observed will 067 * consume memory. This makes the class efficient even when dealing with the full 068 * range of possible {@link UrlSecurityFailureType} values.</p> 069 * 070 * Implements: Task S1 from HTTP verification specification 071 * 072 * @since 1.0 073 * @see UrlSecurityFailureType 074 */ 075public class SecurityEventCounter { 076 077 private final ConcurrentHashMap<UrlSecurityFailureType, AtomicLong> counters = new ConcurrentHashMap<>(); 078 079 /** 080 * Increments the counter for the specified failure type and returns the new count. 081 * 082 * <p>This operation is atomic and thread-safe. If this is the first time the failure 083 * type has been observed, a new counter will be created and initialized to 1.</p> 084 * 085 * @param failureType The type of security failure to increment. Must not be null. 086 * @return The new count value after incrementing 087 * @throws NullPointerException if failureType is null 088 */ 089 public long increment(UrlSecurityFailureType failureType) { 090 091 return counters.computeIfAbsent(failureType, k -> new AtomicLong(0)) 092 .incrementAndGet(); 093 } 094 095 /** 096 * Increments the counter for the specified failure type by the given delta. 097 * 098 * <p>This operation is atomic and thread-safe. If this is the first time the failure 099 * type has been observed, a new counter will be created and initialized to the delta value.</p> 100 * 101 * @param failureType The type of security failure to increment. Must not be null. 102 * @param delta The amount to add to the counter. Must be positive. 103 * @return The new count value after incrementing 104 * @throws NullPointerException if failureType is null 105 * @throws IllegalArgumentException if delta is negative 106 */ 107 public long incrementBy(UrlSecurityFailureType failureType, long delta) { 108 if (delta < 0) { 109 throw new IllegalArgumentException("delta must be non-negative, got: " + delta); 110 } 111 112 return counters.computeIfAbsent(failureType, k -> new AtomicLong(0)) 113 .addAndGet(delta); 114 } 115 116 /** 117 * Returns the current count for the specified failure type. 118 * 119 * <p>Returns 0 if no events of this type have been recorded. This operation 120 * is thread-safe and returns a consistent snapshot of the counter value.</p> 121 * 122 * @param failureType The failure type to query. Must not be null. 123 * @return The current count for the failure type, or 0 if no events recorded 124 * @throws NullPointerException if failureType is null 125 */ 126 public long getCount(UrlSecurityFailureType failureType) { 127 128 return Optional.ofNullable(counters.get(failureType)) 129 .map(AtomicLong::get) 130 .orElse(0L); 131 } 132 133 /** 134 * Returns a snapshot of all current counts. 135 * 136 * <p>Returns an immutable map containing all observed failure types and their 137 * current counts. Only failure types with non-zero counts are included. 138 * This is useful for reporting and monitoring purposes.</p> 139 * 140 * @return An immutable map of failure types to their current counts 141 */ 142 public Map<UrlSecurityFailureType, Long> getAllCounts() { 143 return counters.entrySet().stream() 144 .collect(Collectors.toUnmodifiableMap( 145 Map.Entry::getKey, 146 entry -> entry.getValue().get() 147 )); 148 } 149 150 /** 151 * Returns the total count across all failure types. 152 * 153 * <p>This method sums all individual counters to provide a total count of 154 * security events. Note that this is a snapshot in time and may not be 155 * consistent across concurrent modifications.</p> 156 * 157 * @return The total count of all security events 158 */ 159 public long getTotalCount() { 160 return counters.values().stream() 161 .mapToLong(AtomicLong::get) 162 .sum(); 163 } 164 165 /** 166 * Returns the number of distinct failure types that have been observed. 167 * 168 * <p>This returns the number of different {@link UrlSecurityFailureType} values 169 * that have had at least one event recorded.</p> 170 * 171 * @return The number of distinct failure types with recorded events 172 */ 173 public int getFailureTypeCount() { 174 return counters.size(); 175 } 176 177 /** 178 * Checks if any events have been recorded for the specified failure type. 179 * 180 * @param failureType The failure type to check. Must not be null. 181 * @return true if at least one event has been recorded for this failure type 182 * @throws NullPointerException if failureType is null 183 */ 184 public boolean hasEvents(UrlSecurityFailureType failureType) { 185 return getCount(failureType) > 0; 186 } 187 188 /** 189 * Checks if any security events have been recorded at all. 190 * 191 * @return true if any security events have been recorded 192 */ 193 public boolean hasAnyEvents() { 194 return !counters.isEmpty() && getTotalCount() > 0; 195 } 196 197 /** 198 * Resets the counter for a specific failure type to zero. 199 * 200 * <p>This atomically sets the counter for the specified failure type to zero. 201 * If no events have been recorded for this failure type, this operation has no effect.</p> 202 * 203 * @param failureType The failure type to reset. Must not be null. 204 * @throws NullPointerException if failureType is null 205 */ 206 public void reset(UrlSecurityFailureType failureType) { 207 208 AtomicLong counter = counters.get(failureType); 209 if (counter != null) { 210 counter.set(0); 211 } 212 } 213 214 /** 215 * Resets all counters to zero. 216 * 217 * <p>This atomically resets all failure type counters to zero. The failure types 218 * remain in the internal map but with zero counts. This is useful for periodic 219 * reporting cycles where you want to start fresh counts.</p> 220 */ 221 public void reset() { 222 counters.values().forEach(counter -> counter.set(0)); 223 } 224 225 /** 226 * Completely clears all counters and removes failure types from tracking. 227 * 228 * <p>This removes all failure types from the internal map, effectively returning 229 * the counter to its initial state. This is more aggressive than {@link #reset()} 230 * as it also frees the memory used by the counter objects.</p> 231 */ 232 public void clear() { 233 counters.clear(); 234 } 235 236 /** 237 * Returns a string representation of the counter state. 238 * 239 * <p>This includes the total count and the number of distinct failure types being tracked. 240 * It does not include detailed counts to avoid exposing potentially sensitive information.</p> 241 * 242 * @return A string representation of the counter state 243 */ 244 @Override 245 public String toString() { 246 return "SecurityEventCounter{totalEvents=%d, distinctFailureTypes=%d}".formatted( 247 getTotalCount(), getFailureTypeCount()); 248 } 249}