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&lt;UrlSecurityFailureType, Long&gt; 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}