diff --git a/changes/issue-3300-policies-not-yet-accurate b/changes/issue-3300-policies-not-yet-accurate new file mode 100644 index 0000000000..7678d32552 --- /dev/null +++ b/changes/issue-3300-policies-not-yet-accurate @@ -0,0 +1 @@ +* Indicate if a policy has not completed an initial run \ No newline at end of file diff --git a/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index 92e64a052b..6d4fa456fe 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -332,6 +332,8 @@ describe("Policies flow (seeded)", () => { cy.visit("/policies/manage"); }); it("links to manage host page filtered by policy", () => { + // Move internal clock forward 2 hours so that policies report host results + cy.clock(Date.now() + 1000 * 60 * 120); cy.getAttached(".failing_host_count__cell") .first() .within(() => { diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index c96dff4d23..073b39a2ed 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -11,7 +11,7 @@ const TextCell = ({ value, formatter = (val) => val, // identity function if no formatter is provided greyed, - classes: className = "w250", + classes = "w250", }: ITextCellProps): JSX.Element => { let val = value; @@ -20,7 +20,7 @@ const TextCell = ({ } return ( - + {formatter(val)} ); diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index a43569d180..583c83ff74 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -37,6 +37,8 @@ export interface IPolicyStats extends IPolicy { passing_host_count: number; failing_host_count: number; webhook: string; + has_run: boolean; + osquery_policy_ms: number; } // Used on the host details page and other places where the status of individual hosts are displayed diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index fd70f8e48e..52094f3a84 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -7,7 +7,6 @@ import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; -import { inMilliseconds, secondsToHms } from "fleet/helpers"; import { IPolicyStats, ILoadAllPoliciesResponse } from "interfaces/policy"; import { IWebhookFailingPolicies } from "interfaces/webhook"; import { IConfig } from "interfaces/config"; @@ -20,7 +19,6 @@ import usersAPI, { IGetMeResponse } from "services/entities/users"; import Button from "components/buttons/Button"; import RevealButton from "components/buttons/RevealButton"; -import InfoBanner from "components/InfoBanner/InfoBanner"; import Spinner from "components/Spinner"; import TeamsDropdown from "components/TeamsDropdown"; import TableDataError from "components/TableDataError"; @@ -43,16 +41,12 @@ interface IManagePoliciesPageProps { const baseClass = "manage-policies-page"; -const DOCS_LINK = - "https://fleetdm.com/docs/deploying/configuration#osquery-policy-update-interval"; - const ManagePolicyPage = ({ router, location, }: IManagePoliciesPageProps): JSX.Element => { const { availableTeams, - config, isGlobalAdmin, isGlobalMaintainer, isOnGlobalTeam, @@ -75,7 +69,6 @@ const ManagePolicyPage = ({ const { setLastEditedQueryName, setLastEditedQueryDescription, - setLastEditedQueryBody, setLastEditedQueryResolution, setLastEditedQueryPlatform, } = useContext(PolicyContext); @@ -282,17 +275,8 @@ const ManagePolicyPage = ({ }`; }; - const policyUpdateInterval = - secondsToHms( - inMilliseconds(config?.update_interval.osquery_policy || 0) / 1000 - ) || "osquery policy update interval"; - const showTeamDescription = isPremiumTier && !!teamId; - const showInfoBanner = - (teamId && !teamPoliciesError && !!teamPolicies?.length) || - (!teamId && !globalPoliciesError && !!globalPolicies?.length); - const showInheritedPoliciesButton = !!teamId && !isLoadingTeamPolicies && @@ -395,23 +379,6 @@ const ManagePolicyPage = ({

)} - {!!policyUpdateInterval && showInfoBanner && ( - -

- Your policies are checked every{" "} - {policyUpdateInterval.trim()}.{" "} - {isGlobalAdmin && ( - - Check out the Fleet documentation on{" "} - - how to edit this frequency - - . - - )} -

-
- )}
{!!teamId && teamPoliciesError && } {!!teamId && diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx index b70fd352e5..5201d23dc9 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/PoliciesListWrapper.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useContext } from "react"; +import { AppContext } from "context/app"; import { noop } from "lodash"; import paths from "router/paths"; @@ -42,6 +43,8 @@ const PoliciesListWrapper = ({ }: IPoliciesListWrapperProps): JSX.Element => { const { MANAGE_HOSTS } = paths; + const { config } = useContext(AppContext); + const NoPolicies = () => { return (
{ + const seconds = ms / 1000; + if (seconds < 60) { + return `${seconds} seconds`; + } + if (seconds < 3600) { + const minutes = millisecondsToMinutes(ms); + return `${minutes} minute${minutes > 1 ? "s" : ""}`; + } + const hours = millisecondsToHours(ms); + return `${hours} hour${hours > 1 ? "s" : ""}`; +}; + +const getTooltip = (osqueryPolicyMs: number): JSX.Element => { + return ( + + Fleet is collecting policy results. Try again +
+ in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up. +
+ ); +}; + // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generateTableHeaders = (options: { @@ -102,6 +126,7 @@ const generateTableHeaders = (options: { ), }, @@ -115,19 +140,46 @@ const generateTableHeaders = (options: { ), disableSortBy: true, accessor: "passing_host_count", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), + Cell: (cellProps: ICellProps): JSX.Element => { + if (cellProps.row.original.has_run) { + return ( + + ); + } + return ( + <> + + --- + + + {getTooltip(cellProps.row.original.osquery_policy_ms)} + + + ); + }, }, { title: "No", @@ -139,19 +191,46 @@ const generateTableHeaders = (options: { ), disableSortBy: true, accessor: "failing_host_count", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), + Cell: (cellProps: ICellProps): JSX.Element => { + if (cellProps.row.original.has_run) { + return ( + + ); + } + return ( + <> + + --- + + + {getTooltip(cellProps.row.original.osquery_policy_ms)} + + + ); + }, }, ]; @@ -200,17 +279,36 @@ const generateTableHeaders = (options: { const generateDataSet = ( policiesList: IPolicyStats[] = [], - currentAutomatedPolicies?: number[] + currentAutomatedPolicies?: number[], + osquery_policy?: number ): IPolicyStats[] => { policiesList = policiesList.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name) ); + let policiesLastRun: Date; + let osqueryPolicyMs: number; - policiesList.forEach((policy) => { - policy.webhook = - currentAutomatedPolicies && currentAutomatedPolicies.includes(policy.id) + if (osquery_policy) { + osqueryPolicyMs = osquery_policy / 1000000; + // Convert from nanosecond to milliseconds + policiesLastRun = new Date(Date.now() - osqueryPolicyMs); + } + + policiesList.forEach((policyItem) => { + policyItem.webhook = + currentAutomatedPolicies && + currentAutomatedPolicies.includes(policyItem.id) ? "On" : "Off"; + + // Define policy has_run based on updated_at compared againist last time policies ran as + // defined by osquery_policy. + policyItem.has_run = isAfter( + policiesLastRun, + new Date(policyItem.updated_at) + ); + // Include osquery policy in item for reference in tooltip + policyItem.osquery_policy_ms = osqueryPolicyMs; }); return policiesList; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/_styles.scss index af39273a27..ae50d0c608 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesListWrapper/_styles.scss @@ -33,6 +33,10 @@ } } +.has-not-run { + width: 20px; +} + .no-policies { display: flex; flex-direction: column;