diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java index d237935ac01..0c980268d1d 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java @@ -53,6 +53,20 @@ public IastRequestContext(final TaintedObjects taintedObjects) { this.taintedObjects = taintedObjects; } + /** + * Use this constructor only when you want to create a new context with a fresh overhead context + * (e.g. for testing purposes). + * + * @param taintedObjects the tainted objects to use + * @param overheadContext the overhead context to use + */ + public IastRequestContext( + final TaintedObjects taintedObjects, final OverheadContext overheadContext) { + this.vulnerabilityBatch = new VulnerabilityBatch(); + this.overheadContext = overheadContext; + this.taintedObjects = taintedObjects; + } + public VulnerabilityBatch getVulnerabilityBatch() { return vulnerabilityBatch; } @@ -188,6 +202,7 @@ public void releaseRequestContext(@Nonnull final IastContext context) { pool.offer(unwrapped); iastCtx.setTaintedObjects(TaintedObjects.NoOp.INSTANCE); } + iastCtx.overheadContext.resetMaps(); } } } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadContext.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadContext.java index b34b533f43a..fef5158d343 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadContext.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadContext.java @@ -3,16 +3,62 @@ import static datadog.trace.api.iast.IastDetectionMode.UNLIMITED; import com.datadog.iast.util.NonBlockingSemaphore; +import datadog.trace.api.iast.VulnerabilityTypes; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.jetbrains.annotations.NotNull; public class OverheadContext { + /** Maximum number of distinct endpoints to remember in the global cache. */ + private static final int GLOBAL_MAP_MAX_SIZE = 4096; + + /** + * Global concurrent cache mapping each “method + path” key to its historical vulnerabilityCounts + * map. As soon as size() > GLOBAL_MAP_MAX_SIZE, we clear() the whole map. + */ + static final ConcurrentMap globalMap = + new ConcurrentHashMap() { + + @Override + public AtomicIntegerArray computeIfAbsent( + String key, + @NotNull Function mappingFunction) { + AtomicIntegerArray prev = super.computeIfAbsent(key, mappingFunction); + if (this.size() > GLOBAL_MAP_MAX_SIZE) { + super.clear(); + } + return prev; + } + }; + + // Snapshot of the globalMap for the current request + @Nullable final Map copyMap; + // Map of vulnerabilities per endpoint for the current request, needs to use AtomicIntegerArray + // because it's possible to have concurrent updates in the same request + @Nullable final Map requestMap; + private final NonBlockingSemaphore availableVulnerabilities; + private final boolean isGlobal; public OverheadContext(final int vulnerabilitiesPerRequest) { + this(vulnerabilitiesPerRequest, false); + } + + public OverheadContext(final int vulnerabilitiesPerRequest, final boolean isGlobal) { availableVulnerabilities = vulnerabilitiesPerRequest == UNLIMITED ? NonBlockingSemaphore.unlimited() : NonBlockingSemaphore.withPermitCount(vulnerabilitiesPerRequest); + this.isGlobal = isGlobal; + this.requestMap = isGlobal ? null : new HashMap<>(); + this.copyMap = isGlobal ? null : new HashMap<>(); } public int getAvailableQuota() { @@ -26,4 +72,53 @@ public boolean consumeQuota(final int delta) { public void reset() { availableVulnerabilities.reset(); } + + public void resetMaps() { + // If this is a global context, we do not reset the maps + if (isGlobal || requestMap == null || copyMap == null) { + return; + } + Set endpoints = requestMap.keySet(); + // If the budget is not consumed, we can reset the maps + if (getAvailableQuota() > 0) { + // clean endpoints from globalMap + endpoints.forEach(globalMap::remove); + // Clear the requestMap and copyMap related to this context + requestMap.clear(); + copyMap.clear(); + return; + } + // If the budget is consumed, we need to merge the requestMap into the globalMap + endpoints.forEach( + endpoint -> { + AtomicIntegerArray countMap = requestMap.get(endpoint); + // should not happen, but just in case + if (countMap == null) { + globalMap.remove(endpoint); + return; + } + // Iterate over the vulnerabilities and update the globalMap + int numberOfVulnerabilities = VulnerabilityTypes.STRINGS.length; + for (int i = 0; i < numberOfVulnerabilities; i++) { + int counter = countMap.get(i); + if (counter > 0) { + AtomicIntegerArray globalCountMap = + globalMap.computeIfAbsent( + endpoint, value -> new AtomicIntegerArray(numberOfVulnerabilities)); + int globalCounter = globalCountMap.get(i); + if (counter > globalCounter) { + globalCountMap.set(i, counter); + } + } + } + }); + + // Clear the requestMap and copyMap related to this context + requestMap.clear(); + copyMap.clear(); + } + + public boolean isGlobal() { + return isGlobal; + } } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java index 0cd4575056a..b0e47c86625 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java @@ -1,19 +1,25 @@ package com.datadog.iast.overhead; +import static com.datadog.iast.overhead.OverheadContext.globalMap; import static datadog.trace.api.iast.IastDetectionMode.UNLIMITED; import com.datadog.iast.IastRequestContext; import com.datadog.iast.IastSystem; +import com.datadog.iast.model.VulnerabilityType; import com.datadog.iast.util.NonBlockingSemaphore; import datadog.trace.api.Config; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.iast.IastContext; +import datadog.trace.api.iast.VulnerabilityTypes; import datadog.trace.api.telemetry.LogCollector; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.util.AgentTaskScheduler; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -27,9 +33,12 @@ public interface OverheadController { int releaseRequest(); - boolean hasQuota(final Operation operation, @Nullable final AgentSpan span); + boolean hasQuota(Operation operation, @Nullable AgentSpan span); - boolean consumeQuota(final Operation operation, @Nullable final AgentSpan span); + boolean consumeQuota(Operation operation, @Nullable AgentSpan span); + + boolean consumeQuota( + Operation operation, @Nullable AgentSpan span, @Nullable VulnerabilityType type); static OverheadController build(final Config config, final AgentTaskScheduler scheduler) { return build( @@ -100,14 +109,23 @@ public boolean hasQuota(final Operation operation, @Nullable final AgentSpan spa @Override public boolean consumeQuota(final Operation operation, @Nullable final AgentSpan span) { - final boolean result = delegate.consumeQuota(operation, span); + return consumeQuota(operation, span, null); + } + + @Override + public boolean consumeQuota( + final Operation operation, + @Nullable final AgentSpan span, + @Nullable final VulnerabilityType type) { + final boolean result = delegate.consumeQuota(operation, span, type); if (LOGGER.isDebugEnabled()) { LOGGER.debug( - "consumeQuota: operation={}, result={}, availableQuota={}, span={}", + "consumeQuota: operation={}, result={}, availableQuota={}, span={}, type={}", operation, result, getAvailableQuote(span), - span); + span, + type); } return result; } @@ -147,7 +165,7 @@ class OverheadControllerImpl implements OverheadController { private volatile long lastAcquiredTimestamp = Long.MAX_VALUE; final OverheadContext globalContext = - new OverheadContext(Config.get().getIastVulnerabilitiesPerRequest()); + new OverheadContext(Config.get().getIastVulnerabilitiesPerRequest(), true); public OverheadControllerImpl( final float requestSampling, @@ -192,7 +210,90 @@ public boolean hasQuota(final Operation operation, @Nullable final AgentSpan spa @Override public boolean consumeQuota(final Operation operation, @Nullable final AgentSpan span) { - return operation.consumeQuota(getContext(span)); + return consumeQuota(operation, span, null); + } + + @Override + public boolean consumeQuota( + final Operation operation, + @Nullable final AgentSpan span, + @Nullable final VulnerabilityType type) { + + OverheadContext ctx = getContext(span); + if (ctx == null) { + return false; + } + if (ctx.isGlobal()) { + return operation.consumeQuota(ctx); + } + if (operation.hasQuota(ctx)) { + String method = null; + String path = null; + if (span != null) { + AgentSpan rootSpan = span.getLocalRootSpan(); + Object methodTag = rootSpan.getTag(Tags.HTTP_METHOD); + method = (methodTag == null) ? "" : methodTag.toString(); + Object routeTag = rootSpan.getTag(Tags.HTTP_ROUTE); + path = (routeTag == null) ? "" : routeTag.toString(); + } + if (!maybeSkipVulnerability(ctx, type, method, path)) { + return operation.consumeQuota(ctx); + } + } + return false; + } + + /** + * Method to be called when a vulnerability of a certain type is detected. Implements the + * RFC-1029 algorithm. + * + * @param ctx the overhead context for the current request + * @param type the type of vulnerability detected + * @param httpMethod the HTTP method of the request (e.g., GET, POST) + * @param httpPath the HTTP path of the request + * @return true if the vulnerability should be skipped, false otherwise + */ + private boolean maybeSkipVulnerability( + @Nullable final OverheadContext ctx, + @Nullable final VulnerabilityType type, + @Nullable final String httpMethod, + @Nullable final String httpPath) { + + if (ctx == null || type == null || ctx.requestMap == null || ctx.copyMap == null) { + return false; + } + + int numberOfVulnerabilities = VulnerabilityTypes.STRINGS.length; + + String currentEndpoint = httpMethod + " " + httpPath; + Set endpoints = ctx.requestMap.keySet(); + + if (!endpoints.contains(currentEndpoint)) { + AtomicIntegerArray globalArray = + globalMap.getOrDefault( + currentEndpoint, new AtomicIntegerArray(numberOfVulnerabilities)); + ctx.copyMap.put(currentEndpoint, toIntArray(globalArray)); + } + + ctx.requestMap.computeIfAbsent( + currentEndpoint, k -> new AtomicIntegerArray(numberOfVulnerabilities)); + int counter = ctx.requestMap.get(currentEndpoint).getAndIncrement(type.type()); + int storedCounter = 0; + int[] copyArray = ctx.copyMap.get(currentEndpoint); + if (copyArray != null) { + storedCounter = copyArray[type.type()]; + } + + return counter < storedCounter; + } + + private static int[] toIntArray(AtomicIntegerArray atomic) { + int length = atomic.length(); + int[] result = new int[length]; + for (int i = 0; i < length; i++) { + result[i] = atomic.get(i); + } + return result; } @Nullable diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HttpResponseHeaderModuleImpl.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HttpResponseHeaderModuleImpl.java index 21d372c7cab..bccd0e339ab 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HttpResponseHeaderModuleImpl.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/HttpResponseHeaderModuleImpl.java @@ -1,5 +1,6 @@ package com.datadog.iast.sink; +import static com.datadog.iast.model.VulnerabilityType.INSECURE_COOKIE; import static com.datadog.iast.util.HttpHeader.SET_COOKIE; import static com.datadog.iast.util.HttpHeader.SET_COOKIE2; import static java.util.Collections.singletonList; @@ -65,7 +66,10 @@ private void onCookies(final List cookies) { return; } final AgentSpan span = AgentTracer.activeSpan(); - if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { + // algorithm is able to report all endpoint vulnerabilities + if (!overheadController.consumeQuota( + Operations.REPORT_VULNERABILITY, span, INSECURE_COOKIE // we need a type to check quota + )) { return; } final Location location = Location.forSpanAndStack(span, getCurrentStackTrace()); diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java index fb694a96d77..38c66cb4c53 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java @@ -58,7 +58,8 @@ protected void report(final Vulnerability vulnerability) { } protected void report(@Nullable final AgentSpan span, final Vulnerability vulnerability) { - if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { + if (!overheadController.consumeQuota( + Operations.REPORT_VULNERABILITY, span, vulnerability.getType())) { return; } reporter.report(span, vulnerability); @@ -70,7 +71,7 @@ protected void report(final VulnerabilityType type, final Evidence evidence) { protected void report( @Nullable final AgentSpan span, final VulnerabilityType type, final Evidence evidence) { - if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { + if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span, type)) { return; } final Vulnerability vulnerability = @@ -170,7 +171,7 @@ protected final Evidence checkInjection( } final AgentSpan span = AgentTracer.activeSpan(); - if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { + if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span, type)) { return null; } @@ -251,7 +252,7 @@ protected final Evidence checkInjection( if (!spanFetched && valueRanges != null && valueRanges.length > 0) { span = AgentTracer.activeSpan(); spanFetched = true; - if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { + if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span, type)) { return null; } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastModuleImplTestBase.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastModuleImplTestBase.groovy index 0d031c7d566..7d7d77e7dd6 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastModuleImplTestBase.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastModuleImplTestBase.groovy @@ -114,6 +114,7 @@ class IastModuleImplTestBase extends DDSpecification { return Stub(OverheadController) { acquireRequest() >> true consumeQuota(_ as Operation, _) >> true + consumeQuota(_ as Operation, _, _) >> true } } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastRequestContextTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastRequestContextTest.groovy index 5d7fe0dde7c..a9621171fb6 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastRequestContextTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastRequestContextTest.groovy @@ -1,6 +1,8 @@ package com.datadog.iast import com.datadog.iast.model.Range +import com.datadog.iast.overhead.OverheadContext +import com.datadog.iast.taint.TaintedMap import com.datadog.iast.taint.TaintedObjects import datadog.trace.api.Config import datadog.trace.api.gateway.RequestContext @@ -120,4 +122,16 @@ class IastRequestContextTest extends DDSpecification { then: ctx.taintedObjects.count() == 0 } + + void 'on release context overheadContext reset is called'() { + setup: + final overheadCtx = Mock(OverheadContext) + final ctx = new IastRequestContext(TaintedObjects.build(TaintedMap.build(TaintedMap.DEFAULT_CAPACITY)), overheadCtx) + + when: + provider.releaseRequestContext(ctx) + + then: + 1 * overheadCtx.resetMaps() + } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadContextTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadContextTest.groovy index c606e30c125..befdc3a4fc3 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadContextTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadContextTest.groovy @@ -1,7 +1,9 @@ package com.datadog.iast.overhead +import com.datadog.iast.model.VulnerabilityType import datadog.trace.api.Config import datadog.trace.api.iast.IastContext +import datadog.trace.api.iast.VulnerabilityTypes import datadog.trace.test.util.DDSpecification import datadog.trace.util.AgentTaskScheduler import com.datadog.iast.overhead.OverheadController.OverheadControllerImpl @@ -9,9 +11,16 @@ import groovy.transform.CompileDynamic import static datadog.trace.api.iast.IastDetectionMode.UNLIMITED +import java.util.concurrent.atomic.AtomicIntegerArray + @CompileDynamic class OverheadContextTest extends DDSpecification { + @Override + void cleanup() { + OverheadContext.globalMap.clear() + } + void 'Can reset global overhead context'() { given: def taskSchedler = Stub(AgentTaskScheduler) @@ -69,4 +78,190 @@ class OverheadContextTest extends DDSpecification { !consumed.any { !it } overheadContext.availableQuota == Integer.MAX_VALUE } + + void 'if it is global sampling maps are null'() { + given: + OverheadContext ctx = new OverheadContext(1, true) + + expect: + ctx.requestMap == null + ctx.copyMap == null + } + + void "resetMaps is no-op when context is global"() { + given: + def ctx = new OverheadContext(5, true) + def array = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + array.incrementAndGet(VulnerabilityType.WEAK_HASH.type()) + OverheadContext.globalMap.put("endpoint", array) + + + when: + ctx.resetMaps() + + then: + // globalMap remains unchanged + OverheadContext.globalMap.get('endpoint').get(VulnerabilityType.WEAK_HASH.type()) == 1 + ctx.copyMap == null + ctx.requestMap == null + } + + void "resetMaps clears request and copy maps when quota remains"() { + given: + def ctx = new OverheadContext(3, false) + // Prepare global entry for "endpoint" + def globalArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + globalArray.getAndSet(VulnerabilityType.SQL_INJECTION.type(), 2) + OverheadContext.globalMap.put("endpoint", globalArray) + def requestArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + requestArray.set(VulnerabilityType.WEAK_HASH.type(), 1) + ctx.requestMap.put("endpoint", requestArray) + def copyArray = new int[VulnerabilityTypes.STRINGS.length] + copyArray[VulnerabilityType.WEAK_HASH.type()] = 1 + ctx.copyMap.put("endpoint", copyArray) + assert ctx.getAvailableQuota() > 0 + + when: + ctx.resetMaps() + + then: + // Since quota > 0, we remove any global entry for "endpoint" (none here) + OverheadContext.globalMap.isEmpty() + // Per-request and copy maps are cleared + ctx.requestMap.isEmpty() + ctx.copyMap.isEmpty() + } + + void "resetMaps removes global entry when quota consumed and countMap is null"() { + given: + def ctx = new OverheadContext(1, false) + // Prepare global entry for "endpoint" + def globalArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + globalArray.getAndSet(VulnerabilityType.SQL_INJECTION.type(), 1) + // Simulate per-request endpoint present but no inner map + ctx.requestMap.put("endpoint", null) + def copyArray = new int[VulnerabilityTypes.STRINGS.length] + copyArray[VulnerabilityType.SQL_INJECTION.type()] = 2 + ctx.copyMap.put("endpoint", copyArray) + // Consume the only permit + ctx.consumeQuota(1) + assert ctx.getAvailableQuota() == 0 + + when: + ctx.resetMaps() + + then: + // requestMap get("endpoint") was null → globalMap.remove("endpoint") + !OverheadContext.globalMap.containsKey("endpoint") + ctx.requestMap.isEmpty() + ctx.copyMap.isEmpty() + } + + void "resetMaps merges and updates global entry when quota consumed "() { + given: + def ctx = new OverheadContext(1, false) + def globalArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + globalArray.getAndSet(VulnerabilityType.WEAK_HASH.type(), 1) + OverheadContext.globalMap.put("endpoint", globalArray) + // Simulate we saw 3 in this request + def requestArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + requestArray.set(VulnerabilityType.WEAK_HASH.type(), 3) + ctx.requestMap.put("endpoint", requestArray) + def copyArray = new int[VulnerabilityTypes.STRINGS.length] + copyArray[VulnerabilityType.WEAK_HASH.type()] = 1 + ctx.copyMap.put("endpoint", copyArray) + ctx.consumeQuota(1) + assert ctx.getAvailableQuota() == 0 + + when: + ctx.resetMaps() + + then: + // The max of (global=1, request=3) is 3, so globalMap is updated + OverheadContext.globalMap.get("endpoint").get(VulnerabilityType.WEAK_HASH.type()) == 3 + ctx.requestMap.isEmpty() + ctx.copyMap.isEmpty() + } + + void "resetMaps merges and updates global entry when quota consumed and counter <= globalCounter"() { + given: + def ctx = new OverheadContext(1, false) + def globalArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + globalArray.getAndSet(VulnerabilityType.WEAK_HASH.type(), 2) + OverheadContext.globalMap.put("endpoint", globalArray) + def requestArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + requestArray.set(VulnerabilityType.WEAK_HASH.type(), 1) + ctx.requestMap.put("endpoint", requestArray) + def copyArray = new int[VulnerabilityTypes.STRINGS.length] + copyArray[VulnerabilityType.WEAK_HASH.type()] = 2 + ctx.copyMap.put("endpoint", copyArray) + ctx.consumeQuota(1) + assert ctx.getAvailableQuota() == 0 + + when: + ctx.resetMaps() + + then: + // The max of (global=1, request=3) is 3, so globalMap is updated + OverheadContext.globalMap.get("endpoint").get(VulnerabilityType.WEAK_HASH.type()) == 2 + ctx.requestMap.isEmpty() + ctx.copyMap.isEmpty() + } + + void "resetMaps merges and updates global entry when quota consumed and a vuln is detected in a new endpoint"() { + given: + def ctx = new OverheadContext(1, false) + def globalArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + globalArray.getAndSet(VulnerabilityType.WEAK_HASH.type(), 1) + OverheadContext.globalMap.put("endpoint", globalArray) + def requestArray = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + requestArray.set(VulnerabilityType.WEAK_CIPHER.type(), 1) + ctx.requestMap.put("endpoint2", requestArray) + def copyArray = new int[VulnerabilityTypes.STRINGS.length] + copyArray[VulnerabilityType.WEAK_HASH.type()] = 1 + ctx.copyMap.put("endpoint", copyArray) + ctx.consumeQuota(1) + assert ctx.getAvailableQuota() == 0 + + when: + ctx.resetMaps() + + then: + OverheadContext.globalMap.get("endpoint").get(VulnerabilityType.WEAK_HASH.type()) == 1 + OverheadContext.globalMap.get("endpoint2").get(VulnerabilityType.WEAK_CIPHER.type()) == 1 + ctx.requestMap.isEmpty() + ctx.copyMap.isEmpty() + } + + + void "computeIfAbsent should not clear until size exceeds GLOBAL_MAP_MAX_SIZE"() { + given: "We know the maximum size" + int maxSize = OverheadContext.GLOBAL_MAP_MAX_SIZE + + when: "We insert exactly maxSize distinct keys via computeIfAbsent" + (1..maxSize).each { i -> + AtomicIntegerArray arr = OverheadContext.globalMap.computeIfAbsent("key" + i) { + new AtomicIntegerArray([i] as int[]) + } + // verify returned array holds the correct value + assert arr.get(0) == i + } + + then: "The map size is exactly maxSize and none of those keys was evicted" + OverheadContext.globalMap.size() == maxSize + (1..maxSize).each { i -> + assert OverheadContext.globalMap.containsKey("key"+i) + assert OverheadContext.globalMap.get("key"+i).get(0) == i + } + + when: "We invoke computeIfAbsent on one more distinct key, which should trigger clear()" + AtomicIntegerArray extra = OverheadContext.globalMap.computeIfAbsent("keyExtra") { + new AtomicIntegerArray([999] as int[]) + } + + then: "Upon exceeding maxSize, the map has been cleared completely" + OverheadContext.globalMap.isEmpty() + // And the returned array is still the one newly created + extra.get(0) == 999 + } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadControllerTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadControllerTest.groovy index 875789e7466..bc448a66bcf 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadControllerTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/overhead/OverheadControllerTest.groovy @@ -2,14 +2,19 @@ package com.datadog.iast.overhead import com.datadog.iast.IastRequestContext import com.datadog.iast.overhead.OverheadController.OverheadControllerImpl +import com.datadog.iast.taint.TaintedMap +import com.datadog.iast.taint.TaintedObjects import datadog.trace.api.Config import datadog.trace.api.gateway.RequestContext import datadog.trace.api.gateway.RequestContextSlot +import datadog.trace.api.iast.VulnerabilityTypes import datadog.trace.bootstrap.instrumentation.api.AgentSpan +import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.test.util.DDSpecification import datadog.trace.util.AgentTaskScheduler import groovy.transform.CompileDynamic import spock.lang.Shared +import com.datadog.iast.model.VulnerabilityType import java.util.concurrent.Callable import java.util.concurrent.CountDownLatch @@ -19,6 +24,9 @@ import java.util.concurrent.Semaphore import static datadog.trace.api.iast.IastDetectionMode.UNLIMITED +import java.util.concurrent.atomic.AtomicIntegerArray + + @CompileDynamic class OverheadControllerTest extends DDSpecification { @@ -283,6 +291,171 @@ class OverheadControllerTest extends DDSpecification { !lastAcquired } + void "maybeSkipVulnerability returns false if ctx or type is null"() { + given: + final controller = new OverheadControllerImpl(100f, 10, true, null) + final ctx = Mock(OverheadContext) + ctx.requestMap >>> [null, [:]] + ctx.copyMap >> null + + when: "maybeSkipVulnerability returns false if ctx or type is null" + def skip1 = controller.maybeSkipVulnerability(null, VulnerabilityType.WEAK_HASH, "GET", "/path") + + then: + !skip1 + + when: "maybeSkipVulnerability returns false if type is null" + def skip2 = controller.maybeSkipVulnerability(ctx, null, "GET", "/path") + + then: + !skip2 + + when: "maybeSkipVulnerability returns false if ctx.requestMap is null" + def skip3 = controller.maybeSkipVulnerability(ctx, null, "GET", "/path") + + then: + !skip3 + + when: "maybeSkipVulnerability returns false if ctx.requestMap is empty" + def skip4 = controller.maybeSkipVulnerability(ctx, VulnerabilityType.WEAK_HASH, "GET", "/path") + + then: + !skip4 + } + + void "maybeSkipVulnerability returns true when global count is higher"() { + given: + final ctx = new OverheadContext(Config.get().getIastVulnerabilitiesPerRequest()) + final controller = new OverheadControllerImpl(100f, 10, true, null) + // Simulate that in a previous request, GLOBAL has counted 3 SQL_INJECTION for "GET /bar" + def array = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + array.set(VulnerabilityType.SQL_INJECTION.type(), 3) + OverheadContext.globalMap.put("GET /bar", array) + + when: + // First occurrence in this request: counter=1, storedCounter=3 ⇒ skip + boolean skipFirst = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "GET", "/bar") + // Second occurrence: requestMap now equals 1 internally, storedCounter=3 still ⇒ skip + boolean skipSecond = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "GET", "/bar") + // Simulate calling a third time: counter in requestMap incremented to 2 ⇒ still 2 < 3 ⇒ skip + boolean skipThird = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "GET", "/bar") + // Fourth time: counter=3, storedCounter=3 ⇒ not skip + boolean skipFourth = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "GET", "/bar") + + then: + skipFirst + skipSecond + skipThird + !skipFourth + + when: + boolean skipPost = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "POST", "/bar") + + then: + !skipPost + + when: + boolean skipAnotherEndpoint = controller.maybeSkipVulnerability(ctx, VulnerabilityType.SQL_INJECTION, "GET", "/bar2") + + then: + !skipAnotherEndpoint + } + + void "consumeQuota: globalContext path always calls operation.consumeQuota"() { + given: "An Operation stub that always returns true for both hasQuota and consumeQuota" + final controller = new OverheadControllerImpl(100f, 10, true, null) + def dummyOp = Stub(Operation) { + hasQuota(_ as OverheadContext) >> false // hasQuota won’t matter for global path + consumeQuota(_ as OverheadContext) >> true + } + + expect: + // Because controller was built with useGlobalAsFallback=true, passing null span yields globalContext + controller.consumeQuota(dummyOp, null, VulnerabilityType.WEAK_HASH) + } + + void "consumeQuota: when IAST context present and hasQuota false returns false"() { + given: + // Build a fake span + requestContext + IAST context setup + OverheadContext localCtx = new OverheadContext(10, false) + def iastCtx = new IastRequestContext(TaintedObjects.build(TaintedMap.build(TaintedMap.DEFAULT_CAPACITY)), localCtx) + final controller = new OverheadControllerImpl(100f, 10, true, null) + + RequestContext rc = Stub(RequestContext) { + getData(RequestContextSlot.IAST) >> iastCtx + } + AgentSpan span = Stub(AgentSpan) + span.getRequestContext() >> rc + span.getLocalRootSpan() >> span // return itself for tag lookups + span.getTag(Tags.HTTP_METHOD) >> "POST" + span.getTag(Tags.HTTP_ROUTE) >> "/do" + + def op = Stub(Operation) { + hasQuota(localCtx) >> false // even though context exists, hasQuota = false + consumeQuota(localCtx) >> true + } + + expect: + !controller.consumeQuota(op, span, VulnerabilityType.WEAK_CIPHER) + } + + void "consumeQuota: when hasQuota true but maybeSkipVulnerability returns true => false"() { + given: + // Prepare local context and global count so that skip logic triggers immediately + OverheadContext localCtx = new OverheadContext(10, false) + def array = new AtomicIntegerArray(VulnerabilityTypes.STRINGS.length) + array.set(VulnerabilityType.WEAK_CIPHER.type(), 2) + OverheadContext.globalMap.put("PUT /skipme", array) + def iastCtx = new IastRequestContext(TaintedObjects.build(TaintedMap.build(TaintedMap.DEFAULT_CAPACITY)), localCtx) + final controller = new OverheadControllerImpl(100f, 10, true, null) + + RequestContext rc = Stub(RequestContext) { + getData(RequestContextSlot.IAST) >> iastCtx + } + AgentSpan span = Stub(AgentSpan) + span.getRequestContext() >> rc + span.getLocalRootSpan() >> span // return the stub itself + span.getTag(Tags.HTTP_METHOD) >> "PUT" + span.getTag(Tags.HTTP_ROUTE) >> "/skipme" + + def op = Stub(Operation) { + hasQuota(localCtx) >> true + consumeQuota(localCtx) >> true + } + + expect: + // First call: maybeSkip sees global=2, request counter=1 ⇒ skip → consumeQuota not called + !controller.consumeQuota(op, span, VulnerabilityType.WEAK_CIPHER) + } + + void "consumeQuota: when hasQuota true and skip=false, calls consume and returns true"() { + given: + OverheadContext localCtx = new OverheadContext(10, false) + def iastCtx = new IastRequestContext(TaintedObjects.build(TaintedMap.build(TaintedMap.DEFAULT_CAPACITY)), localCtx) + final controller = new OverheadControllerImpl(100f, 10, true, null) + + RequestContext rc = Stub(RequestContext) { + getData(RequestContextSlot.IAST) >> iastCtx + } + AgentSpan span = Stub(AgentSpan) + span.getRequestContext() >> rc + span.getLocalRootSpan() >> span // return the stub itself + span.getTag(Tags.HTTP_METHOD) >> "PATCH" + span.getTag(Tags.HTTP_ROUTE) >> "/allow" + + // No globalMap entry for "PATCH /allow", so skip=false on first invocation + def op = Stub(Operation) { + hasQuota(localCtx) >> true + consumeQuota(localCtx) >> { OverheadContext ctx -> + // As soon as consumeQuota is called, record it by incrementing a counter + return true + } + } + + expect: + controller.consumeQuota(op, span, VulnerabilityType.WEAK_CIPHER) + } + private AgentSpan getAgentSpanWithOverheadContext() { def iastRequestContext = Stub(IastRequestContext) iastRequestContext.getOverheadContext() >> new OverheadContext(Config.get().getIastVulnerabilitiesPerRequest()) diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy index bc5f5ef305a..e86040d9cc2 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy @@ -36,7 +36,7 @@ class HstsMissingHeaderModuleTest extends IastModuleImplTestBase { protected OverheadController buildOverheadController() { return Mock(OverheadController) { acquireRequest() >> true - consumeQuota(_ as Operation, _ as AgentSpan) >> true + consumeQuota(_ as Operation, _ as AgentSpan, _ as VulnerabilityType) >> true } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HttpResponseHeaderModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HttpResponseHeaderModuleTest.groovy index c1a35d10c65..d530fefa6d3 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HttpResponseHeaderModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HttpResponseHeaderModuleTest.groovy @@ -85,7 +85,7 @@ class HttpResponseHeaderModuleTest extends IastModuleImplTestBase { module.onHeader(header, value) then: - overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span) >> false // do not report in this test + overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span, _ as VulnerabilityType) >> false // do not report in this test activeSpanCount * tracer.activeSpan() >> { return span } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/WeakRandomnessModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/WeakRandomnessModuleTest.groovy index 03125f3f996..e1f9806a046 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/WeakRandomnessModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/WeakRandomnessModuleTest.groovy @@ -2,6 +2,7 @@ package com.datadog.iast.sink import com.datadog.iast.IastModuleImplTestBase import com.datadog.iast.Reporter +import com.datadog.iast.model.VulnerabilityType import com.datadog.iast.overhead.Operations import datadog.trace.api.iast.sink.WeakRandomnessModule @@ -54,7 +55,7 @@ class WeakRandomnessModuleTest extends IastModuleImplTestBase { module.onWeakRandom(Random) then: - overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span) >> false + overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span, _ as VulnerabilityType) >> false 0 * _ } } diff --git a/dd-java-agent/agent-iast/src/testFixtures/groovy/com/datadog/iast/test/NoopOverheadController.groovy b/dd-java-agent/agent-iast/src/testFixtures/groovy/com/datadog/iast/test/NoopOverheadController.groovy index 32152663dc1..7535c112003 100644 --- a/dd-java-agent/agent-iast/src/testFixtures/groovy/com/datadog/iast/test/NoopOverheadController.groovy +++ b/dd-java-agent/agent-iast/src/testFixtures/groovy/com/datadog/iast/test/NoopOverheadController.groovy @@ -1,5 +1,6 @@ package com.datadog.iast.test +import com.datadog.iast.model.VulnerabilityType import com.datadog.iast.overhead.Operation import com.datadog.iast.overhead.OverheadController import com.github.javaparser.quality.Nullable @@ -28,6 +29,11 @@ class NoopOverheadController implements OverheadController { true } + @Override + boolean consumeQuota(Operation operation, @Nullable AgentSpan span, @Nullable VulnerabilityType type) { + true + } + @Override void reset() { } diff --git a/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastSamplingController.java b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastSamplingController.java new file mode 100644 index 00000000000..93f1b3f5c76 --- /dev/null +++ b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastSamplingController.java @@ -0,0 +1,97 @@ +package datadog.smoketest.springboot.controller; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IastSamplingController { + + @GetMapping("/multiple_vulns/{i}") + public String multipleVulns( + @PathVariable("i") int i, + @RequestParam(name = "param", required = false) String paramValue, + HttpServletRequest request, + HttpServletResponse response) + throws NoSuchAlgorithmException { + // weak hash + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)); + // Insecure cookie + Cookie cookie = new Cookie("user-id", "7"); + response.addCookie(cookie); + // weak hash + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)); + // untrusted deserialization + try { + final ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); + ois.close(); + } catch (IOException e) { + // Ignore IOException + } + // weak hash + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)); + return "OK"; + } + + @GetMapping("/multiple_vulns-2/{i}") + public String multipleVulns2( + @PathVariable("i") int i, + @RequestParam(name = "param", required = false) String paramValue, + HttpServletRequest request, + HttpServletResponse response) + throws NoSuchAlgorithmException { + // weak hash + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)); + // Insecure cookie + Cookie cookie = new Cookie("user-id", "7"); + response.addCookie(cookie); + // weak hash + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)); + // untrusted deserialization + try { + final ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); + ois.close(); + } catch (IOException e) { + // Ignore IOException + } + // weak hash + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)); + return "OK"; + } + + @PostMapping("/multiple_vulns/{i}") + public String multipleVulnsPost( + @PathVariable("i") int i, + @RequestParam(name = "param", required = false) String paramValue, + HttpServletRequest request, + HttpServletResponse response) + throws NoSuchAlgorithmException { + // weak hash + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)); + // Insecure cookie + Cookie cookie = new Cookie("user-id", "7"); + response.addCookie(cookie); + // weak hash + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)); + // untrusted deserialization + try { + final ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); + ois.close(); + } catch (IOException e) { + // Ignore IOException + } + // weak hash + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)); + return "OK"; + } +} diff --git a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastOverheadControlSpringBootSmokeTest.groovy b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastOverheadControlSpringBootSmokeTest.groovy new file mode 100644 index 00000000000..73f6d5beafa --- /dev/null +++ b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastOverheadControlSpringBootSmokeTest.groovy @@ -0,0 +1,92 @@ +package datadog.smoketest + +import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE +import static datadog.trace.api.config.IastConfig.IAST_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING +import groovy.transform.CompileDynamic +import okhttp3.FormBody +import okhttp3.Request + +@CompileDynamic +class IastOverheadControlSpringBootSmokeTest extends AbstractIastServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty('datadog.smoketest.springboot.shadowJar.path') + + List command = [] + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(iastJvmOpts()) + command.addAll((String[]) ['-jar', springBootShadowJar, "--server.port=${httpPort}"]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + // Spring will print all environment variables to the log, which may pollute it and affect log assertions. + processBuilder.environment().clear() + return processBuilder + } + + protected List iastJvmOpts() { + return [ + withSystemProperty(IAST_ENABLED, true), + withSystemProperty(IAST_DETECTION_MODE, 'DEFAULT'), + withSystemProperty(IAST_DEBUG_ENABLED, true), + withSystemProperty(IAST_REQUEST_SAMPLING, 100), + ] + } + + void 'test'() { + given: + // prepare a list of exactly three GET requests with path and query param + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/multiple_vulns/${i}/?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/multiple_vulns-2/${i}/?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'NO_SAMESITE_COOKIE' && vul.location.method == 'multipleVulns'} + hasVulnerability { vul -> vul.type == 'NO_HTTPONLY_COOKIE' && vul.location.method == 'multipleVulns' } + hasVulnerability { vul -> vul.type == 'INSECURE_COOKIE' && vul.location.method == 'multipleVulns'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'UNTRUSTED_DESERIALIZATION' && vul.location.method == 'multipleVulns'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns' && vul.evidence.value == 'MD2'} + + and: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns2' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'NO_SAMESITE_COOKIE' && vul.location.method == 'multipleVulns2'} + hasVulnerability { vul -> vul.type == 'NO_HTTPONLY_COOKIE' && vul.location.method == 'multipleVulns2' } + hasVulnerability { vul -> vul.type == 'INSECURE_COOKIE' && vul.location.method == 'multipleVulns2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns2' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'UNTRUSTED_DESERIALIZATION' && vul.location.method == 'multipleVulns2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulns2' && vul.evidence.value == 'MD2'} + + and: 'check post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulnsPost' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'NO_SAMESITE_COOKIE' && vul.location.method == 'multipleVulnsPost'} + hasVulnerability { vul -> vul.type == 'NO_HTTPONLY_COOKIE' && vul.location.method == 'multipleVulnsPost'} + hasVulnerability { vul -> vul.type == 'INSECURE_COOKIE' && vul.location.method == 'multipleVulnsPost'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulnsPost' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'UNTRUSTED_DESERIALIZATION' && vul.location.method == 'multipleVulnsPost'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == 'multipleVulnsPost'&& vul.evidence.value == 'MD2'} + } + +}