├── .gitignore
├── README.md
├── pom.xml
└── src
├── burp
└── BurpExtender.java
└── main
└── java
├── BurpSuiteGraphQLHistory.java
├── ExtensionState.java
├── GraphQLHistoryEvent.java
├── HistoryView.form
├── HistoryView.java
├── MessageEditorController.java
└── ProxyListener.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /target/
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BurpGraphQLViewer
2 | This extension provides a central location for viewing all GraphQL requests/responses within a Burp project. It provides a clean UI that groups all requests by "operationName" and for each GraphQL request shows a pretty printed view of the query and the raw Burp Suite Request/Response.
3 |
4 | # Example UI
5 | 
6 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.staticflow
8 | BurpGraphQLHistory
9 | 1.0-SNAPSHOT
10 |
11 |
12 | net.portswigger.burp.extender
13 | burp-extender-api
14 | 2.3
15 |
16 |
17 | org.apache.commons
18 | commons-lang3
19 | 3.12.0
20 |
21 |
22 | com.graphql-java
23 | graphql-java
24 | 17.3
25 |
26 |
27 |
28 | 8
29 | 8
30 |
31 |
32 | ${project.basedir}/src
33 | BurpSuiteGraphQLHistory
34 |
35 |
36 |
37 |
38 | org.apache.maven.plugins
39 | maven-eclipse-plugin
40 | 2.9
41 |
42 | true
43 | false
44 |
45 |
46 |
47 |
48 |
49 | org.apache.maven.plugins
50 | maven-compiler-plugin
51 | 2.3.2
52 |
53 | 1.8
54 | 1.8
55 |
56 |
57 |
58 |
59 |
60 | org.apache.maven.plugins
61 | maven-assembly-plugin
62 | 2.4.1
63 |
64 |
65 | false
66 |
67 | jar-with-dependencies
68 |
69 |
70 |
71 |
72 |
73 | make-assembly
74 |
75 | package
76 |
77 | single
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/burp/BurpExtender.java:
--------------------------------------------------------------------------------
1 | package burp;
2 |
3 | import main.java.BurpSuiteGraphQLHistory;
4 |
5 | //See @BurpSuiteGraphQLHistory for extension functionality
6 | public class BurpExtender extends BurpSuiteGraphQLHistory {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/BurpSuiteGraphQLHistory.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import burp.*;
4 |
5 | import javax.swing.*;
6 | import java.awt.*;
7 | import java.nio.charset.StandardCharsets;
8 |
9 | /*
10 | This extension collects all previous and future GraphQL requests and displays them in a separate tab for easy viewing.
11 | The graphql requests are grouped by Operation and each request for that operation is shown in a detailed view to the side
12 | which contains the pretty printed GraphQL request, along with the request/response tabs.
13 | */
14 | public class BurpSuiteGraphQLHistory implements IBurpExtender,
15 | IExtensionStateListener, ITab {
16 | @Override
17 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
18 | ExtensionState.getInstance().setCallbacks(callbacks);
19 | ExtensionState.getInstance().setHistoryUI(new HistoryView());
20 | callbacks.registerExtensionStateListener(this);
21 | callbacks.addSuiteTab(this);
22 | callbacks.setExtensionName("BurpSuiteGraphQLHistory");
23 | callbacks.registerProxyListener(ExtensionState.getInstance().getProxyListener());
24 | new Thread(this::searchExistingHistoryForGraphQLRequests).start();
25 |
26 | }
27 |
28 | //Searches the Target History map for any GraphQL requests that happened before the extension was loaded
29 | private void searchExistingHistoryForGraphQLRequests() {
30 | for(IHttpRequestResponse request : ExtensionState.getInstance().getCallbacks().getProxyHistory() ) {
31 | ExtensionState.getInstance().parseRequestForGraphQLContent(request);
32 | }
33 | }
34 |
35 | //Removes the proxy listener and extension tab when the extension is removed
36 | @Override
37 | public void extensionUnloaded() {
38 | ExtensionState.getInstance().getCallbacks().removeProxyListener(ExtensionState.getInstance().getProxyListener());
39 | ExtensionState.getInstance().getCallbacks().removeSuiteTab(this);
40 | }
41 |
42 | @Override
43 | public String getTabCaption() {
44 | return "GraphQL History";
45 | }
46 |
47 | @Override
48 | public Component getUiComponent() {
49 | return ExtensionState.getInstance().getHistoryUI();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/ExtensionState.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import burp.IBurpExtenderCallbacks;
4 | import burp.IHttpRequestResponse;
5 | import burp.IParameter;
6 | import burp.IRequestInfo;
7 | import graphql.language.Field;
8 | import graphql.language.OperationDefinition;
9 | import graphql.parser.Parser;
10 |
11 | import javax.swing.*;
12 | import java.awt.*;
13 | import java.util.*;
14 |
15 |
16 | /*
17 | Static singleton class for keeping extension state.
18 | */
19 | public class ExtensionState {
20 |
21 | //reference to this state object
22 | private static ExtensionState state = null;
23 | //reference to the Burp callbacks
24 | private IBurpExtenderCallbacks callbacks;
25 | //map of all GraphQL requests, keys are "Operations", values are the request data
26 | private final HashMap> graphQLHistoryEvents;
27 | //reference to the Burp Proxy Listener that catches GraphQL requests
28 | private final ProxyListener httpProxyListener;
29 | //reference to this extensions custom Tab UI
30 | private HistoryView historyUI;
31 |
32 |
33 | private ExtensionState() {
34 | graphQLHistoryEvents = new HashMap<>();
35 | httpProxyListener = new ProxyListener();
36 |
37 | }
38 |
39 | public static ExtensionState getInstance() {
40 | if (state == null)
41 | state = new ExtensionState();
42 | return state;
43 | }
44 |
45 | public IBurpExtenderCallbacks getCallbacks() {
46 | return callbacks;
47 | }
48 |
49 | public void setCallbacks(IBurpExtenderCallbacks callbacks) {
50 | this.callbacks = callbacks;
51 | }
52 |
53 | public Map> getGraphQLHistoryEventsMap() {
54 | return graphQLHistoryEvents;
55 | }
56 |
57 | /*
58 | Inserts a new GraphQL requests into the map of current GraphQL requests
59 | */
60 | public void addEventToGraphQLHistoryEventsMap(GraphQLHistoryEvent event, String operationName) {
61 | //if we have seen this operation before append the new event, else create a new entry with the new event
62 | ArrayList operationEvents = graphQLHistoryEvents.computeIfAbsent(operationName, k -> new ArrayList<>());
63 | operationEvents.add(event);
64 | if( operationEvents.size() == 1) {
65 | new SwingWorker() {
66 | @Override
67 | public Boolean doInBackground() {
68 | ((DefaultListModel)historyUI.getOperationsListModel()).addElement(operationName);
69 | return Boolean.TRUE;
70 | }
71 | }.execute();
72 | } else {
73 | historyUI.updateOperationsTable(operationName);
74 | }
75 | }
76 |
77 | public ProxyListener getProxyListener() {
78 | return httpProxyListener;
79 | }
80 |
81 | public Component getHistoryUI() {
82 | return historyUI.$$$getRootComponent$$$();
83 | }
84 |
85 | public void setHistoryUI(HistoryView historyUI) {
86 | this.historyUI = historyUI;
87 | }
88 |
89 | /*
90 | This method extracts the operation name from the GraphQL query body by first checking is the "operationName" field is set and if not, uses the first
91 | statement selection as the operation name.
92 | */
93 | private String parseGraphQLGetRequest(String query) {
94 | OperationDefinition operation;
95 | try {
96 | //Attempt to parse the GraphQL query
97 | operation = (OperationDefinition) (new Parser().parseDocument(query).getDefinitions().get(0));
98 | //check if "operationName" is not set, get the name of the first selection
99 | if (operation.getName() == null || Objects.equals(operation.getName(), "")) {
100 | return ((Field) operation.getSelectionSet().getSelections().get(0)).getName();
101 | } else {
102 | //return the "operationName"
103 | return operation.getName();
104 | }
105 | } catch (Exception e) {
106 | getCallbacks().printError(e.getMessage());
107 | return "";
108 | }
109 | }
110 |
111 | /*
112 | Parse potential Proxy Request to determine if it's a GraphQL request following the steps below:
113 | 1. Check if the URL contains the string "graphql" (This may be lossy or over-zealous) There's no universal GraphQL endpoint scheme
114 | 2. Try and extract the query and operationName from the request.
115 | 3. Parse the query if the "operationName" is not set.
116 | 4. If an "operationName" was found, add it to the GraphQL history map.
117 | */
118 | public void parseRequestForGraphQLContent(IHttpRequestResponse proxyRequestResponse) {
119 | IRequestInfo requestInfo = ExtensionState.getInstance().getCallbacks().getHelpers().analyzeRequest(proxyRequestResponse);
120 | //If url for request is a graphql endpoint
121 | if (requestInfo.getUrl().toString().contains("graphql")) {
122 | String operationName = "";
123 | String query = "";
124 | //Try and extract our parameters if they exist
125 | for (IParameter p : requestInfo.getParameters()) {
126 | switch (p.getName()) {
127 | case "query":
128 | query = getCallbacks().getHelpers().urlDecode(p.getValue()).replaceAll("\\\\n","");
129 | break;
130 | case "operationName":
131 | operationName = p.getValue();
132 | break;
133 | default:
134 | //ignore any other query params
135 | break;
136 | }
137 | }
138 |
139 | if (query.length() == 0) {
140 | //If the query is empty this is probably not a GraphQL request.
141 | return;
142 | }
143 |
144 | //Burp parses the JSON literal `null` as the string "null" so we have to check for that here
145 | if (operationName.length() == 0 || operationName.equals("null")) {
146 | //If the operationName isn't provided the query is parsed to find it
147 | operationName = parseGraphQLGetRequest(query);
148 | }
149 |
150 | if( operationName.length() != 0 ) {
151 | addEventToGraphQLHistoryEventsMap(
152 | new GraphQLHistoryEvent(
153 | proxyRequestResponse,
154 | query
155 | ),
156 | operationName
157 | );
158 | }
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/GraphQLHistoryEvent.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import burp.IHttpRequestResponse;
4 | import burp.IMessageEditor;
5 | import graphql.language.AstPrinter;
6 | import graphql.parser.Parser;
7 |
8 | import javax.swing.*;
9 | import java.awt.*;
10 |
11 |
12 | /*
13 | This class represents a GraphQL request that was found in the Target History or detected via the Proxy Listener
14 | */
15 | public class GraphQLHistoryEvent {
16 | //The Burp request/response pair
17 | private final IHttpRequestResponse graphQLQueryRequestResponse;
18 | //The extracted GraphQL query
19 | private final String graphQLQuery;
20 | //The detailed view component for viewing this GraphQL Event
21 | private final Component detailedView;
22 |
23 |
24 | public GraphQLHistoryEvent(IHttpRequestResponse graphQLQueryRequestResponse, String graphQLQuery) {
25 | this.graphQLQueryRequestResponse = graphQLQueryRequestResponse;
26 | this.graphQLQuery = graphQLQuery;
27 | this.detailedView = buildDetailedView();
28 | }
29 |
30 | /*
31 | The event detailed view consists of a pretty printed text area containing the GraphQL query and the Request/Response Burp view like in Repeater tabs.
32 | */
33 | private Component buildDetailedView() {
34 | JSplitPane operationDetailView = new JSplitPane();
35 | operationDetailView.setDividerLocation(0.5);
36 | JTextArea graphQLQueryText = new JTextArea(AstPrinter.printAst(new Parser().parseDocument(graphQLQuery)));
37 | graphQLQueryText.setEditable(false);
38 | operationDetailView.setLeftComponent(graphQLQueryText);
39 | JTabbedPane requestResponseEditors = new JTabbedPane();
40 | IMessageEditor requestEditor = ExtensionState.getInstance().getCallbacks().createMessageEditor(
41 | new MessageEditorController(
42 | graphQLQueryRequestResponse.getHttpService(),
43 | graphQLQueryRequestResponse.getRequest(),
44 | graphQLQueryRequestResponse.getResponse()
45 | ),
46 | true);
47 | requestEditor.setMessage(graphQLQueryRequestResponse.getRequest(),true);
48 | requestResponseEditors.addTab("Request",requestEditor.getComponent());
49 | IMessageEditor responseEditor = ExtensionState.getInstance().getCallbacks().createMessageEditor(
50 | new MessageEditorController(
51 | graphQLQueryRequestResponse.getHttpService(),
52 | graphQLQueryRequestResponse.getRequest(),
53 | graphQLQueryRequestResponse.getResponse()
54 | ),
55 | true);
56 | responseEditor.setMessage(graphQLQueryRequestResponse.getResponse(),false);
57 | requestResponseEditors.addTab("Response",responseEditor.getComponent());
58 | operationDetailView.setRightComponent(requestResponseEditors);
59 | return operationDetailView;
60 | }
61 |
62 | @Override
63 | public String toString() {
64 | return "GraphQLHistoryEvent{" +
65 | ", graphQLQueryRequestResponse=" + graphQLQueryRequestResponse +
66 | ", graphQLQuery='" + graphQLQuery + '\'' +
67 | '}';
68 | }
69 |
70 | public Component getDetailedView() {
71 | return detailedView;
72 | }
73 | }
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/main/java/HistoryView.form:
--------------------------------------------------------------------------------
1 |
2 |
47 |
--------------------------------------------------------------------------------
/src/main/java/HistoryView.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import javax.swing.*;
4 | import java.awt.*;
5 | import java.util.ArrayList;
6 |
7 | /*
8 | This is generated from Jetbrains Intellij SwingDesigner. It sets up the UI code for displaying the GraphQLHistoryEvent Model.
9 | */
10 | public class HistoryView {
11 | private JPanel basePanel;
12 | private JList operationsList;
13 | private JPanel operationsPanel;
14 | private JScrollPane operationsListScroller;
15 | private JPanel operationsVewPanel;
16 | private JTabbedPane operationsTable;
17 |
18 | public ListModel getOperationsListModel() {
19 | return operationsList.getModel();
20 | }
21 |
22 | private void createUIComponents() {
23 | operationsList = new JList<>();
24 | operationsList.setModel(new DefaultListModel<>());
25 | operationsList.addListSelectionListener(e -> updateInViewOperationTable());
26 | }
27 |
28 | public void updateOperationsTable(String operationName) {
29 | if (operationsList.getSelectedIndex() != -1 && operationsList.getModel().getElementAt(operationsList.getSelectedIndex()).equals(operationName)) {
30 | updateInViewOperationTable();
31 | }
32 | }
33 |
34 | private void updateInViewOperationTable() {
35 | new SwingWorker() {
36 | @Override
37 | public Boolean doInBackground() {
38 | operationsTable.removeAll();
39 | ArrayList selectedHistoryEventsByOperation = ExtensionState.getInstance().getGraphQLHistoryEventsMap().get(operationsList.getModel().getElementAt(operationsList.getSelectedIndex()));
40 | for (int i = 0; i < selectedHistoryEventsByOperation.size(); i++) {
41 | operationsTable.addTab(String.valueOf(i), selectedHistoryEventsByOperation.get(i).getDetailedView());
42 | }
43 | return Boolean.TRUE;
44 | }
45 | }.execute();
46 | }
47 |
48 |
49 | {
50 | // GUI initializer generated by IntelliJ IDEA GUI Designer
51 | // >>> IMPORTANT!! <<<
52 | // DO NOT EDIT OR ADD ANY CODE HERE!
53 | $$$setupUI$$$();
54 | }
55 |
56 | /**
57 | * Method generated by IntelliJ IDEA GUI Designer
58 | * >>> IMPORTANT!! <<<
59 | * DO NOT edit this method OR call it in your code!
60 | *
61 | * @noinspection ALL
62 | */
63 | private void $$$setupUI$$$() {
64 | createUIComponents();
65 | basePanel = new JPanel();
66 | basePanel.setLayout(new BorderLayout(0, 0));
67 | operationsPanel = new JPanel();
68 | operationsPanel.setLayout(new BorderLayout(0, 0));
69 | basePanel.add(operationsPanel, BorderLayout.WEST);
70 | operationsListScroller = new JScrollPane();
71 | operationsPanel.add(operationsListScroller, BorderLayout.CENTER);
72 | operationsList.setMinimumSize(new Dimension(50, 0));
73 | operationsList.setSelectionMode(0);
74 | operationsListScroller.setViewportView(operationsList);
75 | operationsVewPanel = new JPanel();
76 | operationsVewPanel.setLayout(new BorderLayout(0, 0));
77 | basePanel.add(operationsVewPanel, BorderLayout.CENTER);
78 | operationsTable = new JTabbedPane();
79 | operationsVewPanel.add(operationsTable, BorderLayout.CENTER);
80 | }
81 |
82 | /**
83 | * @noinspection ALL
84 | */
85 | public JComponent $$$getRootComponent$$$() {
86 | return basePanel;
87 | }
88 |
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/MessageEditorController.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import burp.IHttpService;
4 | import burp.IMessageEditorController;
5 |
6 | //This stub is needed for enabling "right click send to Repeater/Intruder/etc" from within the GraphQLHistoryEvent detailed view
7 | public class MessageEditorController implements IMessageEditorController {
8 |
9 | private final IHttpService service;
10 | private final byte[] request;
11 | private final byte[] response;
12 |
13 | public MessageEditorController(IHttpService service, byte[] request, byte[] response) {
14 | this.service = service;
15 | this.request = request;
16 | this.response = response;
17 | }
18 |
19 | @Override
20 | public IHttpService getHttpService() {
21 | return service;
22 | }
23 |
24 | @Override
25 | public byte[] getRequest() {
26 | return request;
27 | }
28 |
29 | @Override
30 | public byte[] getResponse() {
31 | return response;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/ProxyListener.java:
--------------------------------------------------------------------------------
1 | package main.java;
2 |
3 | import burp.IHttpRequestResponse;
4 | import burp.IInterceptedProxyMessage;
5 | import burp.IProxyListener;
6 |
7 | /*
8 | This class listens to all Proxy traffic and parses responses for whether it is in response to a GraphQL request.
9 | */
10 | public class ProxyListener implements IProxyListener {
11 |
12 | @Override
13 | public void processProxyMessage(boolean messageIsRequest, IInterceptedProxyMessage proxyMessage) {
14 | if (!messageIsRequest) {
15 | IHttpRequestResponse proxyRequestResponse = proxyMessage.getMessageInfo();
16 | ExtensionState.getInstance().parseRequestForGraphQLContent(proxyRequestResponse);
17 | }
18 | }
19 |
20 |
21 | }
22 |
--------------------------------------------------------------------------------