diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e261c3..0a94c6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,18 +7,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Types of changes
-* **Added** for new features.
-* **Changed** for changes in existing functionality.
-* **Deprecated** for soon-to-be removed features.
-* **Removed** for now removed features.
-* **Fixed** for any bug fixes.
-* **Security** in case of vulnerabilities.
+- **Added** for new features.
+- **Changed** for changes in existing functionality.
+- **Deprecated** for soon-to-be removed features.
+- **Removed** for now removed features.
+- **Fixed** for any bug fixes.
+- **Security** in case of vulnerabilities.
+
+## v0.2.0 (Beta)
+
+- Categorize GraphQL API calls as either a `QUERY` or a `MUTATION` ([Issue 3](https://github.com/rubrikinc/api-capture-chrome-extension/issues/3))
+- Add support for logging API calls from Polaris ([Issue 1](https://github.com/rubrikinc/api-capture-chrome-extension/issues/1))
+- Format the GraphQL request body output so that it is human readable ([Issue 5](https://github.com/rubrikinc/api-capture-chrome-extension/issues/5))
+- Add the ability to view GraphQL request variables ([Issue 5](https://github.com/rubrikinc/api-capture-chrome-extension/issues/5))
+
## v0.1.0 (Beta)
### Added
-* Monitor network traffic for API calls made from the Rubrik CDM UI and then list each call detected
-* View the Rubrik API calls Response and Request Bodies
+- Monitor network traffic for API calls made from the Rubrik CDM UI and then list each call detected
+- View the Rubrik API calls Response and Request Bodies
## Unreleased
diff --git a/package-lock.json b/package-lock.json
index a24f814..7d7c397 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5937,6 +5937,11 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
},
+ "graphql": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.0.0.tgz",
+ "integrity": "sha512-ZyVO1xIF9F+4cxfkdhOJINM+51B06Friuv4M66W7HzUOeFd+vNzUn4vtswYINPi6sysjf1M2Ri/rwZALqgwbaQ=="
+ },
"growly": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
diff --git a/package.json b/package.json
index 2c4887a..ef20af2 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"@material-ui/core": "^4.9.14",
"@material-ui/icons": "^4.9.1",
"@material-ui/styles": "^4.9.14",
+ "graphql": "^15.0.0",
"jss-rtl": "^0.3.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
diff --git a/src/components/CreatePanels.css b/src/components/CreatePanels.css
index a221def..03727aa 100644
--- a/src/components/CreatePanels.css
+++ b/src/components/CreatePanels.css
@@ -66,6 +66,10 @@ body {
color: #48c4ba;
}
+.query {
+ color: #815ae0;
+}
+
/* API Endpoint */
/* ******************* */
diff --git a/src/components/CreatePanels.js b/src/components/CreatePanels.js
index ee31b86..028f0ce 100644
--- a/src/components/CreatePanels.js
+++ b/src/components/CreatePanels.js
@@ -14,11 +14,13 @@ export default function Panel({
requestBody,
responseTime,
showRequestBody,
+ requestVariables,
}) {
const httpSuccessCodes = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226];
- const handleClick = (id, responseBody, requestBody) => (event) =>
- showRequestBody(id, responseBody, requestBody);
+ const handleClick = (id, responseBody, requestBody, requestVariables) => (
+ event
+ ) => showRequestBody(id, responseBody, requestBody, requestVariables);
return (
@@ -33,9 +35,13 @@ export default function Panel({
}
>
diff --git a/src/components/DevToolsPanel.js b/src/components/DevToolsPanel.js
index 4cc66c9..0ed971f 100644
--- a/src/components/DevToolsPanel.js
+++ b/src/components/DevToolsPanel.js
@@ -1,12 +1,13 @@
import React from "react";
-import Panel from "./CreatePanels";
import HeaderBar from "./AppBar";
+import Panel from "./CreatePanels";
+import FullScreenDialog from "./Dialog";
import "./DevToolsPanel.css";
import "./CreatePanels.css";
-import { FullScreenDialog } from "./Dialog";
+import { parse, print } from "graphql";
// Known API calls that the Rubrik UI uses for internal functionality checks
-const helperApiCalls = [
+const cdmBackgroundApiCalls = [
"/internal/cluster/me/is_registered",
"/internal/cluster/me/is_azure_cloud_only",
"/internal/cluster/me/is_registered",
@@ -30,6 +31,14 @@ const helperApiCalls = [
"/v1/blackout_window",
"/v1/event/latest?limit=15",
"/internal/authorization/effective/roles?principal",
+ "/v1/saml/sso_status",
+];
+
+const polarisBackgroundApiCalls = ["FeatureFlagQuery"];
+
+const combinedBackgroundApiCalls = [
+ ...cdmBackgroundApiCalls,
+ ...polarisBackgroundApiCalls,
];
export default class DevToolsPanel extends React.Component {
@@ -38,7 +47,12 @@ export default class DevToolsPanel extends React.Component {
this.state = {
apiCalls: [],
showRequestBody: false,
- apiDialogContent: { id: null, responseBody: null, requestBody: null },
+ apiDialogContent: {
+ id: null,
+ responseBody: null,
+ requestBody: null,
+ requestVariables: null,
+ },
};
this.handleShowRequestBody = this.handleShowRequestBody.bind(this);
this.handleCloseRequestBody = this.handleCloseRequestBody.bind(this);
@@ -46,23 +60,111 @@ export default class DevToolsPanel extends React.Component {
scrollToBottomRef = React.createRef();
+ shouldBeFilterd = (path) => {
+ let shouldBeFiltered = false;
+ if (combinedBackgroundApiCalls.includes(path)) {
+ shouldBeFiltered = true;
+ }
+
+ try {
+ // Filter /internal/organization/{orgID}
+ if (path.includes("User") || path.includes("Organization%3A")) {
+ shouldBeFiltered = true;
+ }
+ } catch (error) {}
+
+ return shouldBeFiltered;
+ };
+
handleNetworkRequest = (request) => {
let isRubrikApiCall = false;
+ let httpMethod = request.request.method;
+ let path;
+ let requestBody;
+ let requestVariables = null;
for (const header of request.request.headers) {
- // toLowerCase in order to support pre CDM 5.2
- if (header["name"].toLowerCase() === "rk-web-app-request") {
- isRubrikApiCall = true;
+ // Check to see if the site is CDM
+ try {
+ // toLowerCase in order to support pre CDM 5.2
+ if (header["name"].toLowerCase() === "rk-web-app-request") {
+ isRubrikApiCall = true;
+ }
+ } catch (error) {
+ continue;
}
+
+ // Check to see if the site is Polaris
+ try {
+ if (header["name"] === ":authority") {
+ if (
+ header["value"].includes("my.rubrik.com") ||
+ header["value"].includes("my.rubrik-lab.com")
+ ) {
+ isRubrikApiCall = true;
+ }
+ }
+ } catch (error) {
+ continue;
+ }
+ }
+
+ path = request.request.url.split("/api")[1];
+
+ // Additional Polaris specifc filter for items that get past the initial
+ // header check
+ if (
+ request.request.url.includes("publicKeys.json") ||
+ request.request.url.includes("manifest.json") ||
+ !path
+ ) {
+ isRubrikApiCall = false;
+ }
+
+ // Add another layer of more generic checks for endpoints that have may
+ // cluster specific variables included
+ if (request.request.bodySize !== 0) {
+ let requestBodyJSON = JSON.parse(request.request.postData.text);
+ requestBody = JSON.stringify(requestBodyJSON, null, 2);
+ } else {
+ requestBody = JSON.stringify("null", null, 2);
}
- let path = request.request.url.split("/api")[1];
+ try {
+ if (path.includes("graphql")) {
+ // override the default POST http method with either mutation or query
+ if (request.request.bodySize !== 0) {
+ request.request.postData.text.includes("mutation")
+ ? (httpMethod = "mutation")
+ : (httpMethod = "query");
+ }
+
+ let ast = parse(JSON.parse(request.request.postData.text)["query"]);
+ try {
+ path = ast["definitions"][0]["name"]["value"];
+ } catch (error) {}
+
+ try {
+ requestBody = print(ast);
+ } catch (error) {}
+
+ try {
+ requestVariables = JSON.parse(request.request.postData.text)[
+ "variables"
+ ];
+
+ requestVariables = JSON.stringify(requestVariables, null, 2);
+ } catch (error) {
+ // always return a non-null value. this is used down the line for logic
+ // processing
+ requestVariables = JSON.stringify("{}", null, 2);
+ }
+ }
+ } catch (error) {}
// Before logging -- validate the API calls originated from Rubrik
- // and is not in the helperApiCalls list
- if (isRubrikApiCall && !helperApiCalls.includes(path)) {
- // Add another layer of more generic checks for endpoints that have may
- // cluster specific variables included
- if (!path.includes("User")) {
+ // and is not in the shouldBeFilterd list
+ if (isRubrikApiCall && !this.shouldBeFilterd(path)) {
+ try {
request.getContent((content, encoding) => {
this.setState({
apiCalls: [
@@ -70,19 +172,17 @@ export default class DevToolsPanel extends React.Component {
{
id: this.state.apiCalls.length + 1,
status: request.response.status,
- httpMethod: request.request.method,
+ httpMethod: httpMethod,
path: path,
responseTime: request.time,
responseBody: JSON.parse(content),
- requestBody:
- request.request.bodySize !== 0
- ? JSON.parse(request.request.postData.text)
- : null,
+ requestBody: requestBody,
+ requestVariables: requestVariables,
},
],
});
});
- }
+ } catch (error) {}
}
};
@@ -98,12 +198,13 @@ export default class DevToolsPanel extends React.Component {
this.scrollToBottom();
}
- handleShowRequestBody(id, responseBody, requestBody) {
+ handleShowRequestBody(id, responseBody, requestBody, requestVariables) {
this.setState({
apiDialogContent: {
id: id,
responseBody: responseBody,
requestBody: requestBody,
+ requestVariables: requestVariables,
},
showRequestBody: true,
});
@@ -125,10 +226,10 @@ export default class DevToolsPanel extends React.Component {
{this.state.showRequestBody ? (
) : null}
@@ -152,6 +253,7 @@ export default class DevToolsPanel extends React.Component {
requestBody={apiCall["requestBody"]}
responseTime={apiCall["responseTime"]}
showRequestBody={this.handleShowRequestBody}
+ requestVariables={apiCall["requestVariables"]}
/>
);
})}
diff --git a/src/components/Dialog.js b/src/components/Dialog.js
index 594abac..2e6ffdf 100644
--- a/src/components/Dialog.js
+++ b/src/components/Dialog.js
@@ -1,11 +1,7 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
-import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
-import ListItemText from "@material-ui/core/ListItemText";
-import ListItem from "@material-ui/core/ListItem";
-import List from "@material-ui/core/List";
-import Divider from "@material-ui/core/Divider";
+
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
@@ -52,72 +48,77 @@ function TabPanel(props) {