diff --git a/instrumentation-security/graphql-java-16.2/src/main/java/graphql/ParseAndValidate_Instrumentation.java b/instrumentation-security/graphql-java-16.2/src/main/java/graphql/ParseAndValidate_Instrumentation.java deleted file mode 100644 index 84c3ddf2b..000000000 --- a/instrumentation-security/graphql-java-16.2/src/main/java/graphql/ParseAndValidate_Instrumentation.java +++ /dev/null @@ -1,9 +0,0 @@ -package graphql; - -import com.newrelic.api.agent.weaver.MatchType; -import com.newrelic.api.agent.weaver.Weave; - -@Weave(originalName = "graphql.ParseAndValidate", type = MatchType.ExactClass) -public class ParseAndValidate_Instrumentation { - -} diff --git a/instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java b/instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java new file mode 100644 index 000000000..2ce120fbc --- /dev/null +++ b/instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java @@ -0,0 +1,32 @@ +package graphql.execution; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.HttpRequestCustomDataTypeEnum; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import graphql.ExecutionInput; +import graphql.ExecutionResult; + +import java.util.concurrent.CompletableFuture; + +@Weave(type = MatchType.BaseClass, originalName = "graphql.execution.ExecutionStrategy") +public class ExecutionStrategy_Instrumentation { + + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + try { + if (NewRelicSecurity.isHookProcessingActive()) { + HttpRequest request = NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(); + ExecutionInput executionInput = executionContext.getExecutionInput(); + if (executionInput.getQuery() != null && !executionInput.getQuery().isEmpty()) { + request.getCustomDataType().put("*.query", HttpRequestCustomDataTypeEnum.GRAPHQL_QUERY.name()); + } + if (executionInput.getVariables() != null && !executionInput.getVariables().isEmpty()) { + request.getCustomDataType().put("*.variables", HttpRequestCustomDataTypeEnum.GRAPHQL_VARIABLE.name()); + } + } + } catch (Exception ignored) {} + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/graphql-java-16.2/src/main/java/graphql/GraphQL_Instrumentation.java b/instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/Execution_Instrumentation.java similarity index 75% rename from instrumentation-security/graphql-java-16.2/src/main/java/graphql/GraphQL_Instrumentation.java rename to instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/Execution_Instrumentation.java index bf6f1bbe8..7f1908d69 100644 --- a/instrumentation-security/graphql-java-16.2/src/main/java/graphql/GraphQL_Instrumentation.java +++ b/instrumentation-security/graphql-java-16.2/src/main/java/graphql/execution/Execution_Instrumentation.java @@ -1,4 +1,4 @@ -package graphql; +package graphql.execution; import com.newrelic.api.agent.security.NewRelicSecurity; import com.newrelic.api.agent.security.schema.HttpRequest; @@ -6,16 +6,18 @@ import com.newrelic.api.agent.weaver.MatchType; import com.newrelic.api.agent.weaver.Weave; import com.newrelic.api.agent.weaver.Weaver; +import graphql.ExecutionInput; +import graphql.ExecutionResult; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; import graphql.schema.GraphQLSchema; import java.util.concurrent.CompletableFuture; -@Weave(originalName = "graphql.GraphQL", type = MatchType.ExactClass) -public class GraphQL_Instrumentation { +@Weave(originalName = "graphql.execution.Execution", type = MatchType.ExactClass) +public class Execution_Instrumentation { - private CompletableFuture execute(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { + public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState) { try { if (NewRelicSecurity.isHookProcessingActive()) { HttpRequest request = NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(); diff --git a/instrumentation-security/graphql-java-16.2/src/test/java/com/nr/agent/security/instrumentation/graphql/GraphQLTest.java b/instrumentation-security/graphql-java-16.2/src/test/java/com/nr/agent/security/instrumentation/graphql/GraphQLTest.java new file mode 100644 index 000000000..9f9f0f969 --- /dev/null +++ b/instrumentation-security/graphql-java-16.2/src/test/java/com/nr/agent/security/instrumentation/graphql/GraphQLTest.java @@ -0,0 +1,237 @@ +package com.nr.agent.security.instrumentation.graphql; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.HttpRequestCustomDataTypeEnum; +import com.newrelic.security.test.marker.Java8IncompatibleTest; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.execution.AsyncExecutionStrategy; +import graphql.execution.DefaultValueUnboxer; +import graphql.execution.Execution; +import graphql.execution.ExecutionContextBuilder; +import graphql.execution.ExecutionId; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.ExecutionStrategy; +import graphql.execution.ExecutionStrategyParameters; +import graphql.execution.MergedField; +import graphql.execution.SubscriptionExecutionStrategy; +import graphql.execution.ValueUnboxer; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitor; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.schema.idl.TypeRuntimeWiring; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import org.dataloader.DataLoaderRegistry; +import org.junit.After; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "graphql" }) +@Category({ Java8IncompatibleTest.class }) +public class GraphQLTest { + + private static final String TEST_ARG = "testArg"; + + private static GraphQL graphQL; + private static GraphQLSchema graphQLSchema; + + @BeforeClass + public static void initialize() { + String schema = "type Query{hello(" + TEST_ARG + ": String): String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", + new StaticDataFetcher("world"))) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + } + + @After + public void cleanUp() { + SecurityInstrumentationTestRunner.getIntrospector().clear(); + } + + @Test + public void testQueryWithNoArg() { + trace(createRunnable("{hello}")); + assertCustomDataType(); + } + + @Test + public void testQueryWithArg() { + trace(createRunnable("{hello (" + TEST_ARG + ": \"fo)o\")}")); + assertCustomDataType(); + } + + @Test + public void testQueryWithVariables() throws ExecutionException, InterruptedException { + // Graphql query with variables + ExecutionInput executionInput = ExecutionInput.newExecutionInput().executionId(ExecutionId.generate()).query("{hello($arg: String!)}").variables(Collections.singletonMap("arg", "world")).build(); + Instrumentation instrumentation = new SimpleInstrumentation(); + InstrumentationState instrumentationState = instrumentation.createState(new InstrumentationCreateStateParameters(graphQLSchema, executionInput)); + + Execution execution = new Execution(new AsyncExecutionStrategy(), new AsyncExecutionStrategy(), new SubscriptionExecutionStrategy(), instrumentation, new DefaultValueUnboxer()); + ExecutionId executionId = executionInput.getExecutionId(); + CompletableFuture ans = execution.execute(Document.newDocument() + .definition(OperationDefinition.newOperationDefinition() + .selectionSet(SelectionSet.newSelectionSet().build()) + .operation(OperationDefinition.Operation.QUERY).build()) + .build(), + graphQLSchema, + executionId, + executionInput, + instrumentationState + ); + ans.get(); + assertCustomDataTypes(); + } + + @Test + public void testQueryWithoutVariables() throws ExecutionException, InterruptedException { + // Graphql query without variables + ExecutionInput executionInput = ExecutionInput.newExecutionInput().executionId(ExecutionId.generate()).query("{hello}").build(); + Instrumentation instrumentation = new SimpleInstrumentation(); + InstrumentationState instrumentationState = instrumentation.createState(new InstrumentationCreateStateParameters(graphQLSchema, executionInput)); + + Execution execution = new Execution(new AsyncExecutionStrategy(), new AsyncExecutionStrategy(), new SubscriptionExecutionStrategy(), instrumentation, new DefaultValueUnboxer()); + ExecutionId executionId = executionInput.getExecutionId(); + CompletableFuture ans = execution.execute(Document.newDocument() + .definition(OperationDefinition.newOperationDefinition() + .selectionSet(SelectionSet.newSelectionSet().build()) + .operation(OperationDefinition.Operation.QUERY).build()) + .build(), + graphQLSchema, + executionId, + executionInput, + instrumentationState + ); + ans.get(); + assertCustomDataType(); + } + + @Test + public void testParsingException() { + //when + trace(createRunnable("cause a parse error")); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + HttpRequest request = introspector.getSecurityMetaData().getRequest(); + Assert.assertTrue(request.getCustomDataType().isEmpty()); + } + + @Test + public void validationException() { + trace(createRunnable("{noSuchField}")); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + HttpRequest request = introspector.getSecurityMetaData().getRequest(); + Assert.assertTrue(request.getCustomDataType().isEmpty()); + } + + @Test + public void testResolverException() { + //given + String query = "{hello " + + "\n" + + "bye}"; + + //when + trace(createRunnable(query, graphWithResolverException())); + assertCustomDataType(); + } + + @Trace(dispatcher = true) + private void trace(Runnable runnable) { + runnable.run(); + } + + private Runnable createRunnable(final String query) { + return () -> graphQL.execute(query); + } + + private Runnable createRunnable(final String query, GraphQL graphql) { + return () -> graphql.execute(query); + } + + private GraphQL graphWithResolverException() { + String schema = "type Query{hello(" + TEST_ARG + ": String): String" + + "\n" + + "bye: String!}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(TypeRuntimeWiring.newTypeWiring("Query") + .dataFetcher("hello", environment -> { + throw new RuntimeException("waggle"); + }) + .dataFetcher("bye", environment -> null) + ) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + return GraphQL.newGraphQL(graphQLSchema).build(); + } + + + private void assertCustomDataType() { + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + HttpRequest request = introspector.getSecurityMetaData().getRequest(); + Assert.assertFalse(request.getCustomDataType().isEmpty()); + Assert.assertEquals(Collections.singletonMap("*.query", HttpRequestCustomDataTypeEnum.GRAPHQL_QUERY.name()), request.getCustomDataType()); + } + + private void assertCustomDataTypes() { + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + HttpRequest request = introspector.getSecurityMetaData().getRequest(); + Assert.assertFalse(request.getCustomDataType().isEmpty()); + Map expectedCustomDataType = new HashMap<>(); + expectedCustomDataType.put("*.query", HttpRequestCustomDataTypeEnum.GRAPHQL_QUERY.name()); + expectedCustomDataType.put("*.variables", HttpRequestCustomDataTypeEnum.GRAPHQL_VARIABLE.name()); + + Assert.assertEquals(expectedCustomDataType, request.getCustomDataType()); + } +} diff --git a/instrumentation-security/graphql-java-23.0/build.gradle b/instrumentation-security/graphql-java-23.0/build.gradle new file mode 100644 index 000000000..c99691331 --- /dev/null +++ b/instrumentation-security/graphql-java-23.0/build.gradle @@ -0,0 +1,29 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.graphql-java:graphql-java:23.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.graphql-java-23.0' } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +verifyInstrumentation { + passesOnly('com.graphql-java:graphql-java:[23.0,)') + fails('com.graphql-java:graphql-java:[16.0,23.0)') + excludeRegex('com.graphql-java:graphql-java:(0.0.0|201|202).*') + excludeRegex('com.graphql-java:graphql-java:.*(vTEST|-beta|-alpha1|-nf-execution|-rc|-TEST).*') + exclude('com.graphql-java:graphql-java:15.0') +} + +site { + title 'GraphQL Java' + type 'Framework' +} diff --git a/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java b/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java new file mode 100644 index 000000000..2ce120fbc --- /dev/null +++ b/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/ExecutionStrategy_Instrumentation.java @@ -0,0 +1,32 @@ +package graphql.execution; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.HttpRequestCustomDataTypeEnum; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import graphql.ExecutionInput; +import graphql.ExecutionResult; + +import java.util.concurrent.CompletableFuture; + +@Weave(type = MatchType.BaseClass, originalName = "graphql.execution.ExecutionStrategy") +public class ExecutionStrategy_Instrumentation { + + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + try { + if (NewRelicSecurity.isHookProcessingActive()) { + HttpRequest request = NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(); + ExecutionInput executionInput = executionContext.getExecutionInput(); + if (executionInput.getQuery() != null && !executionInput.getQuery().isEmpty()) { + request.getCustomDataType().put("*.query", HttpRequestCustomDataTypeEnum.GRAPHQL_QUERY.name()); + } + if (executionInput.getVariables() != null && !executionInput.getVariables().isEmpty()) { + request.getCustomDataType().put("*.variables", HttpRequestCustomDataTypeEnum.GRAPHQL_VARIABLE.name()); + } + } + } catch (Exception ignored) {} + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/Execution_Instrumentation.java b/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/Execution_Instrumentation.java new file mode 100644 index 000000000..2a31f2333 --- /dev/null +++ b/instrumentation-security/graphql-java-23.0/src/main/java/graphql/execution/Execution_Instrumentation.java @@ -0,0 +1,34 @@ +package graphql.execution; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.HttpRequestCustomDataTypeEnum; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import graphql.EngineRunningState; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.language.Document; +import graphql.schema.GraphQLSchema; + +import java.util.concurrent.CompletableFuture; + +@Weave(originalName = "graphql.execution.Execution", type = MatchType.ExactClass) +public class Execution_Instrumentation { + + public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState, EngineRunningState engineRunningState) { try { + if (NewRelicSecurity.isHookProcessingActive()) { + HttpRequest request = NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(); + if (executionInput.getQuery() != null && !executionInput.getQuery().isEmpty()) { + request.getCustomDataType().put("*.query", HttpRequestCustomDataTypeEnum.GRAPHQL_QUERY.name()); + } + if (executionInput.getVariables() != null && !executionInput.getVariables().isEmpty()) { + request.getCustomDataType().put("*.variables", HttpRequestCustomDataTypeEnum.GRAPHQL_VARIABLE.name()); + } + } + } catch (Exception ignored) {} + return Weaver.callOriginal(); + } +} diff --git a/settings.gradle b/settings.gradle index afec08f66..0cc4f97c2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -217,6 +217,7 @@ include 'instrumentation:solr-7.0.0' include 'instrumentation:solr-8.0.0' include 'instrumentation:solr-9.0.0' include 'instrumentation:graphql-java-16.2' +include 'instrumentation:graphql-java-23.0' include 'instrumentation:websphere-liberty-profile-environment-8.5.5.5' include 'instrumentation:http4s-blaze-server-2.12_0.21' include 'instrumentation:http4s-blaze-server-2.12_0.22'