Skip to content

Commit

Permalink
Merge pull request #267 from newrelic/feature/api-endpoint/fallback-m…
Browse files Browse the repository at this point in the history
…ech-NR-273607

[NR-273607] Fallback mechanism for route calculation
  • Loading branch information
IshikaDawda authored Jun 22, 2024
2 parents 355db2e + 66463f5 commit ecf40a2
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import com.newrelic.api.agent.security.schema.operation.RXSSOperation;
import com.newrelic.api.agent.security.schema.policy.AgentPolicy;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
Expand All @@ -53,6 +55,7 @@ public class Agent implements SecurityAgent {

public static final String DROPPING_EVENT_AS_IT_WAS_GENERATED_BY_K_2_INTERNAL_API_CALL = "Dropping event as it was generated by agent internal API call : ";
private static final AtomicBoolean firstEventProcessed = new AtomicBoolean(false);
private static final Logger log = LoggerFactory.getLogger(Agent.class);

private AgentInfo info;

Expand Down Expand Up @@ -310,6 +313,17 @@ public void registerOperation(AbstractOperation operation) {
processStackTrace(operation);
// boolean blockNeeded = checkIfBlockingNeeded(operation.getApiID());
// securityMetaData.getMetaData().setApiBlocked(blockNeeded);
HttpRequest request = securityMetaData.getRequest();
// if (StringUtils.isEmpty(request.getRoute())){
Framework frameWork = Framework.UNKNOWN;
if(!securityMetaData.getFuzzRequestIdentifier().getK2Request() && StringUtils.isNotBlank(securityMetaData.getMetaData().getFramework())) {
frameWork = Framework.valueOf(securityMetaData.getMetaData().getFramework());
}
if (!securityMetaData.getFuzzRequestIdentifier().getK2Request() && StringUtils.isEmpty(request.getRoute())){
request.setRoute(getEndpointRoute(StringUtils.substringBefore(request.getUrl(), "?"), frameWork), true);
logger.log(LogLevel.FINEST,"Route detection using Application Endpoint", this.getClass().getName());
}
// }
if (needToGenerateEvent(operation.getApiID())) {
DispatcherPool.getInstance().dispatchEvent(operation, securityMetaData);
if (!firstEventProcessed.get()) {
Expand All @@ -326,6 +340,56 @@ public void registerOperation(AbstractOperation operation) {
}
}
}
private String getEndpointRoute(String uri, Framework framework){
switch (framework){
default: return getEndpointRoute(uri);
}
}

private String getEndpointRoute(String uri) {
List<String> uriSegments = URLMappingsHelper.getSegments(uri);
if (uriSegments.isEmpty()){
return StringUtils.EMPTY;
}
for (RouteSegments routeSegments : URLMappingsHelper.getRouteSegments()) {
int uriSegIdx = 0;
if (StringUtils.equals(routeSegments.getRoute(), uri)){
return routeSegments.getRoute();
}
for (int routeSegIdx = 0; routeSegIdx < routeSegments.getSegments().size(); routeSegIdx++) {
RouteSegment routeSegment = routeSegments.getSegments().get(routeSegIdx);
if (uriSegIdx >= uriSegments.size()){
break;
}
if (routeSegment.isPathParam() || StringUtils.equals(routeSegment.getSegment(), uriSegments.get(uriSegIdx))){
++uriSegIdx;
} else if (routeSegment.isAllowMultipleSegments()) {
// TODO handle * case
// uriSegIdx = jumpRoute(routeSegments.getSegments(), routeSegIdx, uriSegments, uriSegIdx);
} else {
break;
}

if (routeSegIdx == routeSegments.getSegments().size()-1){
return routeSegments.getRoute();
}
}
}
return StringUtils.EMPTY;
}

private int jumpRoute(List<RouteSegment> value, int i1, List<String> uriSegments, int i) {
if (i1 <= value.size()-1 || value.get(i1 + 1).isPathParam()){
return ++i;
}
RouteSegment routeSegment = value.get(i1 + 1);
for (; i < uriSegments.size(); i++) {
if (uriSegments.get(i).equals(routeSegment.getSegment())){
return i;
}
}
return i;
}

private boolean checkIfCSECGeneratedEvent(AbstractOperation operation) {
for (int i = 1, j = 0; i < operation.getStackTrace().length; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.newrelic.api.agent.security.instrumentation.helpers;

import com.newrelic.api.agent.security.schema.RouteSegments;

import java.util.Comparator;

public class RouteComparator implements Comparator<RouteSegments> {


@Override
public int compare(RouteSegments s1, RouteSegments s2) {
int result = Integer.compare(s2.getRoute().length(), s1.getRoute().length());
if(result == 0){
result = s2.getRoute().compareTo(s1.getRoute());
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.newrelic.api.agent.security.instrumentation.helpers;

import com.newrelic.api.agent.security.schema.ApplicationURLMapping;
import com.newrelic.api.agent.security.schema.RouteSegment;
import com.newrelic.api.agent.security.schema.RouteSegments;
import com.newrelic.api.agent.security.schema.StringUtils;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

public class URLMappingsHelper {
Expand All @@ -27,19 +32,64 @@ public static Set<ApplicationURLMapping> getApplicationURLMappings() {
}

private static Set<Integer> handlers = ConcurrentHashMap.newKeySet();

private static Set<RouteSegments> routeSegments = new TreeSet<>(new RouteComparator());
public static Set<Integer> getHandlersHash() {
return handlers;
}

public static Set<RouteSegments> getRouteSegments() {
return routeSegments;
}

public static void addApplicationURLMapping(ApplicationURLMapping mapping) {
if (mapping.getHandler() == null || (mapping.getHandler() != null && !defaultHandlers.contains(mapping.getHandler()))) {
mappings.add(mapping);
generateRouteSegments(mapping.getPath());
}
if (mapping.getHandler() != null){
handlers.add(mapping.getHandler().hashCode());
}
}

private synchronized static void generateRouteSegments(String endpoint) {
try {
List<RouteSegment> segments = new ArrayList<>();
Path uri = Paths.get(endpoint).normalize();
while (uri.getParent() != null){
String path = uri.getFileName().toString();
uri = uri.getParent();
if (StringUtils.equals(path, StringUtils.SEPARATOR)){
continue;
}
RouteSegment routeSegment = new RouteSegment(path, isPathParam(path), false);
segments.add(routeSegment);
}
routeSegments.add(new RouteSegments(endpoint, segments));
} catch (Exception e) {
}
}

private static boolean isPathParam(String path) {
return StringUtils.startsWithAny(path, ":", "$") ||
StringUtils.equals(path,"*") ||
(StringUtils.startsWith(path, "{") && StringUtils.endsWith(path, "}"));
}
private static boolean allowMultiSegments(String path) {
return StringUtils.equals(path, "*");
}

public static List<String> getSegments(String endpoint) {
List<String> segments = new ArrayList<>();
Path uri = Paths.get(endpoint).normalize();
while (uri.getParent() != null) {
String path = uri.getFileName().toString();
uri = uri.getParent();
if (StringUtils.isNotBlank(path) && !StringUtils.equals(path, StringUtils.SEPARATOR)) {
segments.add(path);
}
}
return segments;

public static int getSegmentCount(String path){
Path normalizedPath = Paths.get(StringUtils.prependIfMissing(StringUtils.removeEnd(path, StringUtils.SEPARATOR), StringUtils.SEPARATOR)).normalize();
int i = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public enum Framework {
SPRAY,
SPRAY_HTTP,
SUN_NET_HTTPSERVER,
VERTX
VERTX,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.newrelic.api.agent.security.schema;

public class RouteSegment {
private final String segment;
private final boolean isPathParam;
private final boolean allowMultipleSegments;

public RouteSegment(String segment, boolean isPathParam, boolean allowMultipleSegments) {
this.segment = segment;
this.isPathParam = isPathParam;
this.allowMultipleSegments = allowMultipleSegments;
}

public boolean isPathParam() {
return isPathParam;
}

public String getSegment() {
return segment;
}

public boolean isAllowMultipleSegments() {
return allowMultipleSegments;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.newrelic.api.agent.security.schema;

import java.util.List;
import java.util.Objects;

public class RouteSegments {
private final String route;
private final List<RouteSegment> segments;

public RouteSegments(String route, List<RouteSegment> segments) {
this.route = route;
this.segments = segments;
}

public String getRoute() {
return route;
}

public List<RouteSegment> getSegments() {
return segments;
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof RouteSegments) {
RouteSegments routeSegments = (RouteSegments) obj;
return Objects.equals(this.route, routeSegments.route);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(route);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1316,4 +1316,66 @@ public static String removeEnd(final String str, final String remove) {
return str;
}

/**
* Check if a CharSequence ends with a specified suffix.
*
* <p>{@code null}s are handled without exceptions. Two {@code null}
* references are considered to be equal. The comparison is case-sensitive.</p>
*
* <pre>
* StringUtils.endsWith(null, null) = true
* StringUtils.endsWith(null, "def") = false
* StringUtils.endsWith("abcdef", null) = false
* StringUtils.endsWith("abcdef", "def") = true
* StringUtils.endsWith("ABCDEF", "def") = false
* StringUtils.endsWith("ABCDEF", "cde") = false
* StringUtils.endsWith("ABCDEF", "") = true
* </pre>
*
* @see String#endsWith(String)
* @param str the CharSequence to check, may be null
* @param suffix the suffix to find, may be null
* @return {@code true} if the CharSequence ends with the suffix, case-sensitive, or
* both {@code null}
* @since 2.4
* @since 3.0 Changed signature from endsWith(String, String) to endsWith(CharSequence, CharSequence)
*/
public static boolean endsWith(final CharSequence str, final CharSequence suffix) {
return endsWith(str, suffix, false);
}

/**
* Check if a CharSequence ends with any of the provided case-sensitive suffixes.
*
* <pre>
* StringUtils.endsWithAny(null, null) = false
* StringUtils.endsWithAny(null, new String[] {"abc"}) = false
* StringUtils.endsWithAny("abcxyz", null) = false
* StringUtils.endsWithAny("abcxyz", new String[] {""}) = true
* StringUtils.endsWithAny("abcxyz", new String[] {"xyz"}) = true
* StringUtils.endsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
* StringUtils.endsWithAny("abcXYZ", "def", "XYZ") = true
* StringUtils.endsWithAny("abcXYZ", "def", "xyz") = false
* </pre>
*
* @param sequence the CharSequence to check, may be null
* @param searchStrings the case-sensitive CharSequences to find, may be empty or contain {@code null}
* @see StringUtils#endsWith(CharSequence, CharSequence)
* @return {@code true} if the input {@code sequence} is {@code null} AND no {@code searchStrings} are provided, or
* the input {@code sequence} ends in any of the provided case-sensitive {@code searchStrings}.
* @since 3.0
*/
public static boolean endsWithAny(final CharSequence sequence, final CharSequence... searchStrings) {
if (isEmpty(sequence) || searchStrings == null || searchStrings.length == 0) {
return false;
}
for (final CharSequence searchString : searchStrings) {
if (endsWith(sequence, searchString)) {
return true;
}
}
return false;
}


}

0 comments on commit ecf40a2

Please sign in to comment.