-
Notifications
You must be signed in to change notification settings - Fork 304
Optimize IAST Vulnerability Detection #8885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
416dbae
ea49404
61319c9
b22b445
472547c
0921d3d
97d2a10
b0e2a61
b345cc0
46dea01
4dc2e87
96c0003
1ed6931
21458cf
1ff5c57
73d972e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,17 +2,53 @@ | |
|
||
import static datadog.trace.api.iast.IastDetectionMode.UNLIMITED; | ||
|
||
import com.datadog.iast.model.VulnerabilityType; | ||
import com.datadog.iast.util.NonBlockingSemaphore; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import javax.annotation.Nullable; | ||
|
||
public class OverheadContext { | ||
|
||
/** | ||
* Maximum number of distinct endpoints to remember in the global cache (LRU eviction beyond this | ||
* size). | ||
*/ | ||
private static final int GLOBAL_MAP_MAX_SIZE = 4096; | ||
|
||
/** | ||
* Global LRU cache mapping each “method + path” key to its historical vulnerabilityCounts map. | ||
* Key: HTTP_METHOD + " " + HTTP_PATH Value: Map<vulnerabilityType, count> | ||
*/ | ||
static final Map<String, Map<VulnerabilityType, Integer>> globalMap = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we shouldn't be using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! I’ve updated all the collections as we agreed offline |
||
new LinkedHashMap<String, Map<VulnerabilityType, Integer>>(GLOBAL_MAP_MAX_SIZE, 0.75f, true) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
@Override | ||
protected boolean removeEldestEntry( | ||
Map.Entry<String, Map<VulnerabilityType, Integer>> eldest) { | ||
return size() > GLOBAL_MAP_MAX_SIZE; | ||
} | ||
}; | ||
|
||
@Nullable final Map<String, Map<VulnerabilityType, Integer>> copyMap; | ||
@Nullable final Map<String, Map<VulnerabilityType, Integer>> 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 +62,49 @@ public boolean consumeQuota(final int delta) { | |
public void reset() { | ||
availableVulnerabilities.reset(); | ||
} | ||
|
||
public void resetMaps() { | ||
if (isGlobal || requestMap == null || copyMap == null) { | ||
return; | ||
} | ||
// If the budget is not consumed, we can reset the maps | ||
Set<String> keys = requestMap.keySet(); | ||
if (getAvailableQuota() > 0) { | ||
keys.forEach(globalMap::remove); | ||
keys.clear(); | ||
requestMap.clear(); | ||
copyMap.clear(); | ||
return; | ||
} | ||
keys.forEach( | ||
key -> { | ||
Map<VulnerabilityType, Integer> countMap = requestMap.get(key); | ||
// should not happen, but just in case | ||
if (countMap == null || countMap.isEmpty()) { | ||
globalMap.remove(key); | ||
return; | ||
} | ||
countMap.forEach( | ||
(key1, counter) -> { | ||
Map<VulnerabilityType, Integer> globalCountMap = globalMap.get(key); | ||
if (globalCountMap != null) { | ||
Integer globalCounter = globalCountMap.getOrDefault(key1, 0); | ||
if (counter > globalCounter) { | ||
globalCountMap.put(key1, counter); | ||
} | ||
} else { | ||
globalCountMap = new HashMap<>(); | ||
globalCountMap.put(key1, counter); | ||
globalMap.put(key, globalCountMap); | ||
} | ||
}); | ||
}); | ||
keys.clear(); | ||
requestMap.clear(); | ||
copyMap.clear(); | ||
} | ||
|
||
public boolean isGlobal() { | ||
return isGlobal; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
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; | ||
|
@@ -12,7 +14,11 @@ | |
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.HashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import javax.annotation.Nullable; | ||
|
@@ -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,74 @@ 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) { | ||
Object methodTag = span.getLocalRootSpan().getTag(Tags.HTTP_METHOD); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Get local root span to a variable first, reuse that from there. |
||
method = (methodTag == null) ? "" : methodTag.toString(); | ||
Object routeTag = span.getLocalRootSpan().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 type the type of vulnerability detected | ||
* @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; | ||
} | ||
|
||
String currentKey = httpMethod + " " + httpPath; | ||
Set<String> keys = ctx.requestMap.keySet(); | ||
|
||
if (!keys.contains(currentKey)) { | ||
ctx.copyMap.put(currentKey, globalMap.getOrDefault(currentKey, new HashMap<>())); | ||
} | ||
|
||
ctx.requestMap.computeIfAbsent(currentKey, k -> new HashMap<>()); | ||
|
||
Integer counter = ctx.requestMap.get(currentKey).getOrDefault(type, 0); | ||
ctx.requestMap.get(currentKey).put(type, counter + 1); | ||
|
||
Integer storedCounter = 0; | ||
Map<VulnerabilityType, Integer> copyCountMap = ctx.copyMap.get(currentKey); | ||
if (copyCountMap != null) { | ||
storedCounter = copyCountMap.getOrDefault(type, 0); | ||
} | ||
|
||
return counter < storedCounter; | ||
} | ||
|
||
@Nullable | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,11 @@ private void onCookies(final List<Cookie> cookies) { | |
return; | ||
} | ||
final AgentSpan span = AgentTracer.activeSpan(); | ||
if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) { | ||
// TODO decide if we remove this one quota for all vulnerabilities as new IAST sampling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @smola What do you think about this? We had previously implemented it so that only one quota would be consumed for all header/cookie-related vulnerabilities reported here. The new algorithm would fix this behavior, but maybe it’s worth keeping the existing logic to ensure these vulns (and any future ones like them) are reported sooner. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think we should probably preserve the behavior that all header/cookie vulns are a single one for quota purposes. |
||
// 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()); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need a cast?
getTaintedObjects()
should returnTaintedObjects
or a subclass of it already?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this new constructor
in IastContext we have
with
public IastRequestContext(final TaintedObjects taintedObjects)
andpublic IastRequestContext(final OverheadContext overheadContext)
we need to cast to specify which constructor should be useThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rollback those changes thanks to a different approach