mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
UI: Settings > Integrations tab, Software Vulnerabilities Webhook v. Integration (#4874)
This commit is contained in:
parent
7cb71bc5a8
commit
d885758a6a
BIN
assets/images/icon-jira-24x24@2x.png
Normal file
BIN
assets/images/icon-jira-24x24@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
1
changes/issue-2936-ui-includes-jira-integration
Normal file
1
changes/issue-2936-ui-includes-jira-integration
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Admin users can set jira integrations and software vulnerabilities to jira in the UI
|
@ -197,12 +197,15 @@ describe(
|
|||||||
cy.loginWithCySession("anna@organization.com", "user123#");
|
cy.loginWithCySession("anna@organization.com", "user123#");
|
||||||
cy.visit("/software/manage");
|
cy.visit("/software/manage");
|
||||||
});
|
});
|
||||||
it("allows global admin to update software vulnerability automation", () => {
|
it("allows global admin to create webhook software vulnerability automation", () => {
|
||||||
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
||||||
cy.findByRole("button", { name: /manage automations/i }).click();
|
cy.findByRole("button", {
|
||||||
|
name: /manage automations/i,
|
||||||
|
}).click();
|
||||||
});
|
});
|
||||||
cy.getAttached(".manage-automations-modal").within(() => {
|
cy.getAttached(".manage-automations-modal").within(() => {
|
||||||
cy.getAttached(".fleet-slider").click();
|
cy.getAttached(".fleet-slider").click();
|
||||||
|
cy.getAttached("#webhook-radio-btn").next().click();
|
||||||
});
|
});
|
||||||
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
||||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||||
@ -215,6 +218,7 @@ describe(
|
|||||||
});
|
});
|
||||||
cy.getAttached(".manage-automations-modal").within(() => {
|
cy.getAttached(".manage-automations-modal").within(() => {
|
||||||
cy.getAttached(".fleet-slider--active").should("exist");
|
cy.getAttached(".fleet-slider--active").should("exist");
|
||||||
|
cy.getAttached("#webhook-url").should("exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -187,18 +187,18 @@ describe(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("Manage software page", () => {
|
describe("Manage software page", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => cy.visit("/software/manage"));
|
||||||
cy.loginWithCySession("mary@organization.com", "user123#");
|
it("should restrict global maintainer from 'Manage automations' button", () => {
|
||||||
cy.visit("/software/manage");
|
it("hides manages software automations when all teams selected", () => {
|
||||||
});
|
|
||||||
it("allows maintainer to click 'Manage automations' button", () => {
|
|
||||||
it("manages software automations when all teams selected", () => {
|
|
||||||
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
||||||
cy.getAttached(".Select").within(() => {
|
cy.getAttached(".Select").within(() => {
|
||||||
cy.findByText(/all teams/i).should("exist");
|
cy.findByText(/all teams/i).should("exist");
|
||||||
});
|
});
|
||||||
cy.findByRole("button", { name: /manage automations/i }).click();
|
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
||||||
cy.findByRole("button", { name: /cancel/i }).click();
|
cy.findByRole("button", {
|
||||||
|
name: /manage automations/i,
|
||||||
|
}).should("not.exist");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("hides manage automations button when all teams not selected", () => {
|
it("hides manage automations button when all teams not selected", () => {
|
||||||
|
@ -116,15 +116,13 @@ describe("Premium tier - Admin user", () => {
|
|||||||
});
|
});
|
||||||
describe("Manage software page", () => {
|
describe("Manage software page", () => {
|
||||||
beforeEach(() => cy.visit("/software/manage"));
|
beforeEach(() => cy.visit("/software/manage"));
|
||||||
it("allows global admin to update software vulnerability automation", () => {
|
it("allows global admin to create webhook software vulnerability automation", () => {
|
||||||
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
||||||
cy.getAttached(".Select").within(() => {
|
|
||||||
cy.findByText(/all teams/i).should("exist");
|
|
||||||
});
|
|
||||||
cy.findByRole("button", { name: /manage automations/i }).click();
|
cy.findByRole("button", { name: /manage automations/i }).click();
|
||||||
});
|
});
|
||||||
cy.getAttached(".manage-automations-modal").within(() => {
|
cy.getAttached(".manage-automations-modal").within(() => {
|
||||||
cy.getAttached(".fleet-slider").click();
|
cy.getAttached(".fleet-slider").click();
|
||||||
|
cy.getAttached("#webhook-radio-btn").next().click();
|
||||||
});
|
});
|
||||||
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
||||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||||
@ -137,6 +135,7 @@ describe("Premium tier - Admin user", () => {
|
|||||||
});
|
});
|
||||||
cy.getAttached(".manage-automations-modal").within(() => {
|
cy.getAttached(".manage-automations-modal").within(() => {
|
||||||
cy.getAttached(".fleet-slider--active").should("exist");
|
cy.getAttached(".fleet-slider--active").should("exist");
|
||||||
|
cy.getAttached("#webhook-url").should("exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("hides manage automations button since all teams not selected", () => {
|
it("hides manage automations button since all teams not selected", () => {
|
||||||
|
@ -101,7 +101,7 @@ const EnrollSecretRow = ({
|
|||||||
type={showSecret ? "text" : "password"}
|
type={showSecret ? "text" : "password"}
|
||||||
value={secret.secret}
|
value={secret.secret}
|
||||||
/>
|
/>
|
||||||
{toggleSecretEditorModal && toggleDeleteSecretModal ? (
|
{toggleSecretEditorModal && toggleDeleteSecretModal && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={onEditSecretClick}
|
onClick={onEditSecretClick}
|
||||||
@ -122,7 +122,7 @@ const EnrollSecretRow = ({
|
|||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,8 +13,10 @@
|
|||||||
color: $core-vibrant-red;
|
color: $core-vibrant-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// so tooltips won't be triggered with whitespace
|
||||||
&[data-has-tooltip="true"] {
|
&[data-has-tooltip="true"] {
|
||||||
margin-bottom: $pad-small;
|
margin-bottom: $pad-small;
|
||||||
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
.form-field__label {
|
.form-field__label {
|
||||||
font-size: $x-small;
|
font-size: $x-small;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
// so tooltips won't be triggered with whitespace
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__email-hint {
|
&__email-hint {
|
||||||
|
@ -39,7 +39,7 @@ import {
|
|||||||
|
|
||||||
const baseClass = "app-config-form";
|
const baseClass = "app-config-form";
|
||||||
|
|
||||||
const AppConfigFormFunctional = ({
|
const AppConfigForm = ({
|
||||||
appConfig,
|
appConfig,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
}: IAppConfigFormProps): JSX.Element => {
|
}: IAppConfigFormProps): JSX.Element => {
|
||||||
@ -977,4 +977,4 @@ const AppConfigFormFunctional = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AppConfigFormFunctional;
|
export default AppConfigForm;
|
||||||
|
@ -10,7 +10,7 @@ type InitialStateType = {
|
|||||||
notification: INotification | null;
|
notification: INotification | null;
|
||||||
renderFlash: (
|
renderFlash: (
|
||||||
alertType: "success" | "error" | "warning-filled" | null,
|
alertType: "success" | "error" | "warning-filled" | null,
|
||||||
message: string | null,
|
message: JSX.Element | string | null,
|
||||||
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
) => void;
|
) => void;
|
||||||
hideFlash: () => void;
|
hideFlash: () => void;
|
||||||
@ -57,7 +57,7 @@ const NotificationProvider = ({ children }: Props) => {
|
|||||||
notification: state.notification,
|
notification: state.notification,
|
||||||
renderFlash: (
|
renderFlash: (
|
||||||
alertType: "success" | "error" | "warning-filled" | null,
|
alertType: "success" | "error" | "warning-filled" | null,
|
||||||
message: string | null,
|
message: JSX.Element | string | null,
|
||||||
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
) => {
|
) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IWebhookSoftwareVulnerabilities,
|
IWebhookSoftwareVulnerabilities,
|
||||||
} from "interfaces/webhook";
|
} from "interfaces/webhook";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { IIntegrations } from "./integration";
|
||||||
|
|
||||||
export default PropTypes.shape({
|
export default PropTypes.shape({
|
||||||
org_name: PropTypes.string,
|
org_name: PropTypes.string,
|
||||||
@ -178,6 +179,7 @@ export interface IConfig {
|
|||||||
failing_policies_webhook: IWebhookFailingPolicies;
|
failing_policies_webhook: IWebhookFailingPolicies;
|
||||||
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||||
};
|
};
|
||||||
|
integrations: IIntegrations;
|
||||||
logging: {
|
logging: {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
json: boolean;
|
json: boolean;
|
||||||
|
33
frontend/interfaces/integration.ts
Normal file
33
frontend/interfaces/integration.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export interface IJiraIntegration {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
project_key: string;
|
||||||
|
enable_software_vulnerabilities?: boolean;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraIntegrationIndexed extends IJiraIntegration {
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraIntegrationFormData {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
projectKey: string;
|
||||||
|
enableSoftwareVulnerabilities?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraIntegrationFormErrors {
|
||||||
|
url?: string | null;
|
||||||
|
username?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
projectKey?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIntegrations {
|
||||||
|
jira: IJiraIntegration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IIntegration = IJiraIntegration;
|
@ -59,10 +59,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// so tooltips won't be triggered with whitespace
|
|
||||||
.form-field__label {
|
.form-field__label {
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
top: 22px;
|
top: 22px;
|
||||||
}
|
}
|
||||||
|
373
frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
Normal file
373
frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import React, { useState, useContext, useCallback } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
import { NotificationContext } from "context/notification";
|
||||||
|
import { IConfig } from "interfaces/config";
|
||||||
|
import {
|
||||||
|
IJiraIntegration,
|
||||||
|
IJiraIntegrationIndexed,
|
||||||
|
IJiraIntegrationFormErrors,
|
||||||
|
} from "interfaces/integration";
|
||||||
|
import { IApiError } from "interfaces/errors";
|
||||||
|
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
// @ts-ignore
|
||||||
|
import FleetIcon from "components/icons/FleetIcon";
|
||||||
|
import { DEFAULT_CREATE_INTEGRATION_ERRORS } from "utilities/constants";
|
||||||
|
|
||||||
|
import configAPI from "services/entities/config";
|
||||||
|
|
||||||
|
import TableContainer from "components/TableContainer";
|
||||||
|
import TableDataError from "components/TableDataError";
|
||||||
|
import AddIntegrationModal from "./components/CreateIntegrationModal";
|
||||||
|
import DeleteIntegrationModal from "./components/DeleteIntegrationModal";
|
||||||
|
import EditIntegrationModal from "./components/EditIntegrationModal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateTableHeaders,
|
||||||
|
generateDataSet,
|
||||||
|
} from "./IntegrationsTableConfig";
|
||||||
|
|
||||||
|
const baseClass = "integrations-management";
|
||||||
|
const noIntegrationsClass = "no-integrations";
|
||||||
|
|
||||||
|
const VALIDATION_FAILED_ERROR =
|
||||||
|
"There was a problem with the information you provided.";
|
||||||
|
const BAD_REQUEST_ERROR =
|
||||||
|
"Invalid login credentials or Jira URL. Please correct and try again.";
|
||||||
|
const UNKNOWN_ERROR =
|
||||||
|
"We experienced an error when attempting to connect to Jira. Please try again later.";
|
||||||
|
|
||||||
|
const IntegrationsPage = (): JSX.Element => {
|
||||||
|
const { renderFlash } = useContext(NotificationContext);
|
||||||
|
|
||||||
|
const [showAddIntegrationModal, setShowAddIntegrationModal] = useState(false);
|
||||||
|
const [showDeleteIntegrationModal, setShowDeleteIntegrationModal] = useState(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const [showEditIntegrationModal, setShowEditIntegrationModal] = useState(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
integrationEditing,
|
||||||
|
setIntegrationEditing,
|
||||||
|
] = useState<IJiraIntegrationIndexed>();
|
||||||
|
const [integrationsIndexed, setIntegrationsIndexed] = useState<
|
||||||
|
IJiraIntegrationIndexed[]
|
||||||
|
>();
|
||||||
|
const [backendValidators, setBackendValidators] = useState<{
|
||||||
|
[key: string]: string;
|
||||||
|
}>({});
|
||||||
|
const [
|
||||||
|
createIntegrationError,
|
||||||
|
setCreateIntegrationError,
|
||||||
|
] = useState<IJiraIntegrationFormErrors>(DEFAULT_CREATE_INTEGRATION_ERRORS);
|
||||||
|
const [testingConnection, setTestingConnection] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: integrations,
|
||||||
|
isLoading: isLoadingIntegrations,
|
||||||
|
error: loadingIntegrationsError,
|
||||||
|
refetch: refetchIntegrations,
|
||||||
|
} = useQuery<IConfig, Error, IJiraIntegration[]>(
|
||||||
|
["integrations"],
|
||||||
|
() => configAPI.loadAll(),
|
||||||
|
{
|
||||||
|
select: (data: IConfig) => {
|
||||||
|
return data.integrations.jira;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
const addIndex = data.map((integration, index) => {
|
||||||
|
return { ...integration, index };
|
||||||
|
});
|
||||||
|
setIntegrationsIndexed(addIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleAddIntegrationModal = useCallback(() => {
|
||||||
|
setShowAddIntegrationModal(!showAddIntegrationModal);
|
||||||
|
setBackendValidators({});
|
||||||
|
}, [
|
||||||
|
showAddIntegrationModal,
|
||||||
|
setShowAddIntegrationModal,
|
||||||
|
setBackendValidators,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toggleDeleteIntegrationModal = useCallback(
|
||||||
|
(integration?: IJiraIntegrationIndexed) => {
|
||||||
|
setShowDeleteIntegrationModal(!showDeleteIntegrationModal);
|
||||||
|
integration
|
||||||
|
? setIntegrationEditing(integration)
|
||||||
|
: setIntegrationEditing(undefined);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
showDeleteIntegrationModal,
|
||||||
|
setShowDeleteIntegrationModal,
|
||||||
|
setIntegrationEditing,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleEditIntegrationModal = useCallback(
|
||||||
|
(integration?: IJiraIntegrationIndexed) => {
|
||||||
|
setShowEditIntegrationModal(!showEditIntegrationModal);
|
||||||
|
setBackendValidators({});
|
||||||
|
integration
|
||||||
|
? setIntegrationEditing(integration)
|
||||||
|
: setIntegrationEditing(undefined);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
showEditIntegrationModal,
|
||||||
|
setShowEditIntegrationModal,
|
||||||
|
setIntegrationEditing,
|
||||||
|
setBackendValidators,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCreateSubmit = useCallback(
|
||||||
|
(jiraIntegrationSubmitData: IJiraIntegration[]) => {
|
||||||
|
setTestingConnection(true);
|
||||||
|
configAPI
|
||||||
|
.update({ integrations: { jira: jiraIntegrationSubmitData } })
|
||||||
|
.then(() => {
|
||||||
|
renderFlash(
|
||||||
|
"success",
|
||||||
|
<>
|
||||||
|
Successfully added{" "}
|
||||||
|
<b>
|
||||||
|
{
|
||||||
|
jiraIntegrationSubmitData[
|
||||||
|
jiraIntegrationSubmitData.length - 1
|
||||||
|
].url
|
||||||
|
}
|
||||||
|
</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
setBackendValidators({});
|
||||||
|
toggleAddIntegrationModal();
|
||||||
|
refetchIntegrations();
|
||||||
|
})
|
||||||
|
.catch((createError: { data: IApiError }) => {
|
||||||
|
if (createError.data.message.includes("Validation Failed")) {
|
||||||
|
renderFlash("error", VALIDATION_FAILED_ERROR);
|
||||||
|
}
|
||||||
|
if (createError.data.message.includes("Bad request")) {
|
||||||
|
renderFlash("error", BAD_REQUEST_ERROR);
|
||||||
|
}
|
||||||
|
if (createError.data.message.includes("Unknown Error")) {
|
||||||
|
renderFlash("error", UNKNOWN_ERROR);
|
||||||
|
} else {
|
||||||
|
renderFlash(
|
||||||
|
"error",
|
||||||
|
<>
|
||||||
|
Could not add{" "}
|
||||||
|
<b>
|
||||||
|
{
|
||||||
|
jiraIntegrationSubmitData[
|
||||||
|
jiraIntegrationSubmitData.length - 1
|
||||||
|
].url
|
||||||
|
}
|
||||||
|
</b>
|
||||||
|
. Please try again.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
toggleAddIntegrationModal();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTestingConnection(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[toggleAddIntegrationModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeleteSubmit = useCallback(() => {
|
||||||
|
if (integrationEditing) {
|
||||||
|
integrations?.splice(integrationEditing.index, 1);
|
||||||
|
configAPI
|
||||||
|
.update({ integrations: { jira: integrations } })
|
||||||
|
.then(() => {
|
||||||
|
renderFlash(
|
||||||
|
"success",
|
||||||
|
<>
|
||||||
|
Successfully deleted <b>{integrationEditing.url}</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
renderFlash(
|
||||||
|
"error",
|
||||||
|
<>
|
||||||
|
Could not delete <b>{integrationEditing.url}</b>. Please try
|
||||||
|
again.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refetchIntegrations();
|
||||||
|
toggleDeleteIntegrationModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [integrationEditing, toggleDeleteIntegrationModal]);
|
||||||
|
|
||||||
|
const onEditSubmit = useCallback(
|
||||||
|
(jiraIntegrationSubmitData: IJiraIntegration[]) => {
|
||||||
|
if (integrationEditing) {
|
||||||
|
setTestingConnection(true);
|
||||||
|
configAPI
|
||||||
|
.update({ integrations: { jira: jiraIntegrationSubmitData } })
|
||||||
|
.then(() => {
|
||||||
|
renderFlash(
|
||||||
|
"success",
|
||||||
|
<>
|
||||||
|
Successfully edited{" "}
|
||||||
|
<b>
|
||||||
|
{jiraIntegrationSubmitData[integrationEditing?.index].url}
|
||||||
|
</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
setBackendValidators({});
|
||||||
|
setTestingConnection(false);
|
||||||
|
setShowEditIntegrationModal(false);
|
||||||
|
refetchIntegrations();
|
||||||
|
})
|
||||||
|
.catch((editError: { data: IApiError }) => {
|
||||||
|
if (editError.data.message.includes("Validation Failed")) {
|
||||||
|
renderFlash("error", VALIDATION_FAILED_ERROR);
|
||||||
|
}
|
||||||
|
if (editError.data.message.includes("Bad request")) {
|
||||||
|
renderFlash("error", BAD_REQUEST_ERROR);
|
||||||
|
}
|
||||||
|
if (editError.data.message.includes("Unknown Error")) {
|
||||||
|
renderFlash("error", UNKNOWN_ERROR);
|
||||||
|
} else {
|
||||||
|
renderFlash(
|
||||||
|
"error",
|
||||||
|
<>
|
||||||
|
Could not edit <b>{integrationEditing?.url}</b>. Please try
|
||||||
|
again.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTestingConnection(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[integrationEditing, toggleEditIntegrationModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onActionSelection = (
|
||||||
|
action: string,
|
||||||
|
integration: IJiraIntegrationIndexed
|
||||||
|
): void => {
|
||||||
|
switch (action) {
|
||||||
|
case "edit":
|
||||||
|
toggleEditIntegrationModal(integration);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
toggleDeleteIntegrationModal(integration);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoIntegrationsComponent = () => {
|
||||||
|
return (
|
||||||
|
<div className={`${noIntegrationsClass}`}>
|
||||||
|
<div className={`${noIntegrationsClass}__inner`}>
|
||||||
|
<div className={`${noIntegrationsClass}__inner-text`}>
|
||||||
|
<h1>Set up integrations</h1>
|
||||||
|
<p>
|
||||||
|
Create tickets automatically when Fleet detects new
|
||||||
|
vulnerabilities.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Want to learn more?
|
||||||
|
<a
|
||||||
|
href="https://fleetdm.com/docs/using-fleet/automations"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Read about automations
|
||||||
|
<FleetIcon name="external-link" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="brand"
|
||||||
|
className={`${noIntegrationsClass}__create-button`}
|
||||||
|
onClick={toggleAddIntegrationModal}
|
||||||
|
>
|
||||||
|
Add integration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||||
|
const tableData = integrationsIndexed
|
||||||
|
? generateDataSet(integrationsIndexed)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}`}>
|
||||||
|
<p className={`${baseClass}__page-description`}>
|
||||||
|
Add or edit integrations to create tickets when Fleet detects new
|
||||||
|
vulnerabilities.
|
||||||
|
</p>
|
||||||
|
{loadingIntegrationsError ? (
|
||||||
|
<TableDataError />
|
||||||
|
) : (
|
||||||
|
<TableContainer
|
||||||
|
columns={tableHeaders}
|
||||||
|
data={tableData}
|
||||||
|
isLoading={isLoadingIntegrations}
|
||||||
|
defaultSortHeader={"name"}
|
||||||
|
defaultSortDirection={"asc"}
|
||||||
|
actionButtonText={"Add integration"}
|
||||||
|
actionButtonVariant={"brand"}
|
||||||
|
hideActionButton={!integrations || integrations.length === 0}
|
||||||
|
onActionButtonClick={toggleAddIntegrationModal}
|
||||||
|
resultsTitle={"integrations"}
|
||||||
|
emptyComponent={NoIntegrationsComponent}
|
||||||
|
showMarkAllPages={false}
|
||||||
|
isAllPagesSelected={false}
|
||||||
|
disablePagination
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showAddIntegrationModal && (
|
||||||
|
<AddIntegrationModal
|
||||||
|
onCancel={toggleAddIntegrationModal}
|
||||||
|
onSubmit={onCreateSubmit}
|
||||||
|
backendValidators={backendValidators}
|
||||||
|
integrations={integrations || []}
|
||||||
|
testingConnection={testingConnection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDeleteIntegrationModal && (
|
||||||
|
<DeleteIntegrationModal
|
||||||
|
onCancel={toggleDeleteIntegrationModal}
|
||||||
|
onSubmit={onDeleteSubmit}
|
||||||
|
url={integrationEditing?.url || ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showEditIntegrationModal && (
|
||||||
|
<EditIntegrationModal
|
||||||
|
onCancel={toggleEditIntegrationModal}
|
||||||
|
onSubmit={onEditSubmit}
|
||||||
|
backendValidators={backendValidators}
|
||||||
|
integrations={integrations || []}
|
||||||
|
integrationEditing={integrationEditing}
|
||||||
|
testingConnection={testingConnection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntegrationsPage;
|
@ -0,0 +1,140 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||||
|
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IJiraIntegration,
|
||||||
|
IJiraIntegrationIndexed,
|
||||||
|
} from "interfaces/integration";
|
||||||
|
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||||
|
|
||||||
|
import JiraIcon from "../../../../assets/images/icon-jira-24x24@2x.png";
|
||||||
|
|
||||||
|
interface IHeaderProps {
|
||||||
|
column: {
|
||||||
|
title: string;
|
||||||
|
isSortedDesc: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRowProps {
|
||||||
|
row: {
|
||||||
|
original: IJiraIntegrationIndexed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface ICellProps extends IRowProps {
|
||||||
|
cell: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDropdownCellProps extends IRowProps {
|
||||||
|
cell: {
|
||||||
|
value: IDropdownOption[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDataColumn {
|
||||||
|
title: string;
|
||||||
|
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
||||||
|
accessor: string;
|
||||||
|
Cell:
|
||||||
|
| ((props: ICellProps) => JSX.Element)
|
||||||
|
| ((props: IDropdownCellProps) => JSX.Element);
|
||||||
|
disableHidden?: boolean;
|
||||||
|
disableSortBy?: boolean;
|
||||||
|
sortType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIntegrationTableData extends IJiraIntegration {
|
||||||
|
actions: IDropdownOption[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: cellProps come from react-table
|
||||||
|
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||||
|
const generateTableHeaders = (
|
||||||
|
actionSelectHandler: (
|
||||||
|
value: string,
|
||||||
|
integration: IJiraIntegrationIndexed
|
||||||
|
) => void
|
||||||
|
): IDataColumn[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
Header: "",
|
||||||
|
disableSortBy: true,
|
||||||
|
sortType: "caseInsensitive",
|
||||||
|
accessor: "hosts",
|
||||||
|
Cell: () => <img src={JiraIcon} alt="jira-icon" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
Header: "Name",
|
||||||
|
disableSortBy: true,
|
||||||
|
sortType: "caseInsensitive",
|
||||||
|
accessor: "name",
|
||||||
|
Cell: (cellProps: ICellProps) => (
|
||||||
|
<TextCell value={cellProps.cell.value} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
Header: "",
|
||||||
|
disableSortBy: true,
|
||||||
|
accessor: "actions",
|
||||||
|
Cell: (cellProps: IDropdownCellProps) => (
|
||||||
|
<DropdownCell
|
||||||
|
options={cellProps.cell.value}
|
||||||
|
onChange={(value: string) =>
|
||||||
|
actionSelectHandler(value, cellProps.row.original)
|
||||||
|
}
|
||||||
|
placeholder={"Actions"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: may need current user ID later for permission on actions.
|
||||||
|
const generateActionDropdownOptions = (): IDropdownOption[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
disabled: false,
|
||||||
|
value: "edit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
disabled: false,
|
||||||
|
value: "delete",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanceIntegrationData = (
|
||||||
|
integrations: IJiraIntegrationIndexed[]
|
||||||
|
): IIntegrationTableData[] => {
|
||||||
|
return Object.values(integrations).map((integration) => {
|
||||||
|
return {
|
||||||
|
url: integration.url,
|
||||||
|
username: integration.username,
|
||||||
|
password: integration.password,
|
||||||
|
project_key: integration.project_key,
|
||||||
|
actions: generateActionDropdownOptions(),
|
||||||
|
enable_software_vulnerabilities:
|
||||||
|
integration.enable_software_vulnerabilities,
|
||||||
|
name: `${integration.url} - ${integration.project_key}`,
|
||||||
|
index: integration.index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateDataSet = (
|
||||||
|
integrations: IJiraIntegrationIndexed[]
|
||||||
|
): IIntegrationTableData[] => {
|
||||||
|
return [...enhanceIntegrationData(integrations)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { generateTableHeaders, generateDataSet };
|
116
frontend/pages/admin/IntegrationsPage/_styles.scss
Normal file
116
frontend/pages/admin/IntegrationsPage/_styles.scss
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
.integrations-management {
|
||||||
|
padding: 0;
|
||||||
|
&__page-description {
|
||||||
|
font-size: $x-small;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
@include sticky-settings-description;
|
||||||
|
padding-bottom: $pad-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-details__team-details {
|
||||||
|
.form-field--dropdown {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-details.team-select-open {
|
||||||
|
.component__tabs-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.members {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-integrations {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: $pad-xxxlarge;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: $x-small;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
font-weight: $bold;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $large;
|
||||||
|
font-weight: $regular;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: $small;
|
||||||
|
font-weight: $bold;
|
||||||
|
margin: 0 0 $pad-large;
|
||||||
|
line-height: 20px;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
&::before {
|
||||||
|
content: "•";
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
margin-right: $pad-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $small;
|
||||||
|
font-weight: $bold;
|
||||||
|
margin-bottom: $pad-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 176px;
|
||||||
|
margin-right: $pad-xlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $core-fleet-black;
|
||||||
|
font-weight: $regular;
|
||||||
|
font-size: $x-small;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: $pad-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filter-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner-text {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hosts__cell {
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import Modal from "components/Modal";
|
||||||
|
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||||
|
// @ts-ignore
|
||||||
|
import FleetIcon from "components/icons/FleetIcon";
|
||||||
|
import Spinner from "components/Spinner";
|
||||||
|
import { IJiraIntegration } from "interfaces/integration";
|
||||||
|
import IntegrationForm from "../IntegrationForm";
|
||||||
|
|
||||||
|
const baseClass = "create-integration-modal";
|
||||||
|
|
||||||
|
interface ICreateIntegrationModalProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||||
|
serverErrors?: { base: string; email: string };
|
||||||
|
backendValidators: { [key: string]: string };
|
||||||
|
integrations: IJiraIntegration[];
|
||||||
|
testingConnection: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateIntegrationModal = ({
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
backendValidators,
|
||||||
|
integrations,
|
||||||
|
testingConnection,
|
||||||
|
}: ICreateIntegrationModalProps): JSX.Element => {
|
||||||
|
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||||
|
backendValidators
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErrors(backendValidators);
|
||||||
|
}, [backendValidators]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={"Add integration"} onExit={onCancel} className={baseClass}>
|
||||||
|
{testingConnection ? (
|
||||||
|
<div className={`${baseClass}__testing-connection`}>
|
||||||
|
<b>Testing connection to Jira</b>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||||
|
<p className={`${baseClass}__info-header`}>
|
||||||
|
Fleet supports Jira as a ticket destination.
|
||||||
|
<a
|
||||||
|
href="https://github.com/fleetdm/fleet/issues/new?assignees=&labels=idea&template=feature-request.md&title="
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Suggest a new destination
|
||||||
|
<FleetIcon name="external-link" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</InfoBanner>
|
||||||
|
<IntegrationForm
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
integrations={integrations}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateIntegrationModal;
|
@ -0,0 +1,32 @@
|
|||||||
|
.create-integration-modal {
|
||||||
|
a {
|
||||||
|
font-size: $x-small;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
font-weight: $bold;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sandbox-info {
|
||||||
|
margin: $pad-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info-header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin-top: $pad-xxlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./CreateIntegrationModal";
|
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import Modal from "components/Modal";
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
|
||||||
|
const baseClass = "delete-integration-modal";
|
||||||
|
|
||||||
|
interface IDeleteIntegrationModalProps {
|
||||||
|
url: string;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteIntegrationModal = ({
|
||||||
|
url,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: IDeleteIntegrationModalProps): JSX.Element => {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: KeyboardEvent) => {
|
||||||
|
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
||||||
|
event.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", listener);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={"Delete integration"} onExit={onCancel} className={baseClass}>
|
||||||
|
<form className={`${baseClass}__form`}>
|
||||||
|
<p>
|
||||||
|
This action will delete the{" "}
|
||||||
|
<span className={`${baseClass}__url`}>{url}</span> integration.
|
||||||
|
</p>
|
||||||
|
<p>The automations that use this integration will be turned off.</p>
|
||||||
|
<div className={`${baseClass}__btn-wrap`}>
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
type="button"
|
||||||
|
onClick={onSubmit}
|
||||||
|
variant="alert"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
onClick={onCancel}
|
||||||
|
variant="inverse-alert"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteIntegrationModal;
|
@ -0,0 +1,15 @@
|
|||||||
|
.delete-integration-modal {
|
||||||
|
&__url {
|
||||||
|
font-weight: $bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin-top: $pad-xxlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./DeleteIntegrationModal";
|
@ -0,0 +1,58 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import Modal from "components/Modal";
|
||||||
|
import Spinner from "components/Spinner";
|
||||||
|
// @ts-ignore
|
||||||
|
import {
|
||||||
|
IJiraIntegration,
|
||||||
|
IJiraIntegrationIndexed,
|
||||||
|
} from "interfaces/integration";
|
||||||
|
import IntegrationForm from "../IntegrationForm";
|
||||||
|
|
||||||
|
const baseClass = "edit-team-modal";
|
||||||
|
|
||||||
|
interface IEditIntegrationModalProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||||
|
backendValidators: { [key: string]: string };
|
||||||
|
integrations: IJiraIntegration[];
|
||||||
|
integrationEditing?: IJiraIntegrationIndexed;
|
||||||
|
testingConnection: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditIntegrationModal = ({
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
backendValidators,
|
||||||
|
integrations,
|
||||||
|
integrationEditing,
|
||||||
|
testingConnection,
|
||||||
|
}: IEditIntegrationModalProps): JSX.Element => {
|
||||||
|
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||||
|
backendValidators
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErrors(backendValidators);
|
||||||
|
}, [backendValidators]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={"Edit integration"} onExit={onCancel} className={baseClass}>
|
||||||
|
{testingConnection ? (
|
||||||
|
<div className={`${baseClass}__testing-connection`}>
|
||||||
|
<b>Testing connection to Jira</b>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<IntegrationForm
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
integrations={integrations}
|
||||||
|
integrationEditing={integrationEditing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditIntegrationModal;
|
@ -0,0 +1,11 @@
|
|||||||
|
.edit-team-modal {
|
||||||
|
&__btn-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin-top: $pad-xxlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./EditIntegrationModal";
|
@ -0,0 +1,190 @@
|
|||||||
|
import React, { FormEvent, useState } from "react";
|
||||||
|
import ReactTooltip from "react-tooltip";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IJiraIntegration,
|
||||||
|
IJiraIntegrationFormData,
|
||||||
|
IJiraIntegrationIndexed,
|
||||||
|
} from "interfaces/integration";
|
||||||
|
|
||||||
|
import Button from "components/buttons/Button";
|
||||||
|
// @ts-ignore
|
||||||
|
import InputField from "components/forms/fields/InputField";
|
||||||
|
|
||||||
|
const baseClass = "integration-form";
|
||||||
|
|
||||||
|
interface IIntegrationFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (jiraIntegrationSubmitData: IJiraIntegration[]) => void;
|
||||||
|
integrationEditing?: IJiraIntegrationIndexed;
|
||||||
|
integrations: IJiraIntegration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFormField {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntegrationForm = ({
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
integrationEditing,
|
||||||
|
integrations,
|
||||||
|
}: IIntegrationFormProps): JSX.Element => {
|
||||||
|
const [formData, setFormData] = useState<IJiraIntegrationFormData>({
|
||||||
|
url: integrationEditing?.url || "",
|
||||||
|
username: integrationEditing?.username || "",
|
||||||
|
password: integrationEditing?.password || "",
|
||||||
|
projectKey: integrationEditing?.project_key || "",
|
||||||
|
enableSoftwareVulnerabilities:
|
||||||
|
integrationEditing?.enable_software_vulnerabilities || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, username, password, projectKey } = formData;
|
||||||
|
|
||||||
|
const onInputChange = ({ name, value }: IFormField) => {
|
||||||
|
setFormData({ ...formData, [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// IntegrationForm component can be used to create a new jira integration or edit an existing jira integration so submitData will be assembled accordingly
|
||||||
|
const createSubmitData = (): IJiraIntegration[] => {
|
||||||
|
let jiraIntegrationSubmitData = integrations;
|
||||||
|
|
||||||
|
if (integrationEditing) {
|
||||||
|
// Edit existing integration using array replacement
|
||||||
|
jiraIntegrationSubmitData.splice(integrationEditing.index, 1, {
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
project_key: projectKey,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new integration at end of array
|
||||||
|
jiraIntegrationSubmitData = [
|
||||||
|
...jiraIntegrationSubmitData,
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
project_key: projectKey,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return jiraIntegrationSubmitData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = (evt: FormEvent): void => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
return onSubmit(createSubmitData());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={`${baseClass}__form`}
|
||||||
|
onSubmit={onFormSubmit}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<InputField
|
||||||
|
autofocus
|
||||||
|
name="url"
|
||||||
|
onChange={onInputChange}
|
||||||
|
label="Jira site URL"
|
||||||
|
placeholder="https://jira.example.com"
|
||||||
|
parseTarget
|
||||||
|
value={url}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="username"
|
||||||
|
onChange={onInputChange}
|
||||||
|
label="Jira username"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
parseTarget
|
||||||
|
value={username}
|
||||||
|
tooltip={
|
||||||
|
"\
|
||||||
|
This user must have “Create issues” for the project <br/> \
|
||||||
|
in which the issues are created. \
|
||||||
|
"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="password"
|
||||||
|
onChange={onInputChange}
|
||||||
|
label="Jira password"
|
||||||
|
parseTarget
|
||||||
|
value={password}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="projectKey"
|
||||||
|
onChange={onInputChange}
|
||||||
|
label="Jira project key"
|
||||||
|
placeholder="JRAEXAMPLE"
|
||||||
|
parseTarget
|
||||||
|
value={projectKey}
|
||||||
|
tooltip={
|
||||||
|
"\
|
||||||
|
To find the Jira project key, head to your project in <br /> \
|
||||||
|
Jira. Your project key is in URL. For example, in <br /> \
|
||||||
|
“jira.example.com/projects/JRAEXAMPLE,” <br /> \
|
||||||
|
“JRAEXAMPLE” is your project key. \
|
||||||
|
"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={`${baseClass}__btn-wrap`}>
|
||||||
|
<div
|
||||||
|
data-tip
|
||||||
|
data-for="create-integration-button"
|
||||||
|
data-tip-disable={
|
||||||
|
!(
|
||||||
|
formData.url === "" ||
|
||||||
|
formData.username === "" ||
|
||||||
|
formData.password === "" ||
|
||||||
|
formData.projectKey === ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
type="submit"
|
||||||
|
variant="brand"
|
||||||
|
disabled={
|
||||||
|
formData.url === "" ||
|
||||||
|
formData.username === "" ||
|
||||||
|
formData.password === "" ||
|
||||||
|
formData.projectKey === ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>{" "}
|
||||||
|
<ReactTooltip
|
||||||
|
className={`create-integration-tooltip`}
|
||||||
|
place="bottom"
|
||||||
|
type="dark"
|
||||||
|
effect="solid"
|
||||||
|
backgroundColor="#3e4771"
|
||||||
|
id="create-integration-button"
|
||||||
|
data-html
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`tooltip`}
|
||||||
|
style={{ width: "152px", textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Complete all fields to save the integration
|
||||||
|
</div>
|
||||||
|
</ReactTooltip>
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
onClick={onCancel}
|
||||||
|
variant="inverse"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntegrationForm;
|
@ -0,0 +1,135 @@
|
|||||||
|
.integration-form {
|
||||||
|
a {
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
font-size: $x-small;
|
||||||
|
font-weight: $bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-user-container {
|
||||||
|
margin-top: $pad-large;
|
||||||
|
margin-bottom: $pad-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sso-input {
|
||||||
|
margin-top: $pad-large;
|
||||||
|
margin-bottom: $pad-large;
|
||||||
|
|
||||||
|
.fleet-checkbox {
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: $x-small;
|
||||||
|
font-weight: $bold;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sso-disabled {
|
||||||
|
width: 375px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-disabled {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-tooltip-text {
|
||||||
|
width: 310px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-team {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
color: $core-fleet-black;
|
||||||
|
font-size: $x-small;
|
||||||
|
font-weight: $bold;
|
||||||
|
margin-bottom: $pad-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__user-permissions-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: $pad-xlarge;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 $pad-medium 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected-teams-container {
|
||||||
|
margin-bottom: $pad-xxlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__radio-input {
|
||||||
|
margin-bottom: $pad-medium;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
.radio__control {
|
||||||
|
background-color: $ui-fleet-black-25;
|
||||||
|
}
|
||||||
|
.radio__label {
|
||||||
|
color: $ui-fleet-black-50;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__invite-admin {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__password {
|
||||||
|
width: 98%;
|
||||||
|
float: left;
|
||||||
|
padding: 0 $pad-medium 0 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.input-icon-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
float: right;
|
||||||
|
width: 2%;
|
||||||
|
|
||||||
|
.icon-tooltip {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: $core-fleet-black;
|
||||||
|
|
||||||
|
&--brand {
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sublabel {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip-text {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./IntegrationForm";
|
1
frontend/pages/admin/IntegrationsPage/index.ts
Normal file
1
frontend/pages/admin/IntegrationsPage/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./IntegrationsPage";
|
@ -16,6 +16,10 @@ const settingsSubNav: ISettingSubNavItem[] = [
|
|||||||
name: "Organization settings",
|
name: "Organization settings",
|
||||||
pathname: PATHS.ADMIN_SETTINGS,
|
pathname: PATHS.ADMIN_SETTINGS,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Integrations",
|
||||||
|
pathname: PATHS.ADMIN_INTEGRATIONS,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Users",
|
name: "Users",
|
||||||
pathname: PATHS.ADMIN_USERS,
|
pathname: PATHS.ADMIN_USERS,
|
||||||
@ -45,7 +49,7 @@ const SettingsWrapper = ({
|
|||||||
}: ISettingsWrapperProp): JSX.Element => {
|
}: ISettingsWrapperProp): JSX.Element => {
|
||||||
const { isPremiumTier } = useContext(AppContext);
|
const { isPremiumTier } = useContext(AppContext);
|
||||||
|
|
||||||
if (isPremiumTier && settingsSubNav.length === 2) {
|
if (isPremiumTier && settingsSubNav.length === 3) {
|
||||||
settingsSubNav.push({
|
settingsSubNav.push({
|
||||||
name: "Teams",
|
name: "Teams",
|
||||||
pathname: PATHS.ADMIN_TEAMS,
|
pathname: PATHS.ADMIN_TEAMS,
|
||||||
|
@ -452,7 +452,7 @@ const MembersPage = ({
|
|||||||
onCreateNewMember={toggleCreateMemberModal}
|
onCreateNewMember={toggleCreateMemberModal}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showEditUserModal ? (
|
{showEditUserModal && (
|
||||||
<EditUserModal
|
<EditUserModal
|
||||||
editUserErrors={editUserErrors}
|
editUserErrors={editUserErrors}
|
||||||
onCancel={toggleEditMemberModal}
|
onCancel={toggleEditMemberModal}
|
||||||
@ -470,8 +470,8 @@ const MembersPage = ({
|
|||||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||||
currentTeam={currentTeam}
|
currentTeam={currentTeam}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
{showCreateUserModal ? (
|
{showCreateUserModal && (
|
||||||
<CreateUserModal
|
<CreateUserModal
|
||||||
createUserErrors={createUserErrors}
|
createUserErrors={createUserErrors}
|
||||||
onCancel={toggleCreateMemberModal}
|
onCancel={toggleCreateMemberModal}
|
||||||
@ -487,15 +487,15 @@ const MembersPage = ({
|
|||||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||||
isFormSubmitting={isFormSubmitting}
|
isFormSubmitting={isFormSubmitting}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
{showRemoveMemberModal && currentTeam ? (
|
{showRemoveMemberModal && currentTeam && (
|
||||||
<RemoveMemberModal
|
<RemoveMemberModal
|
||||||
memberName={userEditing?.name || ""}
|
memberName={userEditing?.name || ""}
|
||||||
teamName={currentTeam.name}
|
teamName={currentTeam.name}
|
||||||
onCancel={toggleRemoveMemberModal}
|
onCancel={toggleRemoveMemberModal}
|
||||||
onSubmit={onRemoveMemberSubmit}
|
onSubmit={onRemoveMemberSubmit}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -248,28 +248,28 @@ const TeamManagementPage = (): JSX.Element => {
|
|||||||
disablePagination
|
disablePagination
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showCreateTeamModal ? (
|
{showCreateTeamModal && (
|
||||||
<CreateTeamModal
|
<CreateTeamModal
|
||||||
onCancel={toggleCreateTeamModal}
|
onCancel={toggleCreateTeamModal}
|
||||||
onSubmit={onCreateSubmit}
|
onSubmit={onCreateSubmit}
|
||||||
backendValidators={backendValidators}
|
backendValidators={backendValidators}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
{showDeleteTeamModal ? (
|
{showDeleteTeamModal && (
|
||||||
<DeleteTeamModal
|
<DeleteTeamModal
|
||||||
onCancel={toggleDeleteTeamModal}
|
onCancel={toggleDeleteTeamModal}
|
||||||
onSubmit={onDeleteSubmit}
|
onSubmit={onDeleteSubmit}
|
||||||
name={teamEditing?.name || ""}
|
name={teamEditing?.name || ""}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
{showEditTeamModal ? (
|
{showEditTeamModal && (
|
||||||
<EditTeamModal
|
<EditTeamModal
|
||||||
onCancel={toggleEditTeamModal}
|
onCancel={toggleEditTeamModal}
|
||||||
onSubmit={onEditSubmit}
|
onSubmit={onEditSubmit}
|
||||||
defaultName={teamEditing?.name || ""}
|
defaultName={teamEditing?.name || ""}
|
||||||
backendValidators={backendValidators}
|
backendValidators={backendValidators}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -132,9 +132,4 @@
|
|||||||
&__tooltip-text {
|
&__tooltip-text {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// so tooltips won't be triggered with whitespace
|
|
||||||
.form-field__label {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,6 @@
|
|||||||
opacity: 75%;
|
opacity: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// so tooltips won't be triggered with whitespace
|
|
||||||
.form-field__label {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field__label--error {
|
.form-field__label--error {
|
||||||
color: $ui-error;
|
color: $ui-error;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { useDebouncedCallback } from "use-debounce";
|
|||||||
import { AppContext } from "context/app";
|
import { AppContext } from "context/app";
|
||||||
import { NotificationContext } from "context/notification";
|
import { NotificationContext } from "context/notification";
|
||||||
import { IConfig } from "interfaces/config";
|
import { IConfig } from "interfaces/config";
|
||||||
|
import { IJiraIntegration } from "interfaces/integration";
|
||||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
|
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
|
||||||
import configAPI from "services/entities/config";
|
import configAPI from "services/entities/config";
|
||||||
import softwareAPI, {
|
import softwareAPI, {
|
||||||
@ -42,6 +43,15 @@ interface IManageSoftwarePageProps {
|
|||||||
search: string;
|
search: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ISoftwareAutomations {
|
||||||
|
webhook_settings: {
|
||||||
|
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||||
|
};
|
||||||
|
integrations: {
|
||||||
|
jira: IJiraIntegration[];
|
||||||
|
};
|
||||||
|
}
|
||||||
interface IHeaderButtonsState extends ITeamsDropdownState {
|
interface IHeaderButtonsState extends ITeamsDropdownState {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
@ -66,9 +76,9 @@ const ManageSoftwarePage = ({
|
|||||||
|
|
||||||
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
|
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
|
||||||
const [
|
const [
|
||||||
softwareVulnerabilitiesWebhook,
|
isVulnerabilityAutomationsEnabled,
|
||||||
setSoftwareVulnerabilitiesWebhook,
|
setIsVulnerabilityAutomationsEnabled,
|
||||||
] = useState<IWebhookSoftwareVulnerabilities>();
|
] = useState<boolean>();
|
||||||
const [filterVuln, setFilterVuln] = useState(
|
const [filterVuln, setFilterVuln] = useState(
|
||||||
location?.query?.vulnerable || false
|
location?.query?.vulnerable || false
|
||||||
);
|
);
|
||||||
@ -91,6 +101,18 @@ const ManageSoftwarePage = ({
|
|||||||
const { data: config } = useQuery(["config"], configAPI.loadAll, {
|
const { data: config } = useQuery(["config"], configAPI.loadAll, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setIsSoftwareEnabled(data?.host_settings?.enable_software_inventory);
|
setIsSoftwareEnabled(data?.host_settings?.enable_software_inventory);
|
||||||
|
let jiraIntegrationEnabled = false;
|
||||||
|
if (data.integrations.jira) {
|
||||||
|
jiraIntegrationEnabled = data?.integrations.jira.some(
|
||||||
|
(integration: any) => {
|
||||||
|
return integration.enable_software_vulnerabilities;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsVulnerabilityAutomationsEnabled(
|
||||||
|
data?.webhook_settings?.vulnerabilities_webhook
|
||||||
|
.enable_vulnerabilities_webhook || jiraIntegrationEnabled
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -168,18 +190,18 @@ const ManageSoftwarePage = ({
|
|||||||
|
|
||||||
const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer;
|
const canAddOrRemoveSoftwareWebhook = isGlobalAdmin || isGlobalMaintainer;
|
||||||
|
|
||||||
const { isLoading: isLoadingConfig, refetch: refetchConfig } = useQuery<
|
const {
|
||||||
IConfig,
|
data: softwareVulnerabilitiesWebhook,
|
||||||
Error
|
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||||
>(["config"], () => configAPI.loadAll(), {
|
refetch: refetchSoftwareVulnerabilitiesWebhook,
|
||||||
enabled: canAddOrRemoveSoftwareWebhook,
|
} = useQuery<IConfig, Error, IWebhookSoftwareVulnerabilities>(
|
||||||
onSuccess: (data) => {
|
["config"],
|
||||||
setSoftwareVulnerabilitiesWebhook(
|
() => configAPI.loadAll(),
|
||||||
data.webhook_settings.vulnerabilities_webhook
|
{
|
||||||
);
|
enabled: canAddOrRemoveSoftwareWebhook,
|
||||||
setConfig(data);
|
select: (data: IConfig) => data.webhook_settings.vulnerabilities_webhook,
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const onQueryChange = useDebouncedCallback(
|
const onQueryChange = useDebouncedCallback(
|
||||||
async ({
|
async ({
|
||||||
@ -212,19 +234,11 @@ const ManageSoftwarePage = ({
|
|||||||
toggleManageAutomationsModal();
|
toggleManageAutomationsModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCreateWebhookSubmit = async ({
|
const onCreateWebhookSubmit = async (
|
||||||
destination_url,
|
configSoftwareAutomations: ISoftwareAutomations
|
||||||
enable_vulnerabilities_webhook,
|
) => {
|
||||||
}: IWebhookSoftwareVulnerabilities) => {
|
|
||||||
try {
|
try {
|
||||||
const request = configAPI.update({
|
const request = configAPI.update(configSoftwareAutomations);
|
||||||
webhook_settings: {
|
|
||||||
vulnerabilities_webhook: {
|
|
||||||
destination_url,
|
|
||||||
enable_vulnerabilities_webhook,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await request.then(() => {
|
await request.then(() => {
|
||||||
renderFlash(
|
renderFlash(
|
||||||
"success",
|
"success",
|
||||||
@ -238,7 +252,7 @@ const ManageSoftwarePage = ({
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
toggleManageAutomationsModal();
|
toggleManageAutomationsModal();
|
||||||
refetchConfig();
|
refetchSoftwareVulnerabilitiesWebhook();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -250,7 +264,7 @@ const ManageSoftwarePage = ({
|
|||||||
state: IHeaderButtonsState
|
state: IHeaderButtonsState
|
||||||
): JSX.Element | null => {
|
): JSX.Element | null => {
|
||||||
if (
|
if (
|
||||||
(state.isGlobalAdmin || state.isGlobalMaintainer) &&
|
state.isGlobalAdmin &&
|
||||||
(!state.isPremiumTier || state.teamId === 0) &&
|
(!state.isPremiumTier || state.teamId === 0) &&
|
||||||
!state.isLoading
|
!state.isLoading
|
||||||
) {
|
) {
|
||||||
@ -297,12 +311,12 @@ const ManageSoftwarePage = ({
|
|||||||
buttons={(state) =>
|
buttons={(state) =>
|
||||||
renderHeaderButtons({
|
renderHeaderButtons({
|
||||||
...state,
|
...state,
|
||||||
isLoading: isLoadingConfig,
|
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [router, location, isLoadingConfig]);
|
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
|
||||||
|
|
||||||
const renderSoftwareCount = useCallback(() => {
|
const renderSoftwareCount = useCallback(() => {
|
||||||
const count = softwareCount;
|
const count = softwareCount;
|
||||||
@ -469,6 +483,9 @@ const ManageSoftwarePage = ({
|
|||||||
onCreateWebhookSubmit={onCreateWebhookSubmit}
|
onCreateWebhookSubmit={onCreateWebhookSubmit}
|
||||||
togglePreviewPayloadModal={togglePreviewPayloadModal}
|
togglePreviewPayloadModal={togglePreviewPayloadModal}
|
||||||
showPreviewPayloadModal={showPreviewPayloadModal}
|
showPreviewPayloadModal={showPreviewPayloadModal}
|
||||||
|
softwareVulnerabilityAutomationEnabled={
|
||||||
|
isVulnerabilityAutomationsEnabled
|
||||||
|
}
|
||||||
softwareVulnerabilityWebhookEnabled={
|
softwareVulnerabilityWebhookEnabled={
|
||||||
softwareVulnerabilitiesWebhook &&
|
softwareVulnerabilitiesWebhook &&
|
||||||
softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook
|
softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook
|
||||||
|
@ -1,22 +1,46 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import PATHS from "router/paths";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IJiraIntegration,
|
||||||
|
IJiraIntegrationIndexed,
|
||||||
|
} from "interfaces/integration";
|
||||||
|
import { IConfig } from "interfaces/config";
|
||||||
|
import configAPI from "services/entities/config";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Dropdown from "components/forms/fields/Dropdown";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
import Slider from "components/forms/fields/Slider";
|
import Slider from "components/forms/fields/Slider";
|
||||||
|
import Radio from "components/forms/fields/Radio";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import InputField from "components/forms/fields/InputField";
|
import InputField from "components/forms/fields/InputField";
|
||||||
|
|
||||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||||
import { useDeepEffect } from "utilities/hooks";
|
import { useDeepEffect } from "utilities/hooks";
|
||||||
import { size } from "lodash";
|
import _, { size } from "lodash";
|
||||||
|
|
||||||
import PreviewPayloadModal from "../PreviewPayloadModal";
|
import PreviewPayloadModal from "../PreviewPayloadModal";
|
||||||
|
|
||||||
|
interface ISoftwareAutomations {
|
||||||
|
webhook_settings: {
|
||||||
|
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||||
|
};
|
||||||
|
integrations: {
|
||||||
|
jira: IJiraIntegration[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface IManageAutomationsModalProps {
|
interface IManageAutomationsModalProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onCreateWebhookSubmit: (formData: IWebhookSoftwareVulnerabilities) => void;
|
onCreateWebhookSubmit: (formData: ISoftwareAutomations) => void;
|
||||||
togglePreviewPayloadModal: () => void;
|
togglePreviewPayloadModal: () => void;
|
||||||
showPreviewPayloadModal: boolean;
|
showPreviewPayloadModal: boolean;
|
||||||
|
softwareVulnerabilityAutomationEnabled?: boolean;
|
||||||
softwareVulnerabilityWebhookEnabled?: boolean;
|
softwareVulnerabilityWebhookEnabled?: boolean;
|
||||||
currentDestinationUrl?: string;
|
currentDestinationUrl?: string;
|
||||||
}
|
}
|
||||||
@ -39,6 +63,7 @@ const ManageAutomationsModal = ({
|
|||||||
onCreateWebhookSubmit,
|
onCreateWebhookSubmit,
|
||||||
togglePreviewPayloadModal,
|
togglePreviewPayloadModal,
|
||||||
showPreviewPayloadModal,
|
showPreviewPayloadModal,
|
||||||
|
softwareVulnerabilityAutomationEnabled,
|
||||||
softwareVulnerabilityWebhookEnabled,
|
softwareVulnerabilityWebhookEnabled,
|
||||||
currentDestinationUrl,
|
currentDestinationUrl,
|
||||||
}: IManageAutomationsModalProps): JSX.Element => {
|
}: IManageAutomationsModalProps): JSX.Element => {
|
||||||
@ -46,11 +71,20 @@ const ManageAutomationsModal = ({
|
|||||||
currentDestinationUrl || ""
|
currentDestinationUrl || ""
|
||||||
);
|
);
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
const [
|
const [
|
||||||
softwareAutomationsEnabled,
|
softwareAutomationsEnabled,
|
||||||
setSoftwareAutomationsEnabled,
|
setSoftwareAutomationsEnabled,
|
||||||
] = useState<boolean>(softwareVulnerabilityWebhookEnabled || false);
|
] = useState<boolean>(softwareVulnerabilityAutomationEnabled || false);
|
||||||
|
const [jiraEnabled, setJiraEnabled] = useState<boolean>(
|
||||||
|
!softwareVulnerabilityWebhookEnabled
|
||||||
|
);
|
||||||
|
const [integrationsIndexed, setIntegrationsIndexed] = useState<
|
||||||
|
IJiraIntegrationIndexed[]
|
||||||
|
>();
|
||||||
|
const [
|
||||||
|
selectedIntegration,
|
||||||
|
setSelectedIntegration,
|
||||||
|
] = useState<IJiraIntegration>();
|
||||||
|
|
||||||
useDeepEffect(() => {
|
useDeepEffect(() => {
|
||||||
if (destination_url) {
|
if (destination_url) {
|
||||||
@ -58,6 +92,30 @@ const ManageAutomationsModal = ({
|
|||||||
}
|
}
|
||||||
}, [destination_url]);
|
}, [destination_url]);
|
||||||
|
|
||||||
|
const { data: integrations } = useQuery<IConfig, Error, IJiraIntegration[]>(
|
||||||
|
["integrations"],
|
||||||
|
() => configAPI.loadAll(),
|
||||||
|
{
|
||||||
|
select: (data: IConfig) => {
|
||||||
|
return data.integrations.jira;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
const addIndex = data.map((integration, index) => {
|
||||||
|
return { ...integration, index };
|
||||||
|
});
|
||||||
|
setIntegrationsIndexed(addIndex);
|
||||||
|
const currentSelectedJiraIntegration = addIndex.find(
|
||||||
|
(integration) => {
|
||||||
|
return integration.enable_software_vulnerabilities === true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setSelectedIntegration(currentSelectedJiraIntegration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const onURLChange = (value: string) => {
|
const onURLChange = (value: string) => {
|
||||||
setDestinationUrl(value);
|
setDestinationUrl(value);
|
||||||
};
|
};
|
||||||
@ -65,21 +123,175 @@ const ManageAutomationsModal = ({
|
|||||||
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
|
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
|
const { valid: validUrl, errors: newErrors } = validateWebhookURL(
|
||||||
|
destination_url
|
||||||
|
);
|
||||||
setErrors({
|
setErrors({
|
||||||
...errors,
|
...errors,
|
||||||
...newErrors,
|
...newErrors,
|
||||||
});
|
});
|
||||||
|
|
||||||
// URL validation only needed if software automation is checked
|
// Original config keys for software automation (webhook_settings, integrations)
|
||||||
if (valid || !softwareAutomationsEnabled) {
|
const configSoftwareAutomations: ISoftwareAutomations = {
|
||||||
onCreateWebhookSubmit({
|
webhook_settings: {
|
||||||
destination_url,
|
vulnerabilities_webhook: {
|
||||||
enable_vulnerabilities_webhook: softwareAutomationsEnabled,
|
destination_url,
|
||||||
});
|
enable_vulnerabilities_webhook: softwareVulnerabilityWebhookEnabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrations: {
|
||||||
|
jira: integrations || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
onReturnToApp();
|
const updateSoftwareAutomation = () => {
|
||||||
}
|
if (!softwareAutomationsEnabled) {
|
||||||
|
// set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false
|
||||||
|
configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = false;
|
||||||
|
const disableAllJira = configSoftwareAutomations.integrations.jira.map(
|
||||||
|
(integration) => {
|
||||||
|
return { ...integration, enable_software_vulnerabilities: false };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
configSoftwareAutomations.integrations.jira = disableAllJira;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!jiraEnabled) {
|
||||||
|
if (!validUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set enable_vulnerabilities_webhook to true and all jira.enable_software_vulnerabilities to false
|
||||||
|
configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = true;
|
||||||
|
const disableAllJira = configSoftwareAutomations.integrations.jira.map(
|
||||||
|
(integration) => {
|
||||||
|
return {
|
||||||
|
...integration,
|
||||||
|
enable_software_vulnerabilities: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
configSoftwareAutomations.integrations.jira = disableAllJira;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set enable_vulnerabilities_webhook to false and all jira.enable_software_vulnerabilities to false
|
||||||
|
// except the one jira integration selected
|
||||||
|
configSoftwareAutomations.webhook_settings.vulnerabilities_webhook.enable_vulnerabilities_webhook = false;
|
||||||
|
const enableSelectedJiraIntegrationOnly = configSoftwareAutomations.integrations.jira.map(
|
||||||
|
(integration, index) => {
|
||||||
|
return {
|
||||||
|
...integration,
|
||||||
|
enable_software_vulnerabilities:
|
||||||
|
index === selectedIntegration?.index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
configSoftwareAutomations.integrations.jira = enableSelectedJiraIntegrationOnly;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSoftwareAutomation();
|
||||||
|
onCreateWebhookSubmit(configSoftwareAutomations);
|
||||||
|
onReturnToApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createIntegrationDropdownOptions = () => {
|
||||||
|
const integrationOptions = integrationsIndexed?.map((i) => {
|
||||||
|
return {
|
||||||
|
value: String(i.index),
|
||||||
|
label: `${i.url} - ${i.project_key}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return integrationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeSelectIntegration = (selectIntegrationIndex: string) => {
|
||||||
|
const integrationWithIndex:
|
||||||
|
| IJiraIntegrationIndexed
|
||||||
|
| undefined = integrationsIndexed?.find(
|
||||||
|
(integ: IJiraIntegrationIndexed) =>
|
||||||
|
integ.index === parseInt(selectIntegrationIndex, 10)
|
||||||
|
);
|
||||||
|
setSelectedIntegration(integrationWithIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRadioChange = (jira: boolean): ((evt: string) => void) => {
|
||||||
|
return () => {
|
||||||
|
setJiraEnabled(jira);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTicket = () => {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__ticket`}>
|
||||||
|
<div className={`${baseClass}__software-automation-description`}>
|
||||||
|
<p>
|
||||||
|
A ticket will be created in your <b>Integration</b> if a detected
|
||||||
|
vulnerability (CVE) was published in the last 30 days.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{integrationsIndexed && integrationsIndexed.length > 0 ? (
|
||||||
|
<Dropdown
|
||||||
|
searchable
|
||||||
|
options={createIntegrationDropdownOptions()}
|
||||||
|
onChange={onChangeSelectIntegration}
|
||||||
|
placeholder={"Select Jira integration"}
|
||||||
|
value={selectedIntegration?.index}
|
||||||
|
label={"Integration"}
|
||||||
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||||
|
hint={
|
||||||
|
"For each new vulnerability detected, Fleet will create a ticket with a list of the affected hosts."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={`${baseClass}__no-integrations`}>
|
||||||
|
<div>
|
||||||
|
<b>You have no integrations.</b>
|
||||||
|
</div>
|
||||||
|
<div className={`${baseClass}__no-integration--cta`}>
|
||||||
|
<Link
|
||||||
|
to={PATHS.ADMIN_INTEGRATIONS}
|
||||||
|
className={`${baseClass}__add-integration-link`}
|
||||||
|
>
|
||||||
|
<span>Add integration</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderWebhook = () => {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__webhook`}>
|
||||||
|
<div className={`${baseClass}__software-automation-description`}>
|
||||||
|
<p>
|
||||||
|
A request will be sent to your configured <b>Destination URL</b> if
|
||||||
|
a detected vulnerability (CVE) was published in the last 30 days.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InputField
|
||||||
|
inputWrapperClass={`${baseClass}__url-input`}
|
||||||
|
name="webhook-url"
|
||||||
|
label={"Destination URL"}
|
||||||
|
type={"text"}
|
||||||
|
value={destination_url}
|
||||||
|
onChange={onURLChange}
|
||||||
|
error={errors.url}
|
||||||
|
hint={
|
||||||
|
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
|
||||||
|
}
|
||||||
|
placeholder={"https://server.com/example"}
|
||||||
|
tooltip="Provide a URL to deliver a webhook request to."
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text-link"
|
||||||
|
onClick={togglePreviewPayloadModal}
|
||||||
|
>
|
||||||
|
Preview payload
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showPreviewPayloadModal) {
|
if (showPreviewPayloadModal) {
|
||||||
@ -105,34 +317,28 @@ const ManageAutomationsModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={`${baseClass}__overlay-container`}>
|
<div className={`${baseClass}__overlay-container`}>
|
||||||
<div className={`${baseClass}__software-automation-enabled`}>
|
<div className={`${baseClass}__software-automation-enabled`}>
|
||||||
<div className={`${baseClass}__software-automation-description`}>
|
<div className={`${baseClass}__workflow`}>
|
||||||
<p>
|
Workflow
|
||||||
A request will be sent to your configured <b>Destination URL</b>{" "}
|
<Radio
|
||||||
if a detected vulnerability (CVE) was published in the last 30
|
className={`${baseClass}__radio-input`}
|
||||||
days.
|
label={"Ticket"}
|
||||||
</p>
|
id={"ticket-radio-btn"}
|
||||||
|
checked={jiraEnabled}
|
||||||
|
value={"ticket"}
|
||||||
|
name={"ticket"}
|
||||||
|
onChange={onRadioChange(true)}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
className={`${baseClass}__radio-input`}
|
||||||
|
label={"Webhook"}
|
||||||
|
id={"webhook-radio-btn"}
|
||||||
|
checked={!jiraEnabled}
|
||||||
|
value={"webhook"}
|
||||||
|
name={"webhook"}
|
||||||
|
onChange={onRadioChange(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InputField
|
{jiraEnabled ? renderTicket() : renderWebhook()}
|
||||||
inputWrapperClass={`${baseClass}__url-input`}
|
|
||||||
name="webhook-url"
|
|
||||||
label={"Destination URL"}
|
|
||||||
type={"text"}
|
|
||||||
value={destination_url}
|
|
||||||
onChange={onURLChange}
|
|
||||||
error={errors.url}
|
|
||||||
hint={
|
|
||||||
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
|
|
||||||
}
|
|
||||||
placeholder={"https://server.com/example"}
|
|
||||||
tooltip="Provide a URL to deliver a webhook request to."
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="text-link"
|
|
||||||
onClick={togglePreviewPayloadModal}
|
|
||||||
>
|
|
||||||
Preview payload
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{!softwareAutomationsEnabled && (
|
{!softwareAutomationsEnabled && (
|
||||||
<div className={`${baseClass}__overlay`} />
|
<div className={`${baseClass}__overlay`} />
|
||||||
@ -151,6 +357,7 @@ const ManageAutomationsModal = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="brand"
|
variant="brand"
|
||||||
onClick={handleSaveAutomation}
|
onClick={handleSaveAutomation}
|
||||||
|
disabled={jiraEnabled && !selectedIntegration}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -47,12 +47,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// so tooltips won't be triggered with whitespace
|
|
||||||
.form-field__label {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field__label--error {
|
.form-field__label--error {
|
||||||
color: $ui-error;
|
color: $ui-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__workflow {
|
||||||
|
margin-top: $medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__radio-input {
|
||||||
|
margin: $small 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-integration--cta {
|
||||||
|
margin-top: $pad-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-integration-link {
|
||||||
|
font-size: $x-small;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
font-weight: $bold;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: $pad-large;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { Provider } from "react-redux";
|
|||||||
import { syncHistoryWithStore } from "react-router-redux";
|
import { syncHistoryWithStore } from "react-router-redux";
|
||||||
|
|
||||||
import AdminAppSettingsPage from "pages/admin/AppSettingsPage";
|
import AdminAppSettingsPage from "pages/admin/AppSettingsPage";
|
||||||
|
import AdminIntegrationsPage from "pages/admin/IntegrationsPage";
|
||||||
import AdminUserManagementPage from "pages/admin/UserManagementPage";
|
import AdminUserManagementPage from "pages/admin/UserManagementPage";
|
||||||
import AdminTeamManagementPage from "pages/admin/TeamManagementPage";
|
import AdminTeamManagementPage from "pages/admin/TeamManagementPage";
|
||||||
import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper";
|
import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper";
|
||||||
@ -104,6 +105,10 @@ const routes = (
|
|||||||
<Route component={SettingsWrapper}>
|
<Route component={SettingsWrapper}>
|
||||||
<Route component={AuthenticatedAdminRoutes}>
|
<Route component={AuthenticatedAdminRoutes}>
|
||||||
<Route path="organization" component={AdminAppSettingsPage} />
|
<Route path="organization" component={AdminAppSettingsPage} />
|
||||||
|
<Route
|
||||||
|
path="integrations"
|
||||||
|
component={AdminIntegrationsPage}
|
||||||
|
/>
|
||||||
<Route path="users" component={AdminUserManagementPage} />
|
<Route path="users" component={AdminUserManagementPage} />
|
||||||
<Route path="teams" component={AdminTeamManagementPage} />
|
<Route path="teams" component={AdminTeamManagementPage} />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -8,6 +8,7 @@ export default {
|
|||||||
HOME: `${URL_PREFIX}/dashboard`,
|
HOME: `${URL_PREFIX}/dashboard`,
|
||||||
ADMIN_USERS: `${URL_PREFIX}/settings/users`,
|
ADMIN_USERS: `${URL_PREFIX}/settings/users`,
|
||||||
ADMIN_SETTINGS: `${URL_PREFIX}/settings/organization`,
|
ADMIN_SETTINGS: `${URL_PREFIX}/settings/organization`,
|
||||||
|
ADMIN_INTEGRATIONS: `${URL_PREFIX}/settings/integrations`,
|
||||||
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,
|
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,
|
||||||
ALL_PACKS: `${URL_PREFIX}/packs/all`,
|
ALL_PACKS: `${URL_PREFIX}/packs/all`,
|
||||||
EDIT_PACK: (packId: number): string => {
|
EDIT_PACK: (packId: number): string => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import sendRequest from "services";
|
import sendRequest from "services";
|
||||||
|
import sendMockRequest from "services/mock_service";
|
||||||
import endpoints from "fleet/endpoints";
|
import endpoints from "fleet/endpoints";
|
||||||
import { IConfig } from "interfaces/config";
|
import { IConfig } from "interfaces/config";
|
||||||
|
|
||||||
|
@ -19,35 +19,15 @@ const WILDCARDS: string[] = [":", "*", "{", "}"];
|
|||||||
// REQUEST_RESPONSE_MAPPINGS dictionary maps your static responses to the specified API request path
|
// REQUEST_RESPONSE_MAPPINGS dictionary maps your static responses to the specified API request path
|
||||||
const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
||||||
GET: {
|
GET: {
|
||||||
// this is a basic path with no wildcards
|
config: RESPONSES.config1, // just first integration -- to throw error, rename config as configz
|
||||||
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc":
|
|
||||||
RESPONSES.ALL_HOSTS,
|
|
||||||
// this basic path only matches with '1337' as the value for the team id query param
|
|
||||||
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id=1337":
|
|
||||||
RESPONSES.HOSTS_TEAM_1337,
|
|
||||||
// this wildcard path matches with any other value for the team id query param
|
|
||||||
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id={team_id}":
|
|
||||||
RESPONSES.HOSTS_TEAM_ID,
|
|
||||||
// this basic path only matches with '1337' as the value for the host id route param
|
|
||||||
"/hosts/1337": RESPONSES.HOST_1337,
|
|
||||||
// this wildcard path matches with any other value for the host id route param
|
|
||||||
"/hosts/*id": RESPONSES.HOST_ID,
|
|
||||||
// this wildcard path matches with any value for the host id route param
|
|
||||||
"/hosts/:id/device_mapping": RESPONSES.DEVICE_MAPPING,
|
|
||||||
// this wildcard path matches with any value for the host id route param
|
|
||||||
"hosts/{*}/macadmins": RESPONSES.MACADMINS,
|
|
||||||
// this is a basic path with no wildcards
|
|
||||||
"hosts/count": {
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
// this wildcard path matches with any value for the team id route param
|
|
||||||
"hosts/count?team_id={*}": {
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.)
|
// additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.)
|
||||||
POST: {
|
PATCH: {
|
||||||
"/:id/refetch": {}, // this wildcard route returns empty JSON
|
config: RESPONSES.configAdd2, // will add second integration to first one
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
// will remove second integration
|
||||||
|
config: RESPONSES.config1,
|
||||||
},
|
},
|
||||||
} as IResponses;
|
} as IResponses;
|
||||||
|
|
||||||
|
@ -4,93 +4,185 @@
|
|||||||
* Also please check the README for how to use the mock service :)
|
* Also please check the README for how to use the mock service :)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const HOST_ID = {
|
const mockJira0 = {
|
||||||
host: {
|
url: "https://example0.jira.com",
|
||||||
created_at: "2021-03-31T00:00:00Z",
|
username: "adminUser",
|
||||||
updated_at: "2021-03-31T00:00:00Z",
|
password: "abc123",
|
||||||
software: [],
|
project_key: "EXAMPLE",
|
||||||
id: 1337,
|
enable_software_vulnerabilities: false,
|
||||||
detail_updated_at: "2021-03-31T00:00:00Z",
|
|
||||||
label_updated_at: "2021-03-31T00:00:00Z",
|
|
||||||
policy_updated_at: "2021-03-31T00:00:00Z",
|
|
||||||
last_enrolled_at: "2021-03-31T00:00:00Z",
|
|
||||||
seen_time: "2021-03-31T00:00:00ZZ",
|
|
||||||
refetch_requested: false,
|
|
||||||
hostname: "myf1337d3v1c3",
|
|
||||||
uuid: "13371337-0000-0000-1337-133713371337",
|
|
||||||
platform: "rhel",
|
|
||||||
osquery_version: "5.1.0",
|
|
||||||
os_version: "Ubuntu 20.4.0",
|
|
||||||
build: "",
|
|
||||||
platform_like: "deb",
|
|
||||||
code_name: "",
|
|
||||||
uptime: 13371337133713371337,
|
|
||||||
memory: 143593800000,
|
|
||||||
cpu_type: "1337",
|
|
||||||
cpu_subtype: "1337",
|
|
||||||
cpu_brand: "Intel(R) Core(TM) i3-37k CPU @ 13.37GHz",
|
|
||||||
cpu_physical_cores: 8,
|
|
||||||
cpu_logical_cores: 8,
|
|
||||||
hardware_vendor: "",
|
|
||||||
hardware_model: "",
|
|
||||||
hardware_version: "",
|
|
||||||
hardware_serial: "",
|
|
||||||
computer_name: "myf1337d3v1c3",
|
|
||||||
primary_ip: "133.7.133.7",
|
|
||||||
primary_mac: "13:37:13:37:13:37",
|
|
||||||
distributed_interval: 1337,
|
|
||||||
config_tls_refresh: 1337,
|
|
||||||
logger_tls_period: 1337,
|
|
||||||
team_id: null,
|
|
||||||
pack_stats: [],
|
|
||||||
team_name: null,
|
|
||||||
users: [
|
|
||||||
{
|
|
||||||
uid: 1337,
|
|
||||||
username: "root",
|
|
||||||
type: "",
|
|
||||||
groupname: "root",
|
|
||||||
shell: "/bin/bash",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
gigs_disk_space_available: 13.37,
|
|
||||||
percent_disk_space_available: 13.37,
|
|
||||||
issues: {
|
|
||||||
total_issues_count: 1337,
|
|
||||||
failing_policies_count: 1337,
|
|
||||||
},
|
|
||||||
labels: [],
|
|
||||||
packs: [],
|
|
||||||
policies: [],
|
|
||||||
status: "online",
|
|
||||||
display_text: "myf1337d3v1c3",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const HOST_1337 = {
|
|
||||||
...HOST_ID,
|
const mockJira1 = {
|
||||||
team_id: 1337,
|
url: "https://example1.jira.com",
|
||||||
team_name: "h4x0r",
|
username: "adminUser",
|
||||||
|
password: "abc123",
|
||||||
|
project_key: "PROJECT",
|
||||||
|
enable_software_vulnerabilities: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockJira2 = {
|
||||||
|
url: "https://example2.jira.com",
|
||||||
|
username: "adminUser",
|
||||||
|
password: "abc123",
|
||||||
|
project_key: "KEY",
|
||||||
|
enable_software_vulnerabilities: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIntegration1 = {
|
||||||
|
jira: [mockJira0, mockJira1],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIntegration2 = {
|
||||||
|
jira: [mockJira2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIntegrationAdd2 = {
|
||||||
|
jira: [mockJira0, mockJira1, mockJira2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
org_info: {
|
||||||
|
org_name: "s",
|
||||||
|
org_logo_url: "",
|
||||||
|
},
|
||||||
|
server_settings: {
|
||||||
|
server_url: "https://localhost:8080",
|
||||||
|
live_query_disabled: false,
|
||||||
|
enable_analytics: true,
|
||||||
|
deferred_save_host: false,
|
||||||
|
},
|
||||||
|
smtp_settings: {
|
||||||
|
enable_smtp: false,
|
||||||
|
configured: true,
|
||||||
|
sender_address: "",
|
||||||
|
server: "",
|
||||||
|
port: 0,
|
||||||
|
authentication_type: "authtype_none",
|
||||||
|
user_name: "",
|
||||||
|
password: "",
|
||||||
|
enable_ssl_tls: true,
|
||||||
|
authentication_method: "authmethod_plain",
|
||||||
|
domain: "",
|
||||||
|
verify_ssl_certs: true,
|
||||||
|
enable_start_tls: true,
|
||||||
|
},
|
||||||
|
host_expiry_settings: {
|
||||||
|
host_expiry_enabled: true,
|
||||||
|
host_expiry_window: 9,
|
||||||
|
},
|
||||||
|
host_settings: {
|
||||||
|
enable_host_users: true,
|
||||||
|
enable_software_inventory: true,
|
||||||
|
},
|
||||||
|
agent_options: {
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
logger_plugin: "tls",
|
||||||
|
pack_delimiter: "/",
|
||||||
|
logger_tls_period: 100,
|
||||||
|
distributed_plugin: "tls",
|
||||||
|
disable_distributed: false,
|
||||||
|
logger_tls_endpoint: "/api/v1/osquery/log",
|
||||||
|
distributed_interval: 10,
|
||||||
|
distributed_tls_max_attempts: 3,
|
||||||
|
},
|
||||||
|
decorators: {
|
||||||
|
load: [
|
||||||
|
"SELECT uuid AS host_uuid FROM system_info;",
|
||||||
|
"SELECT hostname AS hostname FROM system_info;",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: {},
|
||||||
|
},
|
||||||
|
sso_settings: {
|
||||||
|
entity_id: "",
|
||||||
|
issuer_uri: "",
|
||||||
|
idp_image_url: "",
|
||||||
|
metadata: "",
|
||||||
|
metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php",
|
||||||
|
idp_name: "",
|
||||||
|
enable_sso: false,
|
||||||
|
enable_sso_idp_login: false,
|
||||||
|
},
|
||||||
|
vulnerability_settings: {
|
||||||
|
databases_path: "",
|
||||||
|
},
|
||||||
|
webhook_settings: {
|
||||||
|
host_status_webhook: {
|
||||||
|
enable_host_status_webhook: false,
|
||||||
|
destination_url: "",
|
||||||
|
host_percentage: 0,
|
||||||
|
days_count: 0,
|
||||||
|
},
|
||||||
|
failing_policies_webhook: {
|
||||||
|
enable_failing_policies_webhook: false,
|
||||||
|
destination_url: "",
|
||||||
|
policy_ids: [],
|
||||||
|
host_batch_size: 0,
|
||||||
|
},
|
||||||
|
vulnerabilities_webhook: {
|
||||||
|
enable_vulnerabilities_webhook: true,
|
||||||
|
destination_url: "www.example.com/",
|
||||||
|
host_batch_size: 0,
|
||||||
|
},
|
||||||
|
interval: "24h0m0s",
|
||||||
|
},
|
||||||
|
update_interval: {
|
||||||
|
osquery_detail: 10000000000,
|
||||||
|
osquery_policy: 3600000000000,
|
||||||
|
},
|
||||||
|
vulnerabilities: {
|
||||||
|
databases_path: "/tmp/vulndbs",
|
||||||
|
periodicity: 3600000000000,
|
||||||
|
cpe_database_url: "",
|
||||||
|
cve_feed_prefix_url: "",
|
||||||
|
current_instance_checks: "auto",
|
||||||
|
disable_data_sync: false,
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
tier: "premium",
|
||||||
|
organization: "development-only",
|
||||||
|
device_count: 100,
|
||||||
|
expiration: "2022-06-30T20:00:00-04:00",
|
||||||
|
note: "for development only",
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
debug: false,
|
||||||
|
json: false,
|
||||||
|
result: {
|
||||||
|
plugin: "filesystem",
|
||||||
|
config: {
|
||||||
|
status_log_file:
|
||||||
|
"/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_status",
|
||||||
|
result_log_file:
|
||||||
|
"/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_result",
|
||||||
|
enable_log_rotation: false,
|
||||||
|
enable_log_compression: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
plugin: "filesystem",
|
||||||
|
config: {
|
||||||
|
status_log_file:
|
||||||
|
"/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_status",
|
||||||
|
result_log_file:
|
||||||
|
"/var/folders/xh/bxm1d2615tv3vrg4zrxq540h0000gn/T/osquery_result",
|
||||||
|
enable_log_rotation: false,
|
||||||
|
enable_log_compression: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ALL_HOSTS: {
|
config1: { ...mockConfig, integrations: mockIntegration1 },
|
||||||
hosts: [HOST_ID.host],
|
config2: {
|
||||||
|
...mockConfig,
|
||||||
|
integrations: mockIntegration2,
|
||||||
},
|
},
|
||||||
HOSTS_TEAM_ID: {
|
configAdd2: {
|
||||||
hosts: [{ ...HOST_ID.host, team_id: 2, team_name: "n00bz" }],
|
...mockConfig,
|
||||||
},
|
integrations: mockIntegrationAdd2,
|
||||||
HOSTS_TEAM_1337: {
|
|
||||||
hosts: [HOST_1337.host],
|
|
||||||
},
|
|
||||||
HOST_ID,
|
|
||||||
HOST_1337,
|
|
||||||
DEVICE_MAPPING: {
|
|
||||||
host_id: 1337,
|
|
||||||
device_mapping: null,
|
|
||||||
foo: "bar",
|
|
||||||
},
|
|
||||||
MACADMINS: {
|
|
||||||
macadmins: null,
|
|
||||||
foo: "bar",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -340,6 +340,13 @@ export const DEFAULT_CREATE_USER_ERRORS = {
|
|||||||
sso_enabled: null,
|
sso_enabled: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CREATE_INTEGRATION_ERRORS = {
|
||||||
|
url: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
projectKey: "",
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_CREATE_LABEL_ERRORS = {
|
export const DEFAULT_CREATE_LABEL_ERRORS = {
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user