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.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.findByRole("button", { name: /manage automations/i }).click();
|
||||
cy.findByRole("button", {
|
||||
name: /manage automations/i,
|
||||
}).click();
|
||||
});
|
||||
cy.getAttached(".manage-automations-modal").within(() => {
|
||||
cy.getAttached(".fleet-slider").click();
|
||||
cy.getAttached("#webhook-radio-btn").next().click();
|
||||
});
|
||||
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
@ -215,6 +218,7 @@ describe(
|
||||
});
|
||||
cy.getAttached(".manage-automations-modal").within(() => {
|
||||
cy.getAttached(".fleet-slider--active").should("exist");
|
||||
cy.getAttached("#webhook-url").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -187,18 +187,18 @@ describe(
|
||||
});
|
||||
});
|
||||
describe("Manage software page", () => {
|
||||
beforeEach(() => {
|
||||
cy.loginWithCySession("mary@organization.com", "user123#");
|
||||
cy.visit("/software/manage");
|
||||
});
|
||||
it("allows maintainer to click 'Manage automations' button", () => {
|
||||
it("manages software automations when all teams selected", () => {
|
||||
beforeEach(() => cy.visit("/software/manage"));
|
||||
it("should restrict global maintainer from 'Manage automations' button", () => {
|
||||
it("hides manages software automations when all teams selected", () => {
|
||||
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: /cancel/i }).click();
|
||||
cy.getAttached(".manage-software-page__header-wrap").within(() => {
|
||||
cy.findByRole("button", {
|
||||
name: /manage automations/i,
|
||||
}).should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
it("hides manage automations button when all teams not selected", () => {
|
||||
|
@ -116,15 +116,13 @@ describe("Premium tier - Admin user", () => {
|
||||
});
|
||||
describe("Manage software page", () => {
|
||||
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(".Select").within(() => {
|
||||
cy.findByText(/all teams/i).should("exist");
|
||||
});
|
||||
cy.findByRole("button", { name: /manage automations/i }).click();
|
||||
});
|
||||
cy.getAttached(".manage-automations-modal").within(() => {
|
||||
cy.getAttached(".fleet-slider").click();
|
||||
cy.getAttached("#webhook-radio-btn").next().click();
|
||||
});
|
||||
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
@ -137,6 +135,7 @@ describe("Premium tier - Admin user", () => {
|
||||
});
|
||||
cy.getAttached(".manage-automations-modal").within(() => {
|
||||
cy.getAttached(".fleet-slider--active").should("exist");
|
||||
cy.getAttached("#webhook-url").should("exist");
|
||||
});
|
||||
});
|
||||
it("hides manage automations button since all teams not selected", () => {
|
||||
|
@ -101,7 +101,7 @@ const EnrollSecretRow = ({
|
||||
type={showSecret ? "text" : "password"}
|
||||
value={secret.secret}
|
||||
/>
|
||||
{toggleSecretEditorModal && toggleDeleteSecretModal ? (
|
||||
{toggleSecretEditorModal && toggleDeleteSecretModal && (
|
||||
<>
|
||||
<Button
|
||||
onClick={onEditSecretClick}
|
||||
@ -122,7 +122,7 @@ const EnrollSecretRow = ({
|
||||
</>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -13,8 +13,10 @@
|
||||
color: $core-vibrant-red;
|
||||
}
|
||||
|
||||
// so tooltips won't be triggered with whitespace
|
||||
&[data-has-tooltip="true"] {
|
||||
margin-bottom: $pad-small;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,6 @@
|
||||
.form-field__label {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
// so tooltips won't be triggered with whitespace
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__email-hint {
|
||||
|
@ -39,7 +39,7 @@ import {
|
||||
|
||||
const baseClass = "app-config-form";
|
||||
|
||||
const AppConfigFormFunctional = ({
|
||||
const AppConfigForm = ({
|
||||
appConfig,
|
||||
handleSubmit,
|
||||
}: IAppConfigFormProps): JSX.Element => {
|
||||
@ -977,4 +977,4 @@ const AppConfigFormFunctional = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AppConfigFormFunctional;
|
||||
export default AppConfigForm;
|
||||
|
@ -10,7 +10,7 @@ type InitialStateType = {
|
||||
notification: INotification | null;
|
||||
renderFlash: (
|
||||
alertType: "success" | "error" | "warning-filled" | null,
|
||||
message: string | null,
|
||||
message: JSX.Element | string | null,
|
||||
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
||||
) => void;
|
||||
hideFlash: () => void;
|
||||
@ -57,7 +57,7 @@ const NotificationProvider = ({ children }: Props) => {
|
||||
notification: state.notification,
|
||||
renderFlash: (
|
||||
alertType: "success" | "error" | "warning-filled" | null,
|
||||
message: string | null,
|
||||
message: JSX.Element | string | null,
|
||||
undoAction?: (evt: React.MouseEvent<HTMLButtonElement>) => void
|
||||
) => {
|
||||
dispatch({
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
IWebhookSoftwareVulnerabilities,
|
||||
} from "interfaces/webhook";
|
||||
import PropTypes from "prop-types";
|
||||
import { IIntegrations } from "./integration";
|
||||
|
||||
export default PropTypes.shape({
|
||||
org_name: PropTypes.string,
|
||||
@ -178,6 +179,7 @@ export interface IConfig {
|
||||
failing_policies_webhook: IWebhookFailingPolicies;
|
||||
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||
};
|
||||
integrations: IIntegrations;
|
||||
logging: {
|
||||
debug: 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 {
|
||||
display: inline-block;
|
||||
|
||||
.buttons {
|
||||
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",
|
||||
pathname: PATHS.ADMIN_SETTINGS,
|
||||
},
|
||||
{
|
||||
name: "Integrations",
|
||||
pathname: PATHS.ADMIN_INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
pathname: PATHS.ADMIN_USERS,
|
||||
@ -45,7 +49,7 @@ const SettingsWrapper = ({
|
||||
}: ISettingsWrapperProp): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
|
||||
if (isPremiumTier && settingsSubNav.length === 2) {
|
||||
if (isPremiumTier && settingsSubNav.length === 3) {
|
||||
settingsSubNav.push({
|
||||
name: "Teams",
|
||||
pathname: PATHS.ADMIN_TEAMS,
|
||||
|
@ -452,7 +452,7 @@ const MembersPage = ({
|
||||
onCreateNewMember={toggleCreateMemberModal}
|
||||
/>
|
||||
) : null}
|
||||
{showEditUserModal ? (
|
||||
{showEditUserModal && (
|
||||
<EditUserModal
|
||||
editUserErrors={editUserErrors}
|
||||
onCancel={toggleEditMemberModal}
|
||||
@ -470,8 +470,8 @@ const MembersPage = ({
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
currentTeam={currentTeam}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateUserModal ? (
|
||||
)}
|
||||
{showCreateUserModal && (
|
||||
<CreateUserModal
|
||||
createUserErrors={createUserErrors}
|
||||
onCancel={toggleCreateMemberModal}
|
||||
@ -487,15 +487,15 @@ const MembersPage = ({
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
/>
|
||||
) : null}
|
||||
{showRemoveMemberModal && currentTeam ? (
|
||||
)}
|
||||
{showRemoveMemberModal && currentTeam && (
|
||||
<RemoveMemberModal
|
||||
memberName={userEditing?.name || ""}
|
||||
teamName={currentTeam.name}
|
||||
onCancel={toggleRemoveMemberModal}
|
||||
onSubmit={onRemoveMemberSubmit}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -248,28 +248,28 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
disablePagination
|
||||
/>
|
||||
)}
|
||||
{showCreateTeamModal ? (
|
||||
{showCreateTeamModal && (
|
||||
<CreateTeamModal
|
||||
onCancel={toggleCreateTeamModal}
|
||||
onSubmit={onCreateSubmit}
|
||||
backendValidators={backendValidators}
|
||||
/>
|
||||
) : null}
|
||||
{showDeleteTeamModal ? (
|
||||
)}
|
||||
{showDeleteTeamModal && (
|
||||
<DeleteTeamModal
|
||||
onCancel={toggleDeleteTeamModal}
|
||||
onSubmit={onDeleteSubmit}
|
||||
name={teamEditing?.name || ""}
|
||||
/>
|
||||
) : null}
|
||||
{showEditTeamModal ? (
|
||||
)}
|
||||
{showEditTeamModal && (
|
||||
<EditTeamModal
|
||||
onCancel={toggleEditTeamModal}
|
||||
onSubmit={onEditSubmit}
|
||||
defaultName={teamEditing?.name || ""}
|
||||
backendValidators={backendValidators}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -132,9 +132,4 @@
|
||||
&__tooltip-text {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
// so tooltips won't be triggered with whitespace
|
||||
.form-field__label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,6 @@
|
||||
opacity: 75%;
|
||||
}
|
||||
|
||||
// so tooltips won't be triggered with whitespace
|
||||
.form-field__label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-field__label--error {
|
||||
color: $ui-error;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IJiraIntegration } from "interfaces/integration";
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; // @ts-ignore
|
||||
import configAPI from "services/entities/config";
|
||||
import softwareAPI, {
|
||||
@ -42,6 +43,15 @@ interface IManageSoftwarePageProps {
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISoftwareAutomations {
|
||||
webhook_settings: {
|
||||
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||
};
|
||||
integrations: {
|
||||
jira: IJiraIntegration[];
|
||||
};
|
||||
}
|
||||
interface IHeaderButtonsState extends ITeamsDropdownState {
|
||||
isLoading: boolean;
|
||||
}
|
||||
@ -66,9 +76,9 @@ const ManageSoftwarePage = ({
|
||||
|
||||
const [isSoftwareEnabled, setIsSoftwareEnabled] = useState<boolean>();
|
||||
const [
|
||||
softwareVulnerabilitiesWebhook,
|
||||
setSoftwareVulnerabilitiesWebhook,
|
||||
] = useState<IWebhookSoftwareVulnerabilities>();
|
||||
isVulnerabilityAutomationsEnabled,
|
||||
setIsVulnerabilityAutomationsEnabled,
|
||||
] = useState<boolean>();
|
||||
const [filterVuln, setFilterVuln] = useState(
|
||||
location?.query?.vulnerable || false
|
||||
);
|
||||
@ -91,6 +101,18 @@ const ManageSoftwarePage = ({
|
||||
const { data: config } = useQuery(["config"], configAPI.loadAll, {
|
||||
onSuccess: (data) => {
|
||||
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 { isLoading: isLoadingConfig, refetch: refetchConfig } = useQuery<
|
||||
IConfig,
|
||||
Error
|
||||
>(["config"], () => configAPI.loadAll(), {
|
||||
const {
|
||||
data: softwareVulnerabilitiesWebhook,
|
||||
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||
refetch: refetchSoftwareVulnerabilitiesWebhook,
|
||||
} = useQuery<IConfig, Error, IWebhookSoftwareVulnerabilities>(
|
||||
["config"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
enabled: canAddOrRemoveSoftwareWebhook,
|
||||
onSuccess: (data) => {
|
||||
setSoftwareVulnerabilitiesWebhook(
|
||||
data.webhook_settings.vulnerabilities_webhook
|
||||
select: (data: IConfig) => data.webhook_settings.vulnerabilities_webhook,
|
||||
}
|
||||
);
|
||||
setConfig(data);
|
||||
},
|
||||
});
|
||||
|
||||
const onQueryChange = useDebouncedCallback(
|
||||
async ({
|
||||
@ -212,19 +234,11 @@ const ManageSoftwarePage = ({
|
||||
toggleManageAutomationsModal();
|
||||
};
|
||||
|
||||
const onCreateWebhookSubmit = async ({
|
||||
destination_url,
|
||||
enable_vulnerabilities_webhook,
|
||||
}: IWebhookSoftwareVulnerabilities) => {
|
||||
const onCreateWebhookSubmit = async (
|
||||
configSoftwareAutomations: ISoftwareAutomations
|
||||
) => {
|
||||
try {
|
||||
const request = configAPI.update({
|
||||
webhook_settings: {
|
||||
vulnerabilities_webhook: {
|
||||
destination_url,
|
||||
enable_vulnerabilities_webhook,
|
||||
},
|
||||
},
|
||||
});
|
||||
const request = configAPI.update(configSoftwareAutomations);
|
||||
await request.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
@ -238,7 +252,7 @@ const ManageSoftwarePage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
refetchConfig();
|
||||
refetchSoftwareVulnerabilitiesWebhook();
|
||||
}
|
||||
};
|
||||
|
||||
@ -250,7 +264,7 @@ const ManageSoftwarePage = ({
|
||||
state: IHeaderButtonsState
|
||||
): JSX.Element | null => {
|
||||
if (
|
||||
(state.isGlobalAdmin || state.isGlobalMaintainer) &&
|
||||
state.isGlobalAdmin &&
|
||||
(!state.isPremiumTier || state.teamId === 0) &&
|
||||
!state.isLoading
|
||||
) {
|
||||
@ -297,12 +311,12 @@ const ManageSoftwarePage = ({
|
||||
buttons={(state) =>
|
||||
renderHeaderButtons({
|
||||
...state,
|
||||
isLoading: isLoadingConfig,
|
||||
isLoading: isLoadingSoftwareVulnerabilitiesWebhook,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [router, location, isLoadingConfig]);
|
||||
}, [router, location, isLoadingSoftwareVulnerabilitiesWebhook]);
|
||||
|
||||
const renderSoftwareCount = useCallback(() => {
|
||||
const count = softwareCount;
|
||||
@ -469,6 +483,9 @@ const ManageSoftwarePage = ({
|
||||
onCreateWebhookSubmit={onCreateWebhookSubmit}
|
||||
togglePreviewPayloadModal={togglePreviewPayloadModal}
|
||||
showPreviewPayloadModal={showPreviewPayloadModal}
|
||||
softwareVulnerabilityAutomationEnabled={
|
||||
isVulnerabilityAutomationsEnabled
|
||||
}
|
||||
softwareVulnerabilityWebhookEnabled={
|
||||
softwareVulnerabilitiesWebhook &&
|
||||
softwareVulnerabilitiesWebhook.enable_vulnerabilities_webhook
|
||||
|
@ -1,22 +1,46 @@
|
||||
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 Button from "components/buttons/Button";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
import { useDeepEffect } from "utilities/hooks";
|
||||
import { size } from "lodash";
|
||||
import _, { size } from "lodash";
|
||||
|
||||
import PreviewPayloadModal from "../PreviewPayloadModal";
|
||||
|
||||
interface ISoftwareAutomations {
|
||||
webhook_settings: {
|
||||
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
|
||||
};
|
||||
integrations: {
|
||||
jira: IJiraIntegration[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IManageAutomationsModalProps {
|
||||
onCancel: () => void;
|
||||
onCreateWebhookSubmit: (formData: IWebhookSoftwareVulnerabilities) => void;
|
||||
onCreateWebhookSubmit: (formData: ISoftwareAutomations) => void;
|
||||
togglePreviewPayloadModal: () => void;
|
||||
showPreviewPayloadModal: boolean;
|
||||
softwareVulnerabilityAutomationEnabled?: boolean;
|
||||
softwareVulnerabilityWebhookEnabled?: boolean;
|
||||
currentDestinationUrl?: string;
|
||||
}
|
||||
@ -39,6 +63,7 @@ const ManageAutomationsModal = ({
|
||||
onCreateWebhookSubmit,
|
||||
togglePreviewPayloadModal,
|
||||
showPreviewPayloadModal,
|
||||
softwareVulnerabilityAutomationEnabled,
|
||||
softwareVulnerabilityWebhookEnabled,
|
||||
currentDestinationUrl,
|
||||
}: IManageAutomationsModalProps): JSX.Element => {
|
||||
@ -46,11 +71,20 @@ const ManageAutomationsModal = ({
|
||||
currentDestinationUrl || ""
|
||||
);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
const [
|
||||
softwareAutomationsEnabled,
|
||||
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(() => {
|
||||
if (destination_url) {
|
||||
@ -58,6 +92,30 @@ const ManageAutomationsModal = ({
|
||||
}
|
||||
}, [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) => {
|
||||
setDestinationUrl(value);
|
||||
};
|
||||
@ -65,21 +123,175 @@ const ManageAutomationsModal = ({
|
||||
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
|
||||
const { valid: validUrl, errors: newErrors } = validateWebhookURL(
|
||||
destination_url
|
||||
);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
// URL validation only needed if software automation is checked
|
||||
if (valid || !softwareAutomationsEnabled) {
|
||||
onCreateWebhookSubmit({
|
||||
// Original config keys for software automation (webhook_settings, integrations)
|
||||
const configSoftwareAutomations: ISoftwareAutomations = {
|
||||
webhook_settings: {
|
||||
vulnerabilities_webhook: {
|
||||
destination_url,
|
||||
enable_vulnerabilities_webhook: softwareAutomationsEnabled,
|
||||
});
|
||||
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) {
|
||||
@ -105,34 +317,28 @@ const ManageAutomationsModal = ({
|
||||
</div>
|
||||
<div className={`${baseClass}__overlay-container`}>
|
||||
<div className={`${baseClass}__software-automation-enabled`}>
|
||||
<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."
|
||||
<div className={`${baseClass}__workflow`}>
|
||||
Workflow
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Ticket"}
|
||||
id={"ticket-radio-btn"}
|
||||
checked={jiraEnabled}
|
||||
value={"ticket"}
|
||||
name={"ticket"}
|
||||
onChange={onRadioChange(true)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text-link"
|
||||
onClick={togglePreviewPayloadModal}
|
||||
>
|
||||
Preview payload
|
||||
</Button>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Webhook"}
|
||||
id={"webhook-radio-btn"}
|
||||
checked={!jiraEnabled}
|
||||
value={"webhook"}
|
||||
name={"webhook"}
|
||||
onChange={onRadioChange(false)}
|
||||
/>
|
||||
</div>
|
||||
{jiraEnabled ? renderTicket() : renderWebhook()}
|
||||
</div>
|
||||
{!softwareAutomationsEnabled && (
|
||||
<div className={`${baseClass}__overlay`} />
|
||||
@ -151,6 +357,7 @@ const ManageAutomationsModal = ({
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
disabled={jiraEnabled && !selectedIntegration}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
@ -47,12 +47,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
// so tooltips won't be triggered with whitespace
|
||||
.form-field__label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-field__label--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 AdminAppSettingsPage from "pages/admin/AppSettingsPage";
|
||||
import AdminIntegrationsPage from "pages/admin/IntegrationsPage";
|
||||
import AdminUserManagementPage from "pages/admin/UserManagementPage";
|
||||
import AdminTeamManagementPage from "pages/admin/TeamManagementPage";
|
||||
import TeamDetailsWrapper from "pages/admin/TeamManagementPage/TeamDetailsWrapper";
|
||||
@ -104,6 +105,10 @@ const routes = (
|
||||
<Route component={SettingsWrapper}>
|
||||
<Route component={AuthenticatedAdminRoutes}>
|
||||
<Route path="organization" component={AdminAppSettingsPage} />
|
||||
<Route
|
||||
path="integrations"
|
||||
component={AdminIntegrationsPage}
|
||||
/>
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route path="teams" component={AdminTeamManagementPage} />
|
||||
</Route>
|
||||
|
@ -8,6 +8,7 @@ export default {
|
||||
HOME: `${URL_PREFIX}/dashboard`,
|
||||
ADMIN_USERS: `${URL_PREFIX}/settings/users`,
|
||||
ADMIN_SETTINGS: `${URL_PREFIX}/settings/organization`,
|
||||
ADMIN_INTEGRATIONS: `${URL_PREFIX}/settings/integrations`,
|
||||
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,
|
||||
ALL_PACKS: `${URL_PREFIX}/packs/all`,
|
||||
EDIT_PACK: (packId: number): string => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import sendRequest from "services";
|
||||
import sendMockRequest from "services/mock_service";
|
||||
import endpoints from "fleet/endpoints";
|
||||
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
|
||||
const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
||||
GET: {
|
||||
// this is a basic path with no wildcards
|
||||
"/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,
|
||||
},
|
||||
config: RESPONSES.config1, // just first integration -- to throw error, rename config as configz
|
||||
},
|
||||
// additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.)
|
||||
POST: {
|
||||
"/:id/refetch": {}, // this wildcard route returns empty JSON
|
||||
PATCH: {
|
||||
config: RESPONSES.configAdd2, // will add second integration to first one
|
||||
},
|
||||
DELETE: {
|
||||
// will remove second integration
|
||||
config: RESPONSES.config1,
|
||||
},
|
||||
} as IResponses;
|
||||
|
||||
|
@ -4,93 +4,185 @@
|
||||
* Also please check the README for how to use the mock service :)
|
||||
*/
|
||||
|
||||
const HOST_ID = {
|
||||
host: {
|
||||
created_at: "2021-03-31T00:00:00Z",
|
||||
updated_at: "2021-03-31T00:00:00Z",
|
||||
software: [],
|
||||
id: 1337,
|
||||
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 mockJira0 = {
|
||||
url: "https://example0.jira.com",
|
||||
username: "adminUser",
|
||||
password: "abc123",
|
||||
project_key: "EXAMPLE",
|
||||
enable_software_vulnerabilities: false,
|
||||
};
|
||||
const HOST_1337 = {
|
||||
...HOST_ID,
|
||||
team_id: 1337,
|
||||
team_name: "h4x0r",
|
||||
|
||||
const mockJira1 = {
|
||||
url: "https://example1.jira.com",
|
||||
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 {
|
||||
ALL_HOSTS: {
|
||||
hosts: [HOST_ID.host],
|
||||
config1: { ...mockConfig, integrations: mockIntegration1 },
|
||||
config2: {
|
||||
...mockConfig,
|
||||
integrations: mockIntegration2,
|
||||
},
|
||||
HOSTS_TEAM_ID: {
|
||||
hosts: [{ ...HOST_ID.host, team_id: 2, team_name: "n00bz" }],
|
||||
},
|
||||
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",
|
||||
configAdd2: {
|
||||
...mockConfig,
|
||||
integrations: mockIntegrationAdd2,
|
||||
},
|
||||
};
|
||||
|
@ -340,6 +340,13 @@ export const DEFAULT_CREATE_USER_ERRORS = {
|
||||
sso_enabled: null,
|
||||
};
|
||||
|
||||
export const DEFAULT_CREATE_INTEGRATION_ERRORS = {
|
||||
url: "",
|
||||
username: "",
|
||||
password: "",
|
||||
projectKey: "",
|
||||
};
|
||||
|
||||
export const DEFAULT_CREATE_LABEL_ERRORS = {
|
||||
name: "",
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user