Add new feature: Policies (#1772)

This commit is contained in:
gillespi314 2021-08-30 18:02:53 -05:00 committed by GitHub
parent 247267daa5
commit 452392f5d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1642 additions and 54 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

1
changes/1790-policies Normal file
View file

@ -0,0 +1 @@
Add new policies feature

View file

@ -0,0 +1,107 @@
// describe(
// "Policies flow",
// {
// defaultCommandTimeout: 20000,
// },
// () => {
// beforeEach(() => {
// cy.setup();
// cy.login();
// cy.seedQueries();
// });
// it("Can create, check, and delete a policy successfully", () => {
// cy.intercept({
// method: "GET",
// url: "/api/v1/fleet/global/policies",
// }).as("getPolicies");
// cy.intercept({
// method: "GET",
// url: "/api/v1/fleet/config",
// }).as("getConfig");
// cy.visit("/policies/manage");
// // wait for state of policy table to settle otherwise re-renders cause elements to detach and tests will fail
// cy.wait("@getPolicies");
// cy.wait("@getConfig");
// cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
// // Add a policy
// cy.get(".no-policies__inner")
// .findByText(/add a policy/i)
// .should("exist")
// .click();
// cy.get(".add-policy-modal").within(() => {
// cy.findByText(/select query/i)
// .should("exist")
// .click();
// cy.findByText(
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
// ).click();
// cy.findByRole("button", { name: /cancel/i }).should("exist");
// cy.findByRole("button", { name: /add/i }).should("exist").click();
// });
// // Confirm that policy was added successfully
// cy.findByText(/successfully added policy/i).should("exist");
// cy.findByText(/select query/i).should("not.exist");
// cy.get(".policies-list-wrapper").within(() => {
// cy.findByText(/1 query/i).should("exist");
// cy.findByText(/passing/i).should("exist");
// cy.findByText(
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
// ).should("exist");
// // Click on link in table and confirm that policies filter block diplays as expected on manage hosts page
// cy.get("tbody").within(() => {
// cy.get("tr")
// .first()
// .within(() => {
// cy.get("td").last().children().first().should("exist").click();
// });
// });
// });
// cy.get(".manage-hosts__policies-filter-block").within(() => {
// cy.findByText(
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
// ).should("exist");
// cy.findByText(/passing/i).should("not.exist");
// cy.findByText(/failing/i)
// .should("exist")
// .click();
// cy.findByText(/passing/i).should("exist");
// cy.get('img[alt="Remove policy filter"]').click();
// cy.findByText(
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
// ).should("not.exist");
// });
// // Click on policies tab to return to manage policies page
// cy.get(".site-nav-container").within(() => {
// cy.findByText(/policies/i)
// .should("exist")
// .click();
// });
// // Delete policy
// cy.get("tbody").within(() => {
// cy.get("tr")
// .first()
// .within(() => {
// cy.get(".fleet-checkbox__input").check({ force: true });
// });
// });
// cy.findByRole("button", { name: /remove/i }).click();
// cy.get(".remove-policies-modal").within(() => {
// cy.findByRole("button", { name: /cancel/i }).should("exist");
// cy.findByRole("button", { name: /remove/i })
// .should("exist")
// .click();
// });
// cy.findByText(
// /Detect Linux hosts with high severity vulnerable versions of OpenSSL/i
// ).should("not.exist");
// });
// }
// );

View file

@ -71,6 +71,14 @@ Cypress.Commands.add("seedQueries", () => {
description: "List authorized_keys for each user on the system.",
observer_can_run: false,
},
{
name:
"Detect Linux hosts with high severity vulnerable versions of OpenSSL",
query:
"SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%';",
description: "Retrieves the OpenSSL version.",
observer_can_run: false,
},
];
queries.forEach((queryForm) => {

View file

@ -35,7 +35,7 @@ interface ITableContainerProps {
actionButtonIcon?: string;
actionButtonVariant?: string;
onQueryChange: (queryData: ITableQueryData) => void;
inputPlaceHolder: string;
inputPlaceHolder?: string;
disableActionButton?: boolean;
resultsTitle: string;
additionalQueries?: string;

View file

@ -0,0 +1,11 @@
import React from "react";
const PoliciesPageWrapper = (props: {
children: React.ReactNode;
}): React.ReactNode | null => {
const { children } = props;
return children || null;
};
export default PoliciesPageWrapper;

View file

@ -0,0 +1 @@
export { default } from "./PoliciesPageWrapper";

View file

@ -12,6 +12,7 @@ import navItems from "./navItems";
import HostsIcon from "../../../../assets/images/icon-main-hosts@2x-16x16@2x.png";
import QueriesIcon from "../../../../assets/images/icon-main-queries@2x-16x16@2x.png";
import PacksIcon from "../../../../assets/images/icon-main-packs@2x-16x16@2x.png";
import PoliciesIcon from "../../../../assets/images/icon-main-policies-16x16@2x.png";
import AdminIcon from "../../../../assets/images/icon-main-settings@2x-16x16@2x.png";
class SiteTopNav extends Component {
@ -62,6 +63,14 @@ class SiteTopNav extends Component {
icon = (
<img src={PacksIcon} alt={`${iconName} icon`} className={iconClasses} />
);
else if (iconName === "policies")
icon = (
<img
src={PoliciesIcon}
alt={`${iconName} icon`}
className={iconClasses}
/>
);
else if (iconName === "settings")
icon = (
<img src={AdminIcon} alt={`${iconName} icon`} className={iconClasses} />

View file

@ -55,6 +55,15 @@ export default (currentUser) => {
pathname: PATHS.MANAGE_SCHEDULE,
},
},
{
icon: "policies",
name: "Policies",
iconName: "policies",
location: {
regex: new RegExp(`^${URL_PREFIX}/(policies)/`),
pathname: PATHS.MANAGE_POLICIES,
},
},
];
if (permissionUtils.isGlobalAdmin(currentUser)) {

View file

@ -30,6 +30,7 @@ export default {
PACKS: "/v1/fleet/packs",
PERFORM_REQUIRED_PASSWORD_RESET: "/v1/fleet/perform_required_password_reset",
QUERIES: "/v1/fleet/queries",
POLICIES: "/v1/fleet/global/policies",
RESET_PASSWORD: "/v1/fleet/reset_password",
RUN_QUERY: "/v1/fleet/queries/run",
SCHEDULED_QUERIES: "/v1/fleet/schedule",

View file

@ -508,7 +508,7 @@ const inGigaBytes = (bytes: number): string => {
return (bytes / BYTES_PER_GIGABYTE).toFixed(1);
};
const inMilliseconds = (nanoseconds: number): number => {
export const inMilliseconds = (nanoseconds: number): number => {
return nanoseconds / NANOSECONDS_PER_MILLISECOND;
};
@ -639,6 +639,7 @@ export default {
humanHostDetailUpdated,
hostTeamName,
humanQueryLastRun,
inMilliseconds,
licenseExpirationWarning,
secondsToHms,
secondsToDhms,

View file

@ -0,0 +1,7 @@
export interface IPolicy {
id: number;
query_id: number;
query_name: string;
passing_host_count: number;
failing_host_count: number;
}

View file

@ -2,7 +2,7 @@ import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { push, goBack } from "react-router-redux";
import { find, isEmpty, omit } from "lodash";
import { find, isEmpty, memoize, omit } from "lodash";
import Button from "components/buttons/Button";
import Dropdown from "components/forms/fields/Dropdown";
@ -28,8 +28,12 @@ import PATHS from "router/paths";
import deepDifference from "utilities/deep_difference";
import hostClient from "services/entities/hosts";
import policiesClient from "services/entities/policies";
import permissionUtils from "utilities/permissions";
import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
import { getNextLocationPath } from "./helpers";
import {
defaultHiddenColumns,
@ -40,11 +44,13 @@ import EnrollSecretModal from "./components/EnrollSecretModal";
import AddHostModal from "./components/AddHostModal";
import NoHosts from "./components/NoHosts";
import EmptyHosts from "./components/EmptyHosts";
import PoliciesFilter from "./components/PoliciesFilter";
import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
import TransferHostModal from "./components/TransferHostModal";
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
import TrashIcon from "../../../../assets/images/icon-trash-14x14@2x.png";
import CloseIcon from "../../../../assets/images/icon-close-fleet-black-16x16@2x.png";
const baseClass = "manage-hosts";
@ -116,11 +122,12 @@ export class ManageHostsPage extends PureComponent {
canEnrollHosts: PropTypes.bool,
canAddNewLabels: PropTypes.bool,
teams: PropTypes.arrayOf(teamInterface),
loadingTeams: PropTypes.bool,
isGlobalAdmin: PropTypes.bool,
isOnGlobalTeam: PropTypes.bool,
isBasicTier: PropTypes.bool,
currentUser: userInterface,
policyId: PropTypes.number,
policyResponse: PolicyResponse,
};
static defaultProps = {
@ -171,21 +178,42 @@ export class ManageHostsPage extends PureComponent {
sortBy: initialSortBy,
isConfigLoaded: !isEmpty(this.props.config),
isTeamsLoaded: !isEmpty(this.props.teams),
isTeamsLoading: false,
policyName: null,
};
}
componentDidMount() {
const { dispatch } = this.props;
const { dispatch, policyId } = this.props;
dispatch(getLabels());
if (policyId) {
policiesClient
.load(policyId)
.then((response) => {
const { query_name: policyName } = response.policy;
this.setState({ policyName });
})
.catch((err) => {
console.log(err);
// dispatch(
// renderFlash(
// "error",
// "Sorry, we could not retrieve the policy name."
// )
// );
});
}
}
componentWillReceiveProps() {
const { config, dispatch, isBasicTier, loadingTeams } = this.props;
const { isConfigLoaded, isTeamsLoaded } = this.state;
const { config, dispatch, isBasicTier } = this.props;
const { isConfigLoaded, isTeamsLoaded, isTeamsLoading } = this.state;
if (!isConfigLoaded && !isEmpty(config)) {
this.setState({ isConfigLoaded: true });
}
if (isConfigLoaded && isBasicTier && !isTeamsLoaded && !loadingTeams) {
if (isConfigLoaded && isBasicTier && !isTeamsLoaded && !isTeamsLoading) {
this.setState({ isTeamsLoading: true });
dispatch(teamActions.loadAll({}))
.then(() => {
this.setState({
@ -201,6 +229,11 @@ export class ManageHostsPage extends PureComponent {
this.setState({
isTeamsLoaded: false,
});
})
.finally(() => {
this.setState({
isTeamsLoading: false,
});
});
}
}
@ -278,6 +311,8 @@ export class ManageHostsPage extends PureComponent {
const { getValidatedTeamId, retrieveHosts } = this;
const {
dispatch,
policyId,
policyResponse,
routeTemplate,
routeParams,
selectedFilters,
@ -307,7 +342,9 @@ export class ManageHostsPage extends PureComponent {
selectedLabels: selectedFilters,
globalFilter: searchQuery,
sortBy,
teamId,
teamId: selectedTeam,
policyId,
policyResponse,
});
// Rebuild queryParams to dispatch new browser location to react-router
@ -328,6 +365,12 @@ export class ManageHostsPage extends PureComponent {
if (teamId) {
queryParams.team_id = teamId;
}
if (policyId) {
queryParams.policy_id = policyId;
}
if (policyResponse) {
queryParams.policy_response = policyResponse;
}
dispatch(
push(
@ -458,6 +501,8 @@ export class ManageHostsPage extends PureComponent {
selectedFilters,
selectedLabel,
selectedTeam: selectedTeamFilter,
policyId,
policyResponse,
} = this.props;
const {
selectedHostIds,
@ -498,6 +543,8 @@ export class ManageHostsPage extends PureComponent {
globalFilter: searchQuery,
sortBy,
teamId: selectedTeamFilter,
policyId,
policyResponse,
});
})
.catch(() => {
@ -521,6 +568,18 @@ export class ManageHostsPage extends PureComponent {
return selectedFilters.find((f) => !f.includes(LABEL_SLUG_PREFIX));
};
getSortedTeamOptions = memoize((teams) =>
teams
.map((team) => {
return {
disabled: false,
label: team.name,
value: team.id,
};
})
.sort((a, b) => sortUtils.caseInsensitiveAsc(b.label, a.label))
);
getValidatedTeamId = (teamId) => {
const { currentUser, isOnGlobalTeam, teams } = this.props;
@ -545,6 +604,7 @@ export class ManageHostsPage extends PureComponent {
generateTeamFilterDropdownOptions = (teams) => {
const { currentUser, isOnGlobalTeam } = this.props;
const { getSortedTeamOptions } = this;
let currentUserTeams = [];
if (isOnGlobalTeam) {
@ -561,26 +621,7 @@ export class ManageHostsPage extends PureComponent {
},
];
const sortedCurrentUserTeamOptions = currentUserTeams
.map((team) => {
return {
disabled: false,
label: team.name,
value: team.id,
};
})
.sort((a, b) => {
const labelA = a.label.toUpperCase();
const labelB = b.label.toUpperCase();
if (labelA < labelB) {
return -1;
}
if (labelA > labelB) {
return 1;
}
return 0; // values are equal
});
const sortedCurrentUserTeamOptions = getSortedTeamOptions(currentUserTeams);
return allTeamsOption.concat(sortedCurrentUserTeamOptions);
};
@ -593,7 +634,7 @@ export class ManageHostsPage extends PureComponent {
options = {
...options,
team_id: getValidatedTeamId(options.teamId),
teamId: getValidatedTeamId(options.teamId),
};
try {
@ -657,11 +698,79 @@ export class ManageHostsPage extends PureComponent {
}
};
handleChangePoliciesFilter = (policyResponse) => {
const {
dispatch,
policyId,
routeTemplate,
routeParams,
queryParams,
selectedFilters,
selectedTeam,
} = this.props;
const { searchQuery, sortBy } = this.state;
const { retrieveHosts } = this;
retrieveHosts({
globalFilter: searchQuery,
policyId,
policyResponse,
selectedLabels: selectedFilters,
sortBy,
teamId: selectedTeam,
});
dispatch(
push(
getNextLocationPath({
pathPrefix: PATHS.MANAGE_HOSTS,
routeTemplate,
routeParams,
queryParams: Object.assign({}, queryParams, {
policy_id: policyId,
policy_response: policyResponse,
}),
})
)
);
};
handleClearPoliciesFilter = () => {
const {
dispatch,
routeTemplate,
routeParams,
queryParams,
selectedFilters,
selectedTeam,
} = this.props;
const { searchQuery, sortBy } = this.state;
const { retrieveHosts } = this;
retrieveHosts({
globalFilter: searchQuery,
selectedLabels: selectedFilters,
sortBy,
teamId: selectedTeam,
});
dispatch(
push(
getNextLocationPath({
pathPrefix: PATHS.MANAGE_HOSTS,
routeTemplate,
routeParams,
queryParams: omit(queryParams, ["policy_id", "policy_response"]),
})
)
);
};
// The handleChange method below is for the filter-by-team dropdown rather than the dropdown used in modals
// TODO confirm that sort order, pagination work as expected
handleChangeSelectedTeamFilter = (selectedTeam) => {
const {
dispatch,
policyId,
policyResponse,
selectedFilters,
routeTemplate,
routeParams,
@ -678,6 +787,8 @@ export class ManageHostsPage extends PureComponent {
selectedLabels: selectedFilters,
globalFilter: searchQuery,
sortBy,
policyId,
policyResponse,
};
retrieveHosts(hostsOptions);
@ -693,8 +804,7 @@ export class ManageHostsPage extends PureComponent {
};
handleLabelChange = ({ slug }) => {
const { dispatch, selectedFilters, selectedTeam } = this.props;
const { getValidatedTeamId } = this;
const { dispatch, queryParams, selectedFilters } = this.props;
const { MANAGE_HOSTS } = PATHS;
const isAllHosts = slug === ALL_HOSTS_LABEL;
const newFilters = [...selectedFilters];
@ -720,15 +830,22 @@ export class ManageHostsPage extends PureComponent {
}
}
let nextLocation = isAllHosts
? MANAGE_HOSTS
: `${MANAGE_HOSTS}/${newFilters.join("/")}`;
const teamIdParam = getValidatedTeamId(selectedTeam);
if (teamIdParam) {
nextLocation += `?team_id=${teamIdParam}`;
// Non-status labels are not compatible with policies so omit policy params from next location
let newQueryParams = queryParams;
if (newFilters.find((f) => f.includes(LABEL_SLUG_PREFIX))) {
newQueryParams = omit(newQueryParams, ["policy_id", "policy_response"]);
}
dispatch(push(nextLocation));
dispatch(
push(
getNextLocationPath({
pathPrefix: isAllHosts
? MANAGE_HOSTS
: `${MANAGE_HOSTS}/${newFilters.join("/")}`,
queryParams: newQueryParams,
})
)
);
};
handleStatusDropdownChange = (statusName) => {
@ -743,7 +860,6 @@ export class ManageHostsPage extends PureComponent {
handleLabelChange(selected);
};
// TODO revisit UX for server errors for invalid team_id (e.g., team_id=0, team_id=null, team_id=foo, etc.)
renderTeamsFilterDropdown = () => {
const { isBasicTier, selectedTeam, teams } = this.props;
const { isConfigLoaded, isTeamsLoaded } = this.state;
@ -780,6 +896,26 @@ export class ManageHostsPage extends PureComponent {
);
};
renderPoliciesFilterBlock = () => {
const { policyId, policyResponse } = this.props;
const { policyName } = this.state;
const { handleClearPoliciesFilter, handleChangePoliciesFilter } = this;
return (
<div className={`${baseClass}__policies-filter-block`}>
<PoliciesFilter
policyId={policyId}
policyResponse={policyResponse}
onChange={handleChangePoliciesFilter}
/>
<p>{policyName}</p>
<Button onClick={handleClearPoliciesFilter} variant={"text-icon"}>
<img src={CloseIcon} alt="Remove policy filter" />
</Button>
</div>
);
};
renderEditColumnsModal = () => {
const { config, currentUser } = this.props;
const { showEditColumnsModal, hiddenColumns } = this.state;
@ -927,20 +1063,34 @@ export class ManageHostsPage extends PureComponent {
};
renderHeader = () => {
const { renderHeaderLabelBlock, renderTeamsFilterDropdown } = this;
const { selectedLabel } = this.props;
const type = selectedLabel?.type;
const { renderTeamsFilterDropdown } = this;
return (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
{renderTeamsFilterDropdown()}
{type !== "all" &&
</div>
</div>
);
};
renderLabelOrPolicyBlock = () => {
const { renderHeaderLabelBlock, renderPoliciesFilterBlock } = this;
const { policyId, selectedLabel } = this.props;
const type = selectedLabel?.type;
if (policyId || selectedLabel) {
return (
<div className={`${baseClass}__labels-policies-wrap`}>
{policyId && renderPoliciesFilterBlock()}
{!policyId &&
type !== "all" &&
type !== "status" &&
selectedLabel &&
renderHeaderLabelBlock(selectedLabel)}
</div>
</div>
);
);
}
};
renderForm = () => {
@ -1111,6 +1261,7 @@ export class ManageHostsPage extends PureComponent {
const {
renderForm,
renderHeader,
renderLabelOrPolicyBlock,
renderSidePanel,
renderAddHostModal,
renderEnrollSecretModal,
@ -1158,6 +1309,7 @@ export class ManageHostsPage extends PureComponent {
)}
</div>
</div>
{renderLabelOrPolicyBlock()}
{isConfigLoaded && (!isBasicTier || isTeamsLoaded) && renderTable()}
</div>
)}
@ -1178,6 +1330,9 @@ const mapStateToProps = (state, ownProps) => {
const queryParams = location.query;
const routeTemplate = route && route.path ? route.path : "";
const policyId = queryParams?.policy_id;
const policyResponse = queryParams?.policy_response;
const { active_label: activeLabel, label_id: labelID } = params;
const selectedFilters = [];
@ -1208,7 +1363,6 @@ const mapStateToProps = (state, ownProps) => {
const config = state.app.config;
const teams = memoizedGetEntity(state.entities.teams.data);
const loadingTeams = state.entities.teams.loading;
// If there is no team_id, set selectedTeam to 0 so dropdown defaults to "All teams"
const selectedTeam = location.query?.team_id || 0;
@ -1252,8 +1406,9 @@ const mapStateToProps = (state, ownProps) => {
isOnGlobalTeam,
isBasicTier,
teams,
loadingTeams,
selectedTeam,
policyId,
policyResponse,
};
};

View file

@ -4,6 +4,10 @@
align-items: flex-start;
justify-content: space-between;
margin-bottom: $pad-medium;
.button-wrap {
min-width: 266px;
}
}
.ace-fleet {
@ -13,6 +17,7 @@
&__header {
display: flex;
align-items: center;
min-width: 125px;
.form-field {
margin-bottom: 0;
@ -53,8 +58,6 @@
}
&__label-block {
margin-top: $pad-medium;
.title {
display: flex;
align-items: center;
@ -96,6 +99,10 @@
}
}
.table-container {
padding-top: $pad-small;
}
.data-table-container {
.data-table {
&__wrapper {
@ -153,9 +160,7 @@
left: -12px;
border-radius: 6px;
}
}
&__header {
.Select-control {
background-color: #fff;
border: 0 !important;
@ -217,6 +222,21 @@
}
}
&__labels-policies-wrap {
margin-bottom: $pad-medium;
}
&__policies-filter-block {
display: flex;
align-items: center;
p {
font-size: $x-small;
font-weight: $bold;
padding-left: $pad-medium;
}
}
&__enroll-hosts {
padding: $pad-small;
margin-right: $pad-small;

View file

@ -0,0 +1,46 @@
import React from "react";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { PolicyResponse } from "utilities/constants";
interface IPoliciesFilterProps {
policyResponse: PolicyResponse;
onChange: (selectedFilter: PolicyResponse) => void;
}
const baseClass = "policies-filter";
const POLICY_RESPONSE_OPTIONS = [
{
disabled: false,
label: "Passing",
value: PolicyResponse.PASSING,
helpText: "Hosts that passed the last time they checked into Fleet.",
},
{
disabled: false,
label: "Failing",
value: PolicyResponse.FAILING,
helpText: "Hosts that failed the last time they checked into Fleet.",
},
];
const PoliciesFilter = (props: IPoliciesFilterProps): JSX.Element => {
const { onChange, policyResponse } = props;
const value = policyResponse;
return (
<div className={`${baseClass}__policies-block`}>
<Dropdown
value={value}
className={`${baseClass}__status_dropdown`}
options={POLICY_RESPONSE_OPTIONS}
searchable={false}
onChange={onChange}
/>
</div>
);
};
export default PoliciesFilter;

View file

@ -0,0 +1,18 @@
.policies-filter {
display: flex;
justify-content: flex-start;
align-items: center;
&__status_dropdown {
width: 159px;
.Select-menu-outer {
width: 364px;
max-height: 310px;
.Select-menu {
max-height: none;
}
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./PoliciesFilter";

View file

@ -0,0 +1,262 @@
import React, { useState, useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
// @ts-ignore
import { IConfig } from "interfaces/config";
import { IQuery } from "interfaces/query";
import { IPolicy } from "interfaces/policy";
import configAPI from "services/entities/config";
import policiesAPI from "services/entities/policies";
// @ts-ignore
import queryActions from "redux/nodes/entities/queries/actions";
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import { inMilliseconds, secondsToHms } from "fleet/helpers";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
import PolicyError from "./components/PolicyError";
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import AddPolicyModal from "./components/AddPolicyModal";
import RemovePoliciesModal from "./components/RemovePoliciesModal";
const baseClass = "manage-policies-page";
const DOCS_LINK =
"https://github.com/fleetdm/fleet/blob/fleet-v4.3.0/docs/2-Deploying/2-Configuration.md#osquery_detail_update_interval";
interface IRootState {
app: {
config: IConfig;
};
entities: {
queries: {
isLoading: boolean;
data: IQuery[];
};
};
}
const renderTable = (
policiesList: IPolicy[],
isLoading: boolean,
isLoadingError: boolean,
onRemovePoliciesClick: (selectedTableIds: number[]) => void,
toggleAddPolicyModal: () => void
): JSX.Element => {
if (isLoadingError) {
return <PolicyError />;
}
return (
<PoliciesListWrapper
policiesList={policiesList}
isLoading={isLoading}
onRemovePoliciesClick={onRemovePoliciesClick}
toggleAddPolicyModal={toggleAddPolicyModal}
/>
);
};
const ManagePolicyPage = (): JSX.Element => {
const dispatch = useDispatch();
const queries = useSelector((state: IRootState) => state.entities.queries);
const queriesList = Object.values(queries.data);
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [selectedIds, setSelectedIds] = useState<number[] | never[]>([]);
const [updateInterval, setUpdateInterval] = useState<string>(
"update interval"
);
const [policies, setPolicies] = useState<IPolicy[] | never[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingError, setIsLoadingError] = useState(false);
const getPolicies = useCallback(async () => {
setIsLoading(true);
try {
const response = await policiesAPI.loadAll();
setPolicies(response.policies);
} catch (error) {
console.log(error);
dispatch(
renderFlash("error", "Sorry, we could not retrieve your policies.")
);
setIsLoadingError(true);
} finally {
setIsLoading(false);
}
}, [dispatch]);
const getInterval = useCallback(async () => {
try {
const response = await configAPI.loadAll();
const interval = secondsToHms(
inMilliseconds(response.update_interval.osquery_detail) / 1000
);
setUpdateInterval(interval);
} catch (error) {
console.log(error);
dispatch(
renderFlash(
"error",
"Sorry, we could not retrieve your update interval."
)
);
}
}, [dispatch]);
useEffect(() => {
getPolicies();
getInterval();
}, [getInterval, getPolicies]);
useEffect(() => {
dispatch(queryActions.loadAll());
}, [dispatch]);
const toggleAddPolicyModal = useCallback(() => {
setShowAddPolicyModal(!showAddPolicyModal);
}, [showAddPolicyModal, setShowAddPolicyModal]);
const toggleRemovePoliciesModal = useCallback(() => {
setShowRemovePoliciesModal(!showRemovePoliciesModal);
}, [showRemovePoliciesModal, setShowRemovePoliciesModal]);
// TODO typing for mouse event?
const onRemovePoliciesClick = useCallback(
(selectedTableIds: number[]): void => {
toggleRemovePoliciesModal();
setSelectedIds(selectedTableIds);
},
[toggleRemovePoliciesModal]
);
const onRemovePoliciesSubmit = useCallback(() => {
const ids = selectedIds;
policiesAPI
.destroy(ids)
.then(() => {
dispatch(
renderFlash(
"success",
`Successfully removed ${
ids && ids.length === 1 ? "policy" : "policies"
}.`
)
);
})
.catch(() => {
dispatch(
renderFlash(
"error",
`Unable to remove ${
ids && ids.length === 1 ? "policy" : "policies"
}. Please try again.`
)
);
})
.finally(() => {
toggleRemovePoliciesModal();
getPolicies();
});
}, [dispatch, getPolicies, selectedIds, toggleRemovePoliciesModal]);
const onAddPolicySubmit = useCallback(
(query_id: number | undefined) => {
if (!query_id) {
dispatch(
renderFlash("error", "Could not add policy. Please try again.")
);
console.log("Missing query id; cannot add policy");
return false;
}
policiesAPI
.create(query_id)
.then(() => {
dispatch(renderFlash("success", `Successfully added policy.`));
})
.catch(() => {
dispatch(
renderFlash("error", "Could not add policy. Please try again.")
);
})
.finally(() => {
toggleAddPolicyModal();
getPolicies();
});
return false;
},
[dispatch, getPolicies, toggleAddPolicyModal]
);
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper body-wrap`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<h1 className={`${baseClass}__title`}>
<span>Policies</span>
</h1>
</div>
</div>
{policies && policies.length !== 0 && !isLoadingError && (
<div className={`${baseClass}__action-button-container`}>
<Button
variant="brand"
className={`${baseClass}__add-policy-button`}
onClick={toggleAddPolicyModal}
>
Add a policy
</Button>
</div>
)}
</div>
<div className={`${baseClass}__description`}>
<p>Policy queries report which hosts are compliant.</p>
</div>
{policies && policies.length !== 0 && !isLoadingError && (
<InfoBanner className={`${baseClass}__sandbox-info`}>
<p>
Your policies are checked every <b>{updateInterval.trim()}</b>.
Check out the Fleet documentation on{" "}
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
<b>how to edit this frequency</b>
</a>
.
</p>
</InfoBanner>
)}
<div>
{renderTable(
policies,
isLoading,
isLoadingError,
onRemovePoliciesClick,
toggleAddPolicyModal
)}
</div>
{showAddPolicyModal && (
<AddPolicyModal
onCancel={toggleAddPolicyModal}
onSubmit={onAddPolicySubmit}
allQueries={queriesList}
/>
)}
{showRemovePoliciesModal && (
<RemovePoliciesModal
onCancel={toggleRemovePoliciesModal}
onSubmit={onRemovePoliciesSubmit}
/>
)}
</div>
</div>
);
};
export default ManagePolicyPage;

View file

@ -0,0 +1,197 @@
.manage-policies-page {
&__header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $pad-small;
}
&__header {
display: flex;
align-items: center;
.form-field {
margin-bottom: 0;
}
}
&__text {
margin-right: $pad-large;
}
&__title {
font-size: $large;
.fleeticon {
color: $core-fleet-blue;
margin-right: 15px;
}
.fleeticon-success-check {
color: $ui-success;
}
.fleeticon-offline {
color: $ui-error;
}
.fleeticon-mia {
color: $core-fleet-black;
}
}
&__description {
margin: 0;
padding-top: $pad-xsmall;
h2 {
text-transform: uppercase;
color: $core-fleet-black;
font-weight: $regular;
font-size: $small;
}
p {
color: $core-dark-blue-grey;
margin: 0;
font-size: $x-small;
font-style: italic;
}
}
&__action-button-container {
display: flex;
align-items: flex-start;
}
.button {
font-size: $x-small;
img {
transform: scale(0.5);
}
}
&__advanced-button {
margin-right: $pad-medium;
}
&__sandbox-info {
margin-top: $pad-large;
margin-bottom: $pad-xxlarge;
p {
font-size: $x-small;
margin: 0;
margin-bottom: $pad-medium;
}
p:last-child {
margin-bottom: 0;
}
a {
color: $core-vibrant-blue;
text-decoration: none;
}
}
&__modal-buttons {
width: 100%;
display: flex;
justify-content: flex-end;
.button:first-child {
margin-right: $pad-medium;
}
}
&__team-dropdown {
border: 0 !important;
position: relative;
:hover {
cursor: pointer !important;
}
&.is-focused {
.Select-control {
border: 0 !important;
height: 32px;
}
}
.Select-menu-outer {
position: absolute;
left: -12px;
border-radius: 6px;
}
}
.Select.is-open {
.Select-value-label {
color: $core-vibrant-blue !important;
}
}
&__header {
.Select-control {
background-color: #fff;
border: 0 !important;
border-radius: none;
position: none;
width: max-content; // move select arrow
height: 20px;
&:hover {
box-shadow: none;
}
&:hover .Select-value-label {
color: $core-vibrant-blue !important;
}
.Select-arrow-zone {
padding-left: $pad-small;
.Select-arrow {
top: 1px !important;
margin-top: 0 !important;
}
}
.Select-multi-value-wrapper {
width: max-content; // move select arrow
height: 20px;
margin-bottom: $pad-xsmall;
.Select-input {
display: none !important;
}
.Select-value {
position: relative; // move select arrow
display: inline-block; // move select arrow
line-height: 28px;
padding: 0;
border: 0 !important;
background-color: #fff !important;
right: 0;
left: 0;
bottom: 0;
top: 0;
&.is-focused {
border: 0 !important;
}
:hover {
border: 0 !important;
}
.Select-value-label {
font-size: $large !important;
}
}
}
}
}
}

View file

@ -0,0 +1,92 @@
/* This component is used for creating policies */
import React, { useState, useCallback } from "react";
// @ts-ignore
import Modal from "components/modals/Modal";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { IQuery } from "interfaces/query";
const baseClass = "add-policy-modal";
interface IAddPolicyModalProps {
allQueries: IQuery[];
onCancel: () => void;
onSubmit: (query_id: number | undefined) => void;
}
const AddPolicyModal = ({
onCancel,
onSubmit,
allQueries,
}: IAddPolicyModalProps): JSX.Element => {
const [selectedQuery, setSelectedQuery] = useState<number>();
const createQueryDropdownOptions = () => {
return allQueries.map(({ id, name }) => ({
value: id,
label: name,
}));
};
const onChangeSelectQuery = useCallback(
(queryId: number) => {
setSelectedQuery(queryId);
},
[setSelectedQuery]
);
return (
<Modal title={"Add a policy"} onExit={onCancel} className={baseClass}>
<form className={`${baseClass}__form`}>
<Dropdown
searchable
options={createQueryDropdownOptions()}
onChange={onChangeSelectQuery}
placeholder={"Select query"}
value={selectedQuery}
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
/>
<InfoBanner className={`${baseClass}__sandbox-info`}>
<p>
Host that return results for the selected query are <b>Passing</b>.
</p>
<p>
Hosts that do not return results for the selected query are{" "}
<b>Failing</b>.
</p>
<p>
To test which hosts return results, it is recommened to first run
your query as a live query by heading to <b>Queries</b> and then
selecting a query.
</p>
</InfoBanner>
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
type="button"
variant="brand"
onClick={() => onSubmit(selectedQuery)}
disabled={!selectedQuery}
>
Add
</Button>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="inverse"
>
Cancel
</Button>
</div>
</form>
</Modal>
);
};
export default AddPolicyModal;

View file

@ -0,0 +1,71 @@
.add-policy-modal {
&__sandbox-info {
margin-top: $pad-medium;
margin-bottom: $pad-large;
p {
margin: 0;
margin-bottom: $pad-medium;
}
p:last-child {
margin-bottom: 0;
}
}
a {
color: $core-vibrant-blue;
font-weight: $regular;
font-size: $x-small;
text-decoration: none;
}
&__info-header {
font-weight: $bold;
}
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
}
&__btn {
margin-left: 12px;
}
&__advanced-options-button {
margin: $pad-medium 0;
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
.downcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 2px;
}
}
.upcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(180deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 4px;
margin-left: 14px;
}
}
.Select-value-label {
font-size: $small;
}
}

View file

@ -0,0 +1 @@
export { default } from "./AddPolicyModal";

View file

@ -0,0 +1,78 @@
import React from "react";
import { noop } from "lodash";
import Button from "components/buttons/Button";
import { IPolicy } from "interfaces/policy";
import TableContainer from "components/TableContainer";
import { generateTableHeaders, generateDataSet } from "./PoliciesTableConfig";
const baseClass = "policies-list-wrapper";
const noPoliciesClass = "no-policies";
interface IPoliciesListWrapperProps {
policiesList: IPolicy[];
isLoading: boolean;
onRemovePoliciesClick: (selectedTableIds: number[]) => void;
toggleAddPolicyModal: () => void;
}
const PoliciesListWrapper = (props: IPoliciesListWrapperProps): JSX.Element => {
const {
policiesList,
isLoading,
onRemovePoliciesClick,
toggleAddPolicyModal,
} = props;
const NoPolicies = () => {
return (
<div className={`${noPoliciesClass}`}>
<div className={`${noPoliciesClass}__inner`}>
<div className={`${noPoliciesClass}__inner-text`}>
<h2>You don&apos;t have any policies.</h2>
<p>
Policies allow you to monitor which devices meet a certain
standard.
</p>
<div className={`${noPoliciesClass}__-cta-buttons`}>
<Button
variant="brand"
className={`${noPoliciesClass}__add-policy-button`}
onClick={toggleAddPolicyModal}
>
Add a policy
</Button>
</div>
</div>
</div>
</div>
);
};
const tableHeaders = generateTableHeaders();
return (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle={"queries"}
columns={tableHeaders}
data={generateDataSet(policiesList)}
isLoading={isLoading}
defaultSortHeader={"query_name"}
defaultSortDirection={"asc"}
manualSortBy
showMarkAllPages={false}
isAllPagesSelected={false}
disablePagination
onPrimarySelectActionClick={onRemovePoliciesClick}
primarySelectActionButtonVariant="text-icon"
primarySelectActionButtonIcon="close"
primarySelectActionButtonText={"Remove"}
emptyComponent={NoPolicies}
onQueryChange={noop}
/>
</div>
);
};
export default PoliciesListWrapper;

View file

@ -0,0 +1,143 @@
/* eslint-disable react/prop-types */
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { memoize } from "lodash";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { IPolicy } from "interfaces/policy";
import PATHS from "router/paths";
import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)}
const TAGGED_TEMPLATES = {
hostsByStatusRoute: (id: number, status: PolicyResponse) => {
return `?policy_id=${id}&policy_response=${status}`;
},
};
interface IHeaderProps {
column: {
title: string;
isSortedDesc: boolean;
};
getToggleAllRowsSelectedProps: () => any; // TODO: do better with types
toggleAllRowsSelected: () => void;
}
interface ICellProps {
cell: {
value: any;
};
row: {
original: IPolicy;
getToggleRowSelectedProps: () => any; // TODO: do better with types
toggleRowSelected: () => void;
};
}
interface IDataColumn {
Header: ((props: IHeaderProps) => JSX.Element) | string;
Cell: (props: ICellProps) => JSX.Element;
id?: string;
title?: string;
accessor?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
}
// interface IPoliciesTableData {
// name: string;
// passing: number;
// failing: number;
// id: number;
// query_id: number;
// query_name: string;
// }
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateTableHeaders = (): IDataColumn[] => {
return [
{
id: "selection",
Header: (cellProps: IHeaderProps): JSX.Element => {
const props = cellProps.getToggleAllRowsSelectedProps();
const checkboxProps = {
value: props.checked,
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
};
return <Checkbox {...checkboxProps} />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const props = cellProps.row.getToggleRowSelectedProps();
const checkboxProps = {
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
return <Checkbox {...checkboxProps} />;
},
disableHidden: true,
},
{
title: "Query",
Header: "Query",
disableSortBy: true,
// sortType: "caseInsensitive",
accessor: "query_name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Passing",
Header: "Passing",
disableSortBy: true,
accessor: "passing_host_count",
Cell: (cellProps: ICellProps): JSX.Element => (
<LinkCell
value={`${cellProps.cell.value} hosts`}
path={
PATHS.MANAGE_HOSTS +
TAGGED_TEMPLATES.hostsByStatusRoute(
cellProps.row.original.id,
PolicyResponse.PASSING
)
}
/>
),
},
{
title: "Failing",
Header: "Failing",
disableSortBy: true,
accessor: "failing_host_count",
Cell: (cellProps: ICellProps): JSX.Element => (
<LinkCell
value={`${cellProps.cell.value} hosts`}
path={
PATHS.MANAGE_HOSTS +
TAGGED_TEMPLATES.hostsByStatusRoute(
cellProps.row.original.id,
PolicyResponse.FAILING
)
}
/>
),
},
];
};
const generateDataSet = memoize((all_policies: IPolicy[] = []): IPolicy[] => {
all_policies = all_policies.sort((a, b) =>
sortUtils.caseInsensitiveAsc(b.query_name, a.query_name)
);
return all_policies;
});
export { generateTableHeaders, generateDataSet };

View file

@ -0,0 +1,125 @@
.policy-list-wrapper {
border-collapse: collapse;
&__wrapper {
border: 1px solid $ui-fleet-blue-15;
border-radius: 4px;
overflow: hidden;
margin-top: $pad-medium;
}
thead {
background-color: $ui-off-white;
border-bottom: 1px solid $ui-fleet-blue-15;
th {
font-size: $x-small;
font-weight: $bold;
text-align: left;
padding: $pad-medium $pad-large;
&:nth-child(1) {
border-top-left-radius: 6px;
width: 20px;
}
&:nth-child(2) {
width: calc(49% - 20px);
}
&:nth-child(3) {
width: 20%;
max-width: 150px;
}
&:nth-last-child(2) {
border-right: 0;
}
&:last-child {
width: 150px;
border-top-right-radius: 6px;
border-left: 0;
}
}
}
&__th-pack-name {
padding-left: 0;
text-align: left;
}
&__select-all {
margin-bottom: 0;
}
&__empty-table {
text-align: center;
font-size: $x-small;
color: $core-fleet-black;
}
&__policy-count {
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
margin: 0 12px 0 0;
display: inline-block;
}
}
.no-policies {
display: flex;
flex-direction: column;
align-items: center;
margin-top: $pad-xxxlarge;
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;
}
&__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;
}
}
&__inner-text {
padding-left: $pad-xlarge;
width: 485px;
}
&__add-policy-button {
margin-right: $pad-small;
}
}

View file

@ -0,0 +1 @@
export { default } from "./PoliciesListWrapper";

View file

@ -0,0 +1,38 @@
/**
* Component when there is an error retrieving policy set up in fleet
*/
import React from "react";
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../../../../assets/images/icon-error-16x16@2x.png";
const baseClass = "policy-error";
const PolicyError = (): JSX.Element => {
return (
<div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}>
<div className="info">
<span className="info__header">
<img src={ErrorIcon} alt="error icon" id="error-icon" />
Something&apos;s gone wrong.
</span>
<span className="info__data">Refresh the page or log in again.</span>
<span className="info__data">
If this keeps happening, please&nbsp;
<a
href="https://github.com/fleetdm/fleet/issues"
target="_blank"
rel="noopener noreferrer"
>
file an issue.
<img src={OpenNewTabIcon} alt="open new tab" id="new-tab-icon" />
</a>
</span>
</div>
</div>
</div>
);
};
export default PolicyError;

View file

@ -0,0 +1,48 @@
.policy-error {
display: flex;
flex-direction: column;
align-items: center;
margin-top: $pad-xxxlarge;
#error-icon {
height: 12px;
width: 12px;
margin-right: 8px;
}
#new-tab-icon {
height: 12px;
width: 12px;
margin-left: 6px;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
&__inner {
display: flex;
flex-direction: row;
}
.info {
&__header {
display: block;
color: $core-fleet-black;
font-weight: $bold;
font-size: $x-small;
text-align: left;
}
&__data {
display: block;
color: $core-fleet-black;
font-weight: normal;
font-size: $x-small;
text-align: left;
margin-top: 10px;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./PolicyError";

View file

@ -0,0 +1,42 @@
import React from "react";
import Modal from "components/modals/Modal";
import Button from "components/buttons/Button";
const baseClass = "remove-policies-modal";
interface IRemovePoliciesModalProps {
onCancel: () => void;
onSubmit: () => void;
}
const RemovePoliciesModal = (props: IRemovePoliciesModalProps): JSX.Element => {
const { onCancel, onSubmit } = props;
return (
<Modal title={"Remove policies"} onExit={onCancel} className={baseClass}>
<div className={baseClass}>
Are you sure you want to remove the selected policies?
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
type="button"
variant="alert"
onClick={onSubmit}
>
Remove
</Button>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="inverse-alert"
>
Cancel
</Button>
</div>
</div>
</Modal>
);
};
export default RemovePoliciesModal;

View file

@ -0,0 +1,13 @@
.remove-policies-modal {
font-size: $x-small;
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
margin-top: $pad-xxlarge;
}
&__btn {
margin-left: 12px;
}
}

View file

@ -0,0 +1 @@
export { default } from "./RemovePoliciesModal";

View file

@ -0,0 +1 @@
export { default } from "./ManagePoliciesPage";

View file

@ -31,9 +31,11 @@ import LogoutPage from "pages/LogoutPage";
import ManageHostsPage from "pages/hosts/ManageHostsPage";
import ManageQueriesPage from "pages/queries/ManageQueriesPage";
import ManagePacksPage from "pages/packs/ManagePacksPage";
import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
import ManageSchedulePage from "pages/schedule/ManageSchedulePage";
import PackPageWrapper from "components/packs/PackPageWrapper";
import PackComposerPage from "pages/packs/PackComposerPage";
import PoliciesPageWrapper from "components/policies/PoliciesPageWrapper";
import QueryPage from "pages/queries/QueryPage";
import QueryPageWrapper from "components/queries/QueryPageWrapper";
import RegistrationPage from "pages/RegistrationPage";
@ -110,6 +112,9 @@ const routes = (
<Route path="edit" component={EditPackPage} />
</Route>
</Route>
<Route path="policies" component={PoliciesPageWrapper}>
<Route path="manage" component={ManagePoliciesPage} />
</Route>
<Route path="schedule" component={SchedulePageWrapper}>
<Route path="manage" component={ManageSchedulePage} />
<Route

View file

@ -42,6 +42,7 @@ export default {
MANAGE_TEAM_SCHEDULE: (teamId: number): string => {
return `${URL_PREFIX}/schedule/manage/teams/${teamId}`;
},
MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`,
NEW_QUERY: `${URL_PREFIX}/queries/new`,
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,
SETUP: `${URL_PREFIX}/setup`,

View file

@ -0,0 +1,14 @@
import sendRequest from "services";
import endpoints from "fleet/endpoints";
// import { IConfig } from "interfaces/host";
// TODO add other methods from "fleet/entities/config"
export default {
loadAll: () => {
const { CONFIG } = endpoints;
const path = `${CONFIG}`;
return sendRequest("GET", path);
},
};

View file

@ -14,6 +14,8 @@ interface IHostLoadOptions {
globalFilter: string;
sortBy: ISortOption[];
teamId: number;
policyId: number;
policyResponse: string;
}
export default {
@ -43,6 +45,8 @@ export default {
const globalFilter = options?.globalFilter || "";
const sortBy = options?.sortBy || [];
const teamId = options?.teamId || null;
const policyId = options?.policyId || null;
const policyResponse = options?.policyResponse || null;
// TODO: add this query param logic to client class
const pagination = `page=${page}&per_page=${perPage}`;
@ -92,6 +96,11 @@ export default {
path += `&team_id=${teamId}`;
}
if (!label && policyId) {
path += `&policy_id=${policyId}`;
path += `&policy_response=${policyResponse || "passing"}`; // TODO confirm whether there should be a default if there is an id but no response sepcified
}
return sendRequest("GET", path);
},
transferToTeam: (teamId: number | null, hostIds: number[]) => {
@ -102,6 +111,8 @@ export default {
hosts: hostIds,
});
},
// TODO confirm interplay with policies
transferToTeamByFilter: (
teamId: number | null,
query: string,

View file

@ -0,0 +1,28 @@
import sendRequest from "services";
import endpoints from "fleet/endpoints";
// import { IPolicyFormData, IPolicy } from "interfaces/policy";
export default {
create: (query_id: number) => {
const { POLICIES } = endpoints;
return sendRequest("POST", POLICIES, { query_id });
},
destroy: (ids: number[]) => {
const { POLICIES } = endpoints;
const path = `${POLICIES}/delete`;
return sendRequest("POST", path, { ids });
},
load: (id: number) => {
const { POLICIES } = endpoints;
const path = `${POLICIES}/${id}`;
return sendRequest("GET", path);
},
loadAll: () => {
const { POLICIES } = endpoints;
return sendRequest("GET", POLICIES);
},
};

View file

@ -1,3 +1,8 @@
export enum PolicyResponse {
PASSING = "passing",
FAILING = "failing",
}
export const FREQUENCY_DROPDOWN_OPTIONS = [
{ value: 3600, label: "Every hour" },
{ value: 21600, label: "Every 6 hours" },

View file

@ -0,0 +1 @@
export { default } from "./sort_functions";

View file

@ -0,0 +1,14 @@
const caseInsensitiveAsc = (a: string, b: string): number => {
a = a.toLowerCase();
b = b.toLowerCase();
if (b > a) {
return 1;
}
if (b < a) {
return -1;
}
return 0;
};
export default { caseInsensitiveAsc };