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.config;
017
018import org.jspecify.annotations.Nullable;
019
020import java.util.Objects;
021import java.util.Set;
022import java.util.stream.Collectors;
023
024/**
025 * Immutable class representing comprehensive security configuration for HTTP validation.
026 *
027 * <p>This class encapsulates all security policies and settings needed to configure
028 * HTTP security validators. It provides a type-safe, immutable configuration object
029 * that can be shared across multiple validation operations.</p>
030 *
031 * <h3>Design Principles</h3>
032 * <ul>
033 *   <li><strong>Immutability</strong> - Configuration cannot be modified once created</li>
034 *   <li><strong>Type Safety</strong> - Strongly typed configuration parameters</li>
035 *   <li><strong>Completeness</strong> - Covers all aspects of HTTP security validation</li>
036 *   <li><strong>Composability</strong> - Easy to combine with builder patterns</li>
037 *   <li><strong>Performance</strong> - Pre-processes sets for O(1) case-insensitive lookups</li>
038 * </ul>
039 *
040 * <h3>Configuration Categories</h3>
041 * <ul>
042 *   <li><strong>Path Security</strong> - Path traversal prevention, allowed patterns</li>
043 *   <li><strong>Parameter Security</strong> - Query parameter validation rules</li>
044 *   <li><strong>Header Security</strong> - HTTP header validation policies</li>
045 *   <li><strong>Cookie Security</strong> - Cookie validation and security requirements</li>
046 *   <li><strong>Body Security</strong> - Request/response body validation settings</li>
047 *   <li><strong>Encoding Security</strong> - URL encoding and character validation</li>
048 *   <li><strong>Length Limits</strong> - Size restrictions for various HTTP components</li>
049 *   <li><strong>General Policies</strong> - Cross-cutting security concerns</li>
050 * </ul>
051 *
052 * <h3>Usage Examples</h3>
053 * <pre>
054 * // Create with builder
055 * SecurityConfiguration config = SecurityConfiguration.builder()
056 *     .maxPathLength(2048)
057 *     .allowPathTraversal(false)
058 *     .maxParameterCount(100)
059 *     .requireSecureCookies(true)
060 *     .build();
061 *
062 * // Use in validation
063 * PathValidator validator = new PathValidator(config);
064 * validator.validate("/api/users/123");
065 *
066 * // Create restrictive configuration
067 * SecurityConfiguration strict = SecurityConfiguration.strict();
068 *
069 * // Create permissive configuration
070 * SecurityConfiguration lenient = SecurityConfiguration.lenient();
071 * </pre>
072 *
073 * Implements: Task C1 from HTTP verification specification
074 *
075 * @since 1.0
076 * @see SecurityConfigurationBuilder
077 */
078@SuppressWarnings("javaarchitecture:S7027")
079public final class SecurityConfiguration {
080
081    // Path Security
082    private final int maxPathLength;
083    private final boolean allowPathTraversal;
084    private final boolean allowDoubleEncoding;
085
086    // Parameter Security
087    private final int maxParameterCount;
088    private final int maxParameterNameLength;
089    private final int maxParameterValueLength;
090
091    // Header Security
092    private final int maxHeaderCount;
093    private final int maxHeaderNameLength;
094    private final int maxHeaderValueLength;
095    private final @Nullable Set<String> allowedHeaderNames;
096    private final Set<String> blockedHeaderNames;
097
098    // Cookie Security
099    private final int maxCookieCount;
100    private final int maxCookieNameLength;
101    private final int maxCookieValueLength;
102    private final boolean requireSecureCookies;
103    private final boolean requireHttpOnlyCookies;
104
105    // Body Security
106    private final long maxBodySize;
107    private final @Nullable Set<String> allowedContentTypes;
108    private final Set<String> blockedContentTypes;
109
110    // Encoding Security
111    private final boolean allowNullBytes;
112    private final boolean allowControlCharacters;
113    private final boolean allowExtendedAscii;
114    private final boolean normalizeUnicode;
115
116    // General Policies
117    private final boolean caseSensitiveComparison;
118    private final boolean failOnSuspiciousPatterns;
119    private final boolean logSecurityViolations;
120
121    // Pre-processed lowercase sets for O(1) case-insensitive lookups
122    // Only populated when caseSensitiveComparison is false
123    private final @Nullable Set<String> allowedHeaderNamesLowercase;
124    private final Set<String> blockedHeaderNamesLowercase;
125    private final @Nullable Set<String> allowedContentTypesLowercase;
126    private final Set<String> blockedContentTypesLowercase;
127
128    /**
129     * Creates a SecurityConfiguration with validation of constraints.
130     */
131    @SuppressWarnings({"java:S107", "java:S3776"}) // S107: Constructor has many parameters by design - using Builder pattern for construction
132                                                      // S3776: Cognitive complexity is from necessary validation in security-critical code
133    SecurityConfiguration(
134            int maxPathLength,
135            boolean allowPathTraversal,
136            boolean allowDoubleEncoding,
137            int maxParameterCount,
138            int maxParameterNameLength,
139            int maxParameterValueLength,
140            int maxHeaderCount,
141            int maxHeaderNameLength,
142            int maxHeaderValueLength,
143            @Nullable Set<String> allowedHeaderNames,
144            Set<String> blockedHeaderNames,
145            int maxCookieCount,
146            int maxCookieNameLength,
147            int maxCookieValueLength,
148            boolean requireSecureCookies,
149            boolean requireHttpOnlyCookies,
150            long maxBodySize,
151            @Nullable Set<String> allowedContentTypes,
152            Set<String> blockedContentTypes,
153            boolean allowNullBytes,
154            boolean allowControlCharacters,
155            boolean allowExtendedAscii,
156            boolean normalizeUnicode,
157            boolean caseSensitiveComparison,
158            boolean failOnSuspiciousPatterns,
159            boolean logSecurityViolations) {
160
161        // Validate length limits are positive
162        if (maxPathLength <= 0) {
163            throw new IllegalArgumentException("maxPathLength must be positive, got: " + maxPathLength);
164        }
165        if (maxParameterCount < 0) {
166            throw new IllegalArgumentException("maxParameterCount must be non-negative, got: " + maxParameterCount);
167        }
168        if (maxParameterNameLength <= 0) {
169            throw new IllegalArgumentException("maxParameterNameLength must be positive, got: " + maxParameterNameLength);
170        }
171        if (maxParameterValueLength <= 0) {
172            throw new IllegalArgumentException("maxParameterValueLength must be positive, got: " + maxParameterValueLength);
173        }
174        if (maxHeaderCount < 0) {
175            throw new IllegalArgumentException("maxHeaderCount must be non-negative, got: " + maxHeaderCount);
176        }
177        if (maxHeaderNameLength <= 0) {
178            throw new IllegalArgumentException("maxHeaderNameLength must be positive, got: " + maxHeaderNameLength);
179        }
180        if (maxHeaderValueLength <= 0) {
181            throw new IllegalArgumentException("maxHeaderValueLength must be positive, got: " + maxHeaderValueLength);
182        }
183        if (maxCookieCount < 0) {
184            throw new IllegalArgumentException("maxCookieCount must be non-negative, got: " + maxCookieCount);
185        }
186        if (maxCookieNameLength <= 0) {
187            throw new IllegalArgumentException("maxCookieNameLength must be positive, got: " + maxCookieNameLength);
188        }
189        if (maxCookieValueLength <= 0) {
190            throw new IllegalArgumentException("maxCookieValueLength must be positive, got: " + maxCookieValueLength);
191        }
192        if (maxBodySize < 0) {
193            throw new IllegalArgumentException("maxBodySize must be non-negative, got: " + maxBodySize);
194        }
195
196        // Assign final fields
197        this.maxPathLength = maxPathLength;
198        this.allowPathTraversal = allowPathTraversal;
199        this.allowDoubleEncoding = allowDoubleEncoding;
200        this.maxParameterCount = maxParameterCount;
201        this.maxParameterNameLength = maxParameterNameLength;
202        this.maxParameterValueLength = maxParameterValueLength;
203        this.maxHeaderCount = maxHeaderCount;
204        this.maxHeaderNameLength = maxHeaderNameLength;
205        this.maxHeaderValueLength = maxHeaderValueLength;
206        this.maxCookieCount = maxCookieCount;
207        this.maxCookieNameLength = maxCookieNameLength;
208        this.maxCookieValueLength = maxCookieValueLength;
209        this.requireSecureCookies = requireSecureCookies;
210        this.requireHttpOnlyCookies = requireHttpOnlyCookies;
211        this.maxBodySize = maxBodySize;
212        this.allowNullBytes = allowNullBytes;
213        this.allowControlCharacters = allowControlCharacters;
214        this.allowExtendedAscii = allowExtendedAscii;
215        this.normalizeUnicode = normalizeUnicode;
216        this.caseSensitiveComparison = caseSensitiveComparison;
217        this.failOnSuspiciousPatterns = failOnSuspiciousPatterns;
218        this.logSecurityViolations = logSecurityViolations;
219
220        // Ensure sets are immutable and non-null
221        this.allowedHeaderNames = allowedHeaderNames != null ?
222                Set.copyOf(allowedHeaderNames) : null;
223        this.blockedHeaderNames = Set.copyOf(blockedHeaderNames);
224        this.allowedContentTypes = allowedContentTypes != null ?
225                Set.copyOf(allowedContentTypes) : null;
226        this.blockedContentTypes = Set.copyOf(blockedContentTypes);
227
228        // Pre-process sets for case-insensitive comparison if needed
229        // This optimization changes lookups from O(n) to O(1) average case
230        if (!caseSensitiveComparison) {
231            // Convert all strings to lowercase for efficient case-insensitive lookups
232            this.allowedHeaderNamesLowercase = this.allowedHeaderNames != null ?
233                    this.allowedHeaderNames.stream()
234                            .map(String::toLowerCase)
235                            .collect(Collectors.toUnmodifiableSet()) : null;
236            this.blockedHeaderNamesLowercase = this.blockedHeaderNames.stream()
237                    .map(String::toLowerCase)
238                    .collect(Collectors.toUnmodifiableSet());
239            this.allowedContentTypesLowercase = this.allowedContentTypes != null ?
240                    this.allowedContentTypes.stream()
241                            .map(String::toLowerCase)
242                            .collect(Collectors.toUnmodifiableSet()) : null;
243            this.blockedContentTypesLowercase = this.blockedContentTypes.stream()
244                    .map(String::toLowerCase)
245                    .collect(Collectors.toUnmodifiableSet());
246        } else {
247            // Not needed for case-sensitive comparison
248            this.allowedHeaderNamesLowercase = null;
249            this.blockedHeaderNamesLowercase = Set.of();
250            this.allowedContentTypesLowercase = null;
251            this.blockedContentTypesLowercase = Set.of();
252        }
253    }
254
255    /**
256     * Creates a builder for constructing SecurityConfiguration instances.
257     *
258     * @return A new SecurityConfigurationBuilder with default values
259     */
260    public static SecurityConfigurationBuilder builder() {
261        return new SecurityConfigurationBuilder();
262    }
263
264    /**
265     * Creates a strict security configuration with tight restrictions.
266     * This configuration prioritizes security over compatibility.
267     *
268     * @return A SecurityConfiguration with strict security policies
269     */
270    public static SecurityConfiguration strict() {
271        return builder()
272                // Path Security - very restrictive
273                .maxPathLength(1024)
274                .allowPathTraversal(false)
275                .allowDoubleEncoding(false)
276
277                // Parameter Security - strict limits
278                .maxParameterCount(50)
279                .maxParameterNameLength(100)
280                .maxParameterValueLength(1024)
281
282                // Header Security - conservative limits
283                .maxHeaderCount(50)
284                .maxHeaderNameLength(100)
285                .maxHeaderValueLength(4096)
286
287                // Cookie Security - require all security flags
288                .maxCookieCount(20)
289                .maxCookieNameLength(100)
290                .maxCookieValueLength(4096)
291                .requireSecureCookies(true)
292                .requireHttpOnlyCookies(true)
293
294                // Body Security - reasonable limit
295                .maxBodySize(10_485_760) // 10MB
296
297                // Encoding Security - no dangerous characters
298                .allowNullBytes(false)
299                .allowControlCharacters(false)
300                .allowExtendedAscii(false)
301                .normalizeUnicode(true)
302
303                // General Policies - maximum security
304                .caseSensitiveComparison(true)
305                .failOnSuspiciousPatterns(true)
306                .logSecurityViolations(true)
307
308                .build();
309    }
310
311    /**
312     * Creates a lenient security configuration for maximum compatibility.
313     * This configuration should only be used in trusted environments.
314     *
315     * @return A SecurityConfiguration with permissive policies
316     */
317    public static SecurityConfiguration lenient() {
318        return builder()
319                // Path Security - permissive
320                .maxPathLength(8192)
321                .allowPathTraversal(true) // WARNING: Security risk
322                .allowDoubleEncoding(true)
323
324                // Parameter Security - generous limits
325                .maxParameterCount(1000)
326                .maxParameterNameLength(512)
327                .maxParameterValueLength(8192)
328
329                // Header Security - large limits
330                .maxHeaderCount(200)
331                .maxHeaderNameLength(512)
332                .maxHeaderValueLength(16384)
333
334                // Cookie Security - no requirements
335                .maxCookieCount(100)
336                .maxCookieNameLength(512)
337                .maxCookieValueLength(8192)
338                .requireSecureCookies(false)
339                .requireHttpOnlyCookies(false)
340
341                // Body Security - large limit
342                .maxBodySize(104_857_600) // 100MB
343
344                // Encoding Security - allow everything
345                .allowNullBytes(true)
346                .allowControlCharacters(true)
347                .allowExtendedAscii(true)
348                .normalizeUnicode(false)
349
350                // General Policies - minimal security
351                .caseSensitiveComparison(false)
352                .failOnSuspiciousPatterns(false)
353                .logSecurityViolations(false)
354
355                .build();
356    }
357
358    /**
359     * Creates a security configuration with default balanced settings.
360     *
361     * @return A SecurityConfiguration with default security policies
362     */
363    public static SecurityConfiguration defaults() {
364        return builder().build();
365    }
366
367    /**
368     * Checks if the configuration allows a specific header name.
369     *
370     * @param headerName The header name to check (null returns false)
371     * @return true if the header is allowed, false if blocked or null
372     */
373    public boolean isHeaderAllowed(@Nullable String headerName) {
374        return isAllowed(headerName, allowedHeaderNames, blockedHeaderNames,
375                allowedHeaderNamesLowercase, blockedHeaderNamesLowercase);
376    }
377
378    /**
379     * Checks if the configuration allows a specific content type.
380     *
381     * @param contentType The content type to check (null returns false)
382     * @return true if the content type is allowed, false if blocked or null
383     */
384    public boolean isContentTypeAllowed(@Nullable String contentType) {
385        return isAllowed(contentType, allowedContentTypes, blockedContentTypes,
386                allowedContentTypesLowercase, blockedContentTypesLowercase);
387    }
388
389    /**
390     * Optimized helper method to check if a value is allowed based on allow and block lists.
391     * For case-insensitive comparison, uses pre-processed lowercase sets for O(1) lookups
392     * instead of O(n) stream operations.
393     *
394     * @param value The value to check (null returns false)
395     * @param allowedSet The set of allowed values (null means all allowed)
396     * @param blockedSet The set of blocked values
397     * @param allowedSetLowercase Pre-processed lowercase allowed set (used when case-insensitive)
398     * @param blockedSetLowercase Pre-processed lowercase blocked set (used when case-insensitive)
399     * @return true if the value is allowed, false if blocked or null
400     */
401    private boolean isAllowed(@Nullable String value,
402                              @Nullable Set<String> allowedSet,
403                              Set<String> blockedSet,
404                              @Nullable Set<String> allowedSetLowercase,
405                              Set<String> blockedSetLowercase) {
406        if (value == null) {
407            return false;
408        }
409
410        // For case-sensitive comparison, use original sets with O(1) contains
411        if (caseSensitiveComparison) {
412            // Check blocked list first
413            if (blockedSet.contains(value)) {
414                return false;
415            }
416            // If there's an allow list, check it
417            if (allowedSet != null) {
418                return allowedSet.contains(value);
419            }
420            // No allow list means all values are allowed (except blocked ones)
421            return true;
422        }
423
424        // For case-insensitive comparison, use pre-processed lowercase sets
425        // This provides O(1) average case performance instead of O(n)
426        String valueLowercase = value.toLowerCase();
427
428        // Check blocked list first using O(1) contains
429        if (blockedSetLowercase.contains(valueLowercase)) {
430            return false;
431        }
432
433        // If there's an allow list, check it using O(1) contains
434        if (allowedSetLowercase != null) {
435            return allowedSetLowercase.contains(valueLowercase);
436        }
437
438        // No allow list means all values are allowed (except blocked ones)
439        return true;
440    }
441
442    /**
443     * Checks if this configuration is considered "strict" based on key security settings.
444     *
445     * @return true if this configuration uses strict security policies
446     */
447    public boolean isStrict() {
448        return !allowPathTraversal &&
449                !allowDoubleEncoding &&
450                !allowNullBytes &&
451                !allowControlCharacters &&
452                !allowExtendedAscii &&
453                normalizeUnicode &&
454                failOnSuspiciousPatterns;
455    }
456
457    /**
458     * Checks if this configuration is considered "lenient" based on key security settings.
459     *
460     * @return true if this configuration uses lenient security policies
461     */
462    public boolean isLenient() {
463        return allowPathTraversal &&
464                allowDoubleEncoding &&
465                allowNullBytes &&
466                allowControlCharacters &&
467                allowExtendedAscii &&
468                !normalizeUnicode &&
469                !failOnSuspiciousPatterns;
470    }
471
472    // Getters for all fields
473
474    public int maxPathLength() {
475        return maxPathLength;
476    }
477
478    public boolean allowPathTraversal() {
479        return allowPathTraversal;
480    }
481
482    public boolean allowDoubleEncoding() {
483        return allowDoubleEncoding;
484    }
485
486    public int maxParameterCount() {
487        return maxParameterCount;
488    }
489
490    public int maxParameterNameLength() {
491        return maxParameterNameLength;
492    }
493
494    public int maxParameterValueLength() {
495        return maxParameterValueLength;
496    }
497
498    public int maxHeaderCount() {
499        return maxHeaderCount;
500    }
501
502    public int maxHeaderNameLength() {
503        return maxHeaderNameLength;
504    }
505
506    public int maxHeaderValueLength() {
507        return maxHeaderValueLength;
508    }
509
510    public @Nullable Set<String> allowedHeaderNames() {
511        return allowedHeaderNames;
512    }
513
514    public Set<String> blockedHeaderNames() {
515        return blockedHeaderNames;
516    }
517
518    public int maxCookieCount() {
519        return maxCookieCount;
520    }
521
522    public int maxCookieNameLength() {
523        return maxCookieNameLength;
524    }
525
526    public int maxCookieValueLength() {
527        return maxCookieValueLength;
528    }
529
530    public boolean requireSecureCookies() {
531        return requireSecureCookies;
532    }
533
534    public boolean requireHttpOnlyCookies() {
535        return requireHttpOnlyCookies;
536    }
537
538    public long maxBodySize() {
539        return maxBodySize;
540    }
541
542    public @Nullable Set<String> allowedContentTypes() {
543        return allowedContentTypes;
544    }
545
546    public Set<String> blockedContentTypes() {
547        return blockedContentTypes;
548    }
549
550    public boolean allowNullBytes() {
551        return allowNullBytes;
552    }
553
554    public boolean allowControlCharacters() {
555        return allowControlCharacters;
556    }
557
558    public boolean allowExtendedAscii() {
559        return allowExtendedAscii;
560    }
561
562    public boolean normalizeUnicode() {
563        return normalizeUnicode;
564    }
565
566    public boolean caseSensitiveComparison() {
567        return caseSensitiveComparison;
568    }
569
570    public boolean failOnSuspiciousPatterns() {
571        return failOnSuspiciousPatterns;
572    }
573
574    public boolean logSecurityViolations() {
575        return logSecurityViolations;
576    }
577
578    @Override
579    public boolean equals(Object obj) {
580        if (this == obj) return true;
581        if (!(obj instanceof SecurityConfiguration other)) return false;
582
583        return maxPathLength == other.maxPathLength &&
584                allowPathTraversal == other.allowPathTraversal &&
585                allowDoubleEncoding == other.allowDoubleEncoding &&
586                maxParameterCount == other.maxParameterCount &&
587                maxParameterNameLength == other.maxParameterNameLength &&
588                maxParameterValueLength == other.maxParameterValueLength &&
589                maxHeaderCount == other.maxHeaderCount &&
590                maxHeaderNameLength == other.maxHeaderNameLength &&
591                maxHeaderValueLength == other.maxHeaderValueLength &&
592                maxCookieCount == other.maxCookieCount &&
593                maxCookieNameLength == other.maxCookieNameLength &&
594                maxCookieValueLength == other.maxCookieValueLength &&
595                requireSecureCookies == other.requireSecureCookies &&
596                requireHttpOnlyCookies == other.requireHttpOnlyCookies &&
597                maxBodySize == other.maxBodySize &&
598                allowNullBytes == other.allowNullBytes &&
599                allowControlCharacters == other.allowControlCharacters &&
600                allowExtendedAscii == other.allowExtendedAscii &&
601                normalizeUnicode == other.normalizeUnicode &&
602                caseSensitiveComparison == other.caseSensitiveComparison &&
603                failOnSuspiciousPatterns == other.failOnSuspiciousPatterns &&
604                logSecurityViolations == other.logSecurityViolations &&
605                Objects.equals(allowedHeaderNames, other.allowedHeaderNames) &&
606                Objects.equals(blockedHeaderNames, other.blockedHeaderNames) &&
607                Objects.equals(allowedContentTypes, other.allowedContentTypes) &&
608                Objects.equals(blockedContentTypes, other.blockedContentTypes);
609    }
610
611    @Override
612    public int hashCode() {
613        return Objects.hash(
614                maxPathLength, allowPathTraversal, allowDoubleEncoding,
615                maxParameterCount, maxParameterNameLength, maxParameterValueLength,
616                maxHeaderCount, maxHeaderNameLength, maxHeaderValueLength,
617                allowedHeaderNames, blockedHeaderNames,
618                maxCookieCount, maxCookieNameLength, maxCookieValueLength,
619                requireSecureCookies, requireHttpOnlyCookies,
620                maxBodySize, allowedContentTypes, blockedContentTypes,
621                allowNullBytes, allowControlCharacters, allowExtendedAscii, normalizeUnicode,
622                caseSensitiveComparison, failOnSuspiciousPatterns, logSecurityViolations
623        );
624    }
625
626    @Override
627    public String toString() {
628        return "SecurityConfiguration{" +
629                "maxPathLength=" + maxPathLength +
630                ", allowPathTraversal=" + allowPathTraversal +
631                ", allowDoubleEncoding=" + allowDoubleEncoding +
632                ", maxParameterCount=" + maxParameterCount +
633                ", maxParameterNameLength=" + maxParameterNameLength +
634                ", maxParameterValueLength=" + maxParameterValueLength +
635                ", maxHeaderCount=" + maxHeaderCount +
636                ", maxHeaderNameLength=" + maxHeaderNameLength +
637                ", maxHeaderValueLength=" + maxHeaderValueLength +
638                ", allowedHeaderNames=" + allowedHeaderNames +
639                ", blockedHeaderNames=" + blockedHeaderNames +
640                ", maxCookieCount=" + maxCookieCount +
641                ", maxCookieNameLength=" + maxCookieNameLength +
642                ", maxCookieValueLength=" + maxCookieValueLength +
643                ", requireSecureCookies=" + requireSecureCookies +
644                ", requireHttpOnlyCookies=" + requireHttpOnlyCookies +
645                ", maxBodySize=" + maxBodySize +
646                ", allowedContentTypes=" + allowedContentTypes +
647                ", blockedContentTypes=" + blockedContentTypes +
648                ", allowNullBytes=" + allowNullBytes +
649                ", allowControlCharacters=" + allowControlCharacters +
650                ", allowExtendedAscii=" + allowExtendedAscii +
651                ", normalizeUnicode=" + normalizeUnicode +
652                ", caseSensitiveComparison=" + caseSensitiveComparison +
653                ", failOnSuspiciousPatterns=" + failOnSuspiciousPatterns +
654                ", logSecurityViolations=" + logSecurityViolations +
655                '}';
656    }
657}