Skip to content

Commit

Permalink
Rubrik API Capture Chrome Extension Beta v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
drew-russell authored May 29, 2020
2 parents 70cc610 + f59962e commit 82fae7b
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 98 deletions.
24 changes: 16 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/components/CreatePanels.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ body {
color: #48c4ba;
}

.query {
color: #815ae0;
}

/* API Endpoint */
/* ******************* */

Expand Down
14 changes: 10 additions & 4 deletions src/components/CreatePanels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div class="list-padding">
Expand All @@ -33,9 +35,13 @@ export default function Panel({
}
>
<ListItem
key={id}
button
onClick={handleClick(id, responseBody, requestBody)}
onClick={handleClick(
id,
responseBody,
requestBody,
requestVariables
)}
>
<div class="list-container list-spacing">
<div className={`requestMethod ${method.toLowerCase()}`}>
Expand Down
144 changes: 123 additions & 21 deletions src/components/DevToolsPanel.js
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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 {
Expand All @@ -38,51 +47,142 @@ 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);
}

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: [
...this.state.apiCalls,
{
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) {}
}
};

Expand All @@ -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,
});
Expand All @@ -125,10 +226,10 @@ export default class DevToolsPanel extends React.Component {
<HeaderBar />
{this.state.showRequestBody ? (
<FullScreenDialog
key={this.state.apiDialogContent["id"]}
responseBody={this.state.apiDialogContent["responseBody"]}
requestBody={this.state.apiDialogContent["requestBody"]}
closeRequestBody={this.handleCloseRequestBody}
requestVariables={this.state.apiDialogContent["requestVariables"]}
/>
) : null}

Expand All @@ -152,6 +253,7 @@ export default class DevToolsPanel extends React.Component {
requestBody={apiCall["requestBody"]}
responseTime={apiCall["responseTime"]}
showRequestBody={this.handleShowRequestBody}
requestVariables={apiCall["requestVariables"]}
/>
);
})}
Expand Down
Loading

0 comments on commit 82fae7b

Please sign in to comment.