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) {
); } -export const FullScreenDialog = React.memo( - ({ key, responseBody, requestBody, closeRequestBody }) => { - const classes = useStyles(); - const [open, setOpen] = React.useState(true); - - const handleClickOpen = () => { - setOpen(true); - }; - const handleClose = () => { - closeRequestBody(); - setOpen(false); - }; +export default function FullScreenDialog({ + responseBody, + requestBody, + closeRequestBody, + requestVariables, +}) { + const classes = useStyles(); + const [open, setOpen] = React.useState(true); - const [value, setValue] = React.useState(0); + const handleClose = () => { + closeRequestBody(); + setOpen(false); + }; - const handleChange = (event, newValue) => { - setValue(newValue); - }; + const [value, setValue] = React.useState(0); - return ( -
- - - - - - - - - - + const handleChange = (event, newValue) => { + setValue(newValue); + }; - {/* */} - - - - - {JSON.stringify(responseBody, null, 2)} - - + return ( +
+ + + + + + + + + {requestVariables ? : null} + + {} + + + + + + {requestBody} + + + {requestVariables ? ( - {JSON.stringify(requestBody, null, 2)} + {requestVariables} - -
- ); - } -); + ) : null} + + + {JSON.stringify(responseBody, null, 2)} + + +
+
+ ); +}