From ee365ea276e0895aaa5e096cd8bbe46781f9e36c Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Mon, 2 Oct 2023 12:26:53 -0700
Subject: [PATCH 01/41] Frontend: Step 1 for query caching (organize FE
directories, build save modals, modify routes/URLs) (#13908)
---
assets/images/phone-home.svg | 1 -
frontend/components/EmptyTable/_styles.scss | 1 -
.../components/LiveQuery/SelectTargets.tsx | 4 +-
.../LiveQuery/TargetsInput/_styles.scss | 3 +
.../TooltipWrapper/TooltipWrapper.tsx | 1 +
.../components/icons/CollectingResults.tsx | 254 ++++++++++++++++++
frontend/components/icons/index.ts | 2 +
.../AwaitingResults/AwaitingResults.tsx | 17 +-
.../queryResults/AwaitingResults/_styles.scss | 15 --
frontend/context/query.tsx | 21 ++
frontend/interfaces/target.ts | 3 +
.../HostDetailsPage/HostDetailsPage.tsx | 12 +-
.../pages/policies/PolicyPage/PolicyPage.tsx | 18 +-
.../pages/policies/PolicyPage/_styles.scss | 32 ---
.../PolicyPage/screens/QueryEditor.tsx | 2 +-
.../QueriesTable/QueriesTableConfig.tsx | 2 +-
frontend/pages/queries/QueryPage/index.ts | 1 -
.../queries/QueryPage/screens/QueryEditor.tsx | 186 -------------
.../pages/queries/QueryPage/screens/test.js | 0
.../QueryDetailsPage/QueryDetailsPage.tsx | 241 +++++++++++++++++
.../details/QueryDetailsPage/_styles.scss | 65 +++++
.../queries/details/QueryDetailsPage/index.ts | 1 +
.../CachedDetails/CachedDetails.tsx | 14 +
.../details/components/CachedDetails/index.ts | 1 +
.../components/NoResults/NoResults.tsx | 116 ++++++++
.../details/components/NoResults/index.ts | 1 +
.../EditQueryPage/EditQueryPage.tsx} | 237 +++++++++-------
.../queries/edit/EditQueryPage/_styles.scss | 54 ++++
.../pages/queries/edit/EditQueryPage/index.ts | 1 +
.../components/QueryForm/QueryForm.tests.tsx | 1 -
.../components/QueryForm/QueryForm.tsx | 46 +++-
.../components/QueryForm/_styles.scss | 5 -
.../components/QueryForm/index.ts | 0
.../components/QueryResults/QueryResults.tsx | 0
.../QueryResults/QueryResultsTableConfig.tsx | 0
.../components/QueryResults/_styles.scss | 0
.../components/QueryResults/index.ts | 0
.../SaveChangesModal/SaveChangesModal.tsx | 53 ++++
.../edit/components/SaveChangesModal/index.ts | 1 +
.../SaveQueryModal/SaveQueryModal.tsx | 214 ++++++++-------
.../components/SaveQueryModal/_styles.scss | 0
.../components/SaveQueryModal/index.ts | 0
.../live/LiveQueryPage/LiveQueryPage.tsx | 213 +++++++++++++++
.../LiveQueryPage}/_styles.scss | 63 +----
.../pages/queries/live/LiveQueryPage/index.ts | 1 +
.../{QueryPage => live}/screens/RunQuery.tsx | 2 +-
frontend/router/index.tsx | 12 +-
frontend/router/paths.ts | 10 +
frontend/services/entities/queries.ts | 4 +-
frontend/utilities/constants.ts | 7 +-
frontend/utilities/endpoints.ts | 2 +-
frontend/utilities/helpers.ts | 7 +
52 files changed, 1391 insertions(+), 556 deletions(-)
delete mode 100644 assets/images/phone-home.svg
create mode 100644 frontend/components/icons/CollectingResults.tsx
delete mode 100644 frontend/pages/queries/QueryPage/index.ts
delete mode 100644 frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
delete mode 100644 frontend/pages/queries/QueryPage/screens/test.js
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/_styles.scss
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/index.ts
create mode 100644 frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
create mode 100644 frontend/pages/queries/details/components/CachedDetails/index.ts
create mode 100644 frontend/pages/queries/details/components/NoResults/NoResults.tsx
create mode 100644 frontend/pages/queries/details/components/NoResults/index.ts
rename frontend/pages/queries/{QueryPage/QueryPage.tsx => edit/EditQueryPage/EditQueryPage.tsx} (54%)
create mode 100644 frontend/pages/queries/edit/EditQueryPage/_styles.scss
create mode 100644 frontend/pages/queries/edit/EditQueryPage/index.ts
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/QueryForm.tests.tsx (98%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/QueryForm.tsx (95%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/_styles.scss (98%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/index.ts (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/QueryResults.tsx (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/QueryResultsTableConfig.tsx (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/_styles.scss (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/index.ts (100%)
create mode 100644 frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
create mode 100644 frontend/pages/queries/edit/components/SaveChangesModal/index.ts
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/SaveQueryModal.tsx (51%)
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/_styles.scss (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/index.ts (100%)
create mode 100644 frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
rename frontend/pages/queries/{QueryPage => live/LiveQueryPage}/_styles.scss (69%)
create mode 100644 frontend/pages/queries/live/LiveQueryPage/index.ts
rename frontend/pages/queries/{QueryPage => live}/screens/RunQuery.tsx (98%)
diff --git a/assets/images/phone-home.svg b/assets/images/phone-home.svg
deleted file mode 100644
index a0335a1113..0000000000
--- a/assets/images/phone-home.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/components/EmptyTable/_styles.scss b/frontend/components/EmptyTable/_styles.scss
index c24d1c9543..15517d8d8f 100644
--- a/frontend/components/EmptyTable/_styles.scss
+++ b/frontend/components/EmptyTable/_styles.scss
@@ -57,7 +57,6 @@
&__container {
align-self: center;
justify-content: center;
- margin: 0;
margin-bottom: 20px;
min-height: 155px;
max-width: none;
diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx
index c65a3e0e60..2052d13e14 100644
--- a/frontend/components/LiveQuery/SelectTargets.tsx
+++ b/frontend/components/LiveQuery/SelectTargets.tsx
@@ -48,7 +48,9 @@ interface ISelectTargetsProps {
targetedTeams: ITeam[];
goToQueryEditor: () => void;
goToRunQuery: () => void;
- setSelectedTargets: React.Dispatch>;
+ setSelectedTargets:
+ | React.Dispatch> // Used for policies page level useState hook
+ | ((value: ITarget[]) => void); // Used for queries app level QueryContext
setTargetedHosts: React.Dispatch>;
setTargetedLabels: React.Dispatch>;
setTargetedTeams: React.Dispatch>;
diff --git a/frontend/components/LiveQuery/TargetsInput/_styles.scss b/frontend/components/LiveQuery/TargetsInput/_styles.scss
index e2f14ce6e1..137525dd04 100644
--- a/frontend/components/LiveQuery/TargetsInput/_styles.scss
+++ b/frontend/components/LiveQuery/TargetsInput/_styles.scss
@@ -79,4 +79,7 @@
overflow: auto;
}
}
+ .input-icon-field__icon {
+ top: 34px; // Override styling to include label header
+ }
}
diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.tsx
index 0b48e54b91..43e334495a 100644
--- a/frontend/components/TooltipWrapper/TooltipWrapper.tsx
+++ b/frontend/components/TooltipWrapper/TooltipWrapper.tsx
@@ -6,6 +6,7 @@ import * as DOMPurify from "dompurify";
interface ITooltipWrapperProps {
children: string | JSX.Element;
tipContent: string;
+ /** Default: bottom */
position?: "top" | "bottom";
isDelayed?: boolean;
className?: string;
diff --git a/frontend/components/icons/CollectingResults.tsx b/frontend/components/icons/CollectingResults.tsx
new file mode 100644
index 0000000000..0df9824b3e
--- /dev/null
+++ b/frontend/components/icons/CollectingResults.tsx
@@ -0,0 +1,254 @@
+import React from "react";
+
+const CollectingResults = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CollectingResults;
diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts
index cd18850643..36ac93a842 100644
--- a/frontend/components/icons/index.ts
+++ b/frontend/components/icons/index.ts
@@ -4,6 +4,7 @@ import ArrowInternalLink from "./ArrowInternalLink";
import CalendarCheck from "./CalendarCheck";
import Check from "./Check";
import Chevron from "./Chevron";
+import CollectingResults from "./CollectingResults";
import Columns from "./Columns";
import CriticalPolicy from "./CriticalPolicy";
import Disable from "./Disable";
@@ -84,6 +85,7 @@ export const ICON_MAP = {
"calendar-check": CalendarCheck,
chevron: Chevron,
check: Check,
+ "collecting-results": CollectingResults,
columns: Columns,
"critical-policy": CriticalPolicy,
disable: Disable,
diff --git a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
index ed2bf4953a..e807a83b6a 100644
--- a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
+++ b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
@@ -1,19 +1,18 @@
import React from "react";
-import PhoneHome from "../../../../../assets/images/phone-home.svg";
+import EmptyTable from "components/EmptyTable/EmptyTable";
const baseClass = "awaiting-results";
const AwaitingResults = () => {
return (
-
-
-
Phoning home...
-
- There are currently no results to your query. Please wait while we talk
- to more hosts.
-
-
+
);
};
diff --git a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
index 12463a3844..ba2b2dad9b 100644
--- a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
+++ b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
@@ -5,19 +5,4 @@
flex-direction: column;
align-items: center;
text-align: center;
-
- img {
- margin-bottom: $pad-medium;
- }
-
- &__title {
- font-size: $small;
- font-weight: $bold;
- margin-bottom: $pad-small;
- }
-
- &__description {
- font-size: $x-small;
- margin: 0;
- }
}
diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx
index 992e152cad..e313a0a156 100644
--- a/frontend/context/query.tsx
+++ b/frontend/context/query.tsx
@@ -6,6 +6,7 @@ import { DEFAULT_QUERY } from "utilities/constants";
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
import { SelectedPlatformString } from "interfaces/platform";
import { QueryLoggingOption } from "interfaces/schedulable_query";
+import { DEFAULT_TARGETS, ITarget } from "interfaces/target";
type Props = {
children: ReactNode;
@@ -22,6 +23,7 @@ type InitialStateType = {
lastEditedQueryPlatforms: SelectedPlatformString;
lastEditedQueryMinOsqueryVersion: string;
lastEditedQueryLoggingType: QueryLoggingOption;
+ selectedQueryTargets: ITarget[];
setLastEditedQueryId: (value: number | null) => void;
setLastEditedQueryName: (value: string) => void;
setLastEditedQueryDescription: (value: string) => void;
@@ -32,6 +34,7 @@ type InitialStateType = {
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
setLastEditedQueryLoggingType: (value: string) => void;
setSelectedOsqueryTable: (tableName: string) => void;
+ setSelectedQueryTargets: (value: ITarget[]) => void;
};
export type IQueryContext = InitialStateType;
@@ -48,6 +51,7 @@ const initialState = {
lastEditedQueryPlatforms: DEFAULT_QUERY.platform,
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
+ selectedQueryTargets: DEFAULT_TARGETS,
setLastEditedQueryId: () => null,
setLastEditedQueryName: () => null,
setLastEditedQueryDescription: () => null,
@@ -58,11 +62,13 @@ const initialState = {
setLastEditedQueryMinOsqueryVersion: () => null,
setLastEditedQueryLoggingType: () => null,
setSelectedOsqueryTable: () => null,
+ setSelectedQueryTargets: () => null,
};
const actions = {
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
+ SET_SELECTED_QUERY_TARGETS: "SET_SELECTED_QUERY_TARGETS",
} as const;
const reducer = (state: InitialStateType, action: any) => {
@@ -114,6 +120,14 @@ const reducer = (state: InitialStateType, action: any) => {
? state.lastEditedQueryLoggingType
: action.lastEditedQueryLoggingType,
};
+ case actions.SET_SELECTED_QUERY_TARGETS:
+ return {
+ ...state,
+ selectedQueryTargets:
+ typeof action.selectedQueryTargets === "undefined"
+ ? state.selectedQueryTargets
+ : action.selectedQueryTargets,
+ };
default:
return state;
}
@@ -135,6 +149,7 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
+ selectedQueryTargets: state.selectedQueryTargets,
setLastEditedQueryId: (lastEditedQueryId: number | null) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
@@ -193,6 +208,12 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryLoggingType,
});
},
+ setSelectedQueryTargets: (selectedQueryTargets: ITarget[]) => {
+ dispatch({
+ type: actions.SET_SELECTED_QUERY_TARGETS,
+ selectedQueryTargets,
+ });
+ },
setSelectedOsqueryTable: (tableName: string) => {
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
},
diff --git a/frontend/interfaces/target.ts b/frontend/interfaces/target.ts
index 7526719212..873d7907f3 100644
--- a/frontend/interfaces/target.ts
+++ b/frontend/interfaces/target.ts
@@ -49,3 +49,6 @@ export interface IPackTargets {
label_ids: (number | string)[];
team_ids: (number | string)[];
}
+
+// TODO: Also use for testing
+export const DEFAULT_TARGETS: ITarget[] = [];
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 0d1a0b6b46..132dd5b502 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -39,7 +39,11 @@ import MainContent from "components/MainContent";
import InfoBanner from "components/InfoBanner";
import BackLink from "components/BackLink";
-import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers";
+import {
+ normalizeEmptyValues,
+ wrapFleetHelper,
+ TAGGED_TEMPLATES,
+} from "utilities/helpers";
import permissions from "utilities/permissions";
import HostSummaryCard from "../cards/HostSummary";
@@ -99,12 +103,6 @@ interface IHostDetailsSubNavItem {
pathname: string;
}
-const TAGGED_TEMPLATES = {
- queryByHostRoute: (hostId: number | undefined | null) => {
- return `${hostId ? `?host_ids=${hostId}` : ""}`;
- },
-};
-
const HostDetailsPage = ({
route,
router,
diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
index ef3fc1eb4c..5d10551229 100644
--- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx
+++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
@@ -19,7 +19,7 @@ import globalPoliciesAPI from "services/entities/global_policies";
import teamPoliciesAPI from "services/entities/team_policies";
import hostAPI from "services/entities/hosts";
import statusAPI from "services/entities/status";
-import { QUERIES_PAGE_STEPS } from "utilities/constants";
+import { LIVE_POLICY_STEPS } from "utilities/constants";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor";
@@ -127,7 +127,7 @@ const PolicyPage = ({
};
}, []);
- const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
+ const [step, setStep] = useState(LIVE_POLICY_STEPS[1]);
const [selectedTargets, setSelectedTargets] = useState([]);
const [targetedHosts, setTargetedHosts] = useState([]);
const [targetedLabels, setTargetedLabels] = useState([]);
@@ -260,7 +260,7 @@ const PolicyPage = ({
storedPolicyError,
createPolicy,
onOsqueryTableSelect,
- goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
+ goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]),
onOpenSchemaSidebar,
renderLiveQueryWarning,
};
@@ -272,8 +272,8 @@ const PolicyPage = ({
targetedLabels,
targetedTeams,
targetsTotalCount,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
- goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
+ goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
+ goToRunQuery: () => setStep(LIVE_POLICY_STEPS[3]),
setSelectedTargets,
setTargetedHosts,
setTargetedLabels,
@@ -285,21 +285,21 @@ const PolicyPage = ({
selectedTargets,
storedPolicy,
setSelectedTargets,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
+ goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
targetsTotalCount,
};
switch (step) {
- case QUERIES_PAGE_STEPS[2]:
+ case LIVE_POLICY_STEPS[2]:
return ;
- case QUERIES_PAGE_STEPS[3]:
+ case LIVE_POLICY_STEPS[3]:
return ;
default:
return ;
}
};
- const isFirstStep = step === QUERIES_PAGE_STEPS[1];
+ const isFirstStep = step === LIVE_POLICY_STEPS[1];
const showSidebar =
isFirstStep &&
isSidebarOpen &&
diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss
index 262b83b515..1eb28ded67 100644
--- a/frontend/pages/policies/PolicyPage/_styles.scss
+++ b/frontend/pages/policies/PolicyPage/_styles.scss
@@ -34,33 +34,6 @@
}
}
- &__observer-query-details {
- padding: 0 2rem;
-
- h1 {
- margin: $pad-large 0;
- font-size: $large;
- }
-
- p {
- margin-bottom: $pad-small;
- }
-
- .sql-button {
- color: $core-vibrant-blue;
- font-weight: $bold;
- font-size: $x-small;
- }
- }
-
- &__query-preview {
- margin-top: 15px;
-
- .fleet-ace__label {
- display: none;
- }
- }
-
.ace_content {
min-height: 500px !important;
}
@@ -177,9 +150,4 @@
margin-bottom: 0;
}
}
- .targets-input {
- .input-icon-field__icon {
- top: 34px; // Override styling to include label header
- }
- }
}
diff --git a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
index 89e46d024a..5e42e673de 100644
--- a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
+++ b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
@@ -173,7 +173,7 @@ const QueryEditor = ({
return null;
}
- // Function instead of constant eliminates race condition with filteredSoftwarePath
+ // Function instead of constant eliminates race condition with filteredPoliciesPath
const backToPoliciesPath = () => {
return filteredPoliciesPath || PATHS.MANAGE_POLICIES;
};
diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
index f24c17af24..10b4611433 100644
--- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
+++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
@@ -152,7 +152,7 @@ const generateTableHeaders = ({
)}
>
}
- path={PATHS.EDIT_QUERY(
+ path={PATHS.QUERY(
cellProps.row.original.id,
cellProps.row.original.team_id ?? undefined
)}
diff --git a/frontend/pages/queries/QueryPage/index.ts b/frontend/pages/queries/QueryPage/index.ts
deleted file mode 100644
index 8d00fa0475..0000000000
--- a/frontend/pages/queries/QueryPage/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./QueryPage";
diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
deleted file mode 100644
index 7b94956efd..0000000000
--- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-
-import { InjectedRouter } from "react-router/lib/Router";
-import { UseMutateAsyncFunction } from "react-query";
-
-import queryAPI from "services/entities/queries";
-import { AppContext } from "context/app";
-import { QueryContext } from "context/query";
-import { NotificationContext } from "context/notification";
-import {
- ICreateQueryRequestBody,
- ISchedulableQuery,
-} from "interfaces/schedulable_query";
-import PATHS from "router/paths";
-import debounce from "utilities/debounce";
-import deepDifference from "utilities/deep_difference";
-
-import BackLink from "components/BackLink";
-import QueryForm from "pages/queries/QueryPage/components/QueryForm";
-
-interface IQueryEditorProps {
- router: InjectedRouter;
- baseClass: string;
- queryIdForEdit: number | null;
- teamNameForQuery?: string;
- apiTeamIdForQuery?: number;
- storedQuery: ISchedulableQuery | undefined;
- storedQueryError: Error | null;
- showOpenSchemaActionText: boolean;
- isStoredQueryLoading: boolean;
- onOsqueryTableSelect: (tableName: string) => void;
- goToSelectTargets: () => void;
- onOpenSchemaSidebar: () => void;
- renderLiveQueryWarning: () => JSX.Element | null;
-}
-
-const QueryEditor = ({
- router,
- baseClass,
- queryIdForEdit,
- teamNameForQuery,
- apiTeamIdForQuery,
- storedQuery,
- storedQueryError,
- showOpenSchemaActionText,
- isStoredQueryLoading,
- onOsqueryTableSelect,
- goToSelectTargets,
- onOpenSchemaSidebar,
- renderLiveQueryWarning,
-}: IQueryEditorProps): JSX.Element | null => {
- const { currentUser, filteredQueriesPath } = useContext(AppContext);
- const { renderFlash } = useContext(NotificationContext);
-
- // Note: The QueryContext values should always be used for any mutable query data such as query name
- // The storedQuery prop should only be used to access immutable metadata such as author id
- const {
- lastEditedQueryName,
- lastEditedQueryDescription,
- lastEditedQueryBody,
- lastEditedQueryObserverCanRun,
- lastEditedQueryFrequency,
- lastEditedQueryLoggingType,
- lastEditedQueryPlatforms,
- lastEditedQueryMinOsqueryVersion,
- } = useContext(QueryContext);
-
- const [isQuerySaving, setIsQuerySaving] = useState(false);
- const [isQueryUpdating, setIsQueryUpdating] = useState(false);
-
- useEffect(() => {
- if (storedQueryError) {
- renderFlash(
- "error",
- "Something went wrong retrieving your query. Please try again."
- );
- }
- }, []);
-
- const [backendValidators, setBackendValidators] = useState<{
- [key: string]: string;
- }>({});
-
- const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
- setIsQuerySaving(true);
- try {
- const { query } = await queryAPI.create(formData);
- router.push(PATHS.EDIT_QUERY(query.id));
- renderFlash("success", "Query created!");
- setBackendValidators({});
- } catch (createError: any) {
- if (createError.data.errors[0].reason.includes("already exists")) {
- const teamErrorText =
- teamNameForQuery && apiTeamIdForQuery !== 0
- ? `the ${teamNameForQuery} team`
- : "all teams";
- setBackendValidators({
- name: `A query with that name already exists for ${teamErrorText}.`,
- });
- } else {
- renderFlash(
- "error",
- "Something went wrong creating your query. Please try again."
- );
- setBackendValidators({});
- }
- } finally {
- setIsQuerySaving(false);
- }
- });
-
- const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
- if (!queryIdForEdit) {
- return false;
- }
-
- setIsQueryUpdating(true);
-
- const updatedQuery = deepDifference(formData, {
- lastEditedQueryName,
- lastEditedQueryDescription,
- lastEditedQueryBody,
- lastEditedQueryObserverCanRun,
- lastEditedQueryFrequency,
- lastEditedQueryPlatforms,
- lastEditedQueryLoggingType,
- lastEditedQueryMinOsqueryVersion,
- });
-
- try {
- await queryAPI.update(queryIdForEdit, updatedQuery);
- renderFlash("success", "Query updated!");
- } catch (updateError: any) {
- console.error(updateError);
- if (updateError.data.errors[0].reason.includes("Duplicate")) {
- renderFlash("error", "A query with this name already exists.");
- } else {
- renderFlash(
- "error",
- "Something went wrong updating your query. Please try again."
- );
- }
- }
-
- setIsQueryUpdating(false);
-
- return false;
- };
-
- if (!currentUser) {
- return null;
- }
-
- // Function instead of constant eliminates race condition with filteredSoftwarePath
- const backToQueriesPath = () => {
- return filteredQueriesPath || PATHS.MANAGE_QUERIES;
- };
-
- return (
-
- );
-};
-
-export default QueryEditor;
diff --git a/frontend/pages/queries/QueryPage/screens/test.js b/frontend/pages/queries/QueryPage/screens/test.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
new file mode 100644
index 0000000000..be52bd6ad3
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
@@ -0,0 +1,241 @@
+import React, { useContext, useEffect } from "react";
+import { useQuery } from "react-query";
+import { InjectedRouter, Params } from "react-router/lib/Router";
+import { useErrorHandler } from "react-error-boundary";
+
+import PATHS from "router/paths";
+import { AppContext } from "context/app";
+import { QueryContext } from "context/query";
+import useTeamIdParam from "hooks/useTeamIdParam";
+
+import {
+ IGetQueryResponse,
+ ISchedulableQuery,
+} from "interfaces/schedulable_query";
+
+import queryAPI from "services/entities/queries";
+
+import Spinner from "components/Spinner/Spinner";
+import Button from "components/buttons/Button";
+import BackLink from "components/BackLink";
+import MainContent from "components/MainContent";
+import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
+import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator";
+import DataError from "components/DataError/DataError";
+import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
+import CachedDetails from "../components/CachedDetails/CachedDetails";
+import NoResults from "../components/NoResults/NoResults";
+
+interface IQueryDetailsPageProps {
+ router: InjectedRouter; // v3
+ params: Params;
+ location: {
+ pathname: string;
+ query: { team_id?: string };
+ search: string;
+ };
+}
+
+const baseClass = "query-details-page";
+
+const QueryDetailsPage = ({
+ router,
+ params: { id: paramsQueryId },
+ location,
+}: IQueryDetailsPageProps): JSX.Element => {
+ const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
+
+ const {
+ currentTeamName: teamNameForQuery,
+ teamIdForApi: apiTeamIdForQuery,
+ } = useTeamIdParam({
+ location,
+ router,
+ includeAllTeams: true,
+ includeNoTeam: false,
+ });
+
+ const handlePageError = useErrorHandler();
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isAnyTeamMaintainerOrTeamAdmin,
+ isObserverPlus,
+ isAnyTeamObserverPlus,
+ config,
+ filteredQueriesPath,
+ } = useContext(AppContext);
+ const {
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryObserverCanRun,
+ setLastEditedQueryId,
+ setLastEditedQueryName,
+ setLastEditedQueryDescription,
+ setLastEditedQueryBody,
+ setLastEditedQueryObserverCanRun,
+ setLastEditedQueryFrequency,
+ setLastEditedQueryLoggingType,
+ setLastEditedQueryMinOsqueryVersion,
+ setLastEditedQueryPlatforms,
+ } = useContext(QueryContext);
+
+ // Title that shows up on browser tabs (e.g., Query details | Discover TLS certificates | Fleet for osquery)
+ document.title = `Query details | ${lastEditedQueryName} | Fleet for osquery`;
+
+ // disabled on page load so we can control the number of renders
+ // else it will re-populate the context on occasion
+ const {
+ isLoading: isStoredQueryLoading,
+ data: storedQuery,
+ error: storedQueryError,
+ } = useQuery(
+ ["query", queryId],
+ () => queryAPI.load(queryId as number),
+ {
+ enabled: !!queryId,
+ refetchOnWindowFocus: false,
+ select: (data) => data.query,
+ onSuccess: (returnedQuery) => {
+ setLastEditedQueryId(returnedQuery.id);
+ setLastEditedQueryName(returnedQuery.name);
+ setLastEditedQueryDescription(returnedQuery.description);
+ setLastEditedQueryBody(returnedQuery.query);
+ setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
+ setLastEditedQueryFrequency(returnedQuery.interval);
+ setLastEditedQueryPlatforms(returnedQuery.platform);
+ setLastEditedQueryLoggingType(returnedQuery.logging);
+ setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
+ },
+ onError: (error) => handlePageError(error),
+ }
+ );
+
+ const isLoading = isStoredQueryLoading; // TODO: Add || isCachedResultsLoading for new API response
+ const isApiError = storedQueryError || true; // TODO: Add || isCachedResultsError for new API response
+
+ const renderHeader = () => {
+ const canEditQuery =
+ isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin;
+
+ // Function instead of constant eliminates race condition with filteredQueriesPath
+ const backToQueriesPath = () => {
+ return filteredQueriesPath || PATHS.MANAGE_QUERIES;
+ };
+
+ return (
+ <>
+
+
+
+ {!isLoading && !isApiError && (
+
+
+
+ {lastEditedQueryName}
+
+
+ {lastEditedQueryDescription}
+
+
+
+
{
+ queryId && router.push(PATHS.EDIT_QUERY(queryId));
+ }}
+ className={`${baseClass}__manage-automations button`}
+ variant="brand"
+ >
+ {canEditQuery ? "Edit query" : "More details"}
+
+ {(lastEditedQueryObserverCanRun ||
+ isObserverPlus ||
+ isAnyTeamObserverPlus ||
+ canEditQuery) && (
+
+ {
+ queryId && router.push(PATHS.LIVE_QUERY(queryId));
+ }}
+ >
+ Live query
+
+
+ )}
+
+
+ )}
+ {!isLoading && !isApiError && (
+
+
+
+ Automations:
+
+
+
+
+ Log destination: {" "}
+
+
+
+ )}
+ >
+ );
+ };
+
+ const renderReport = () => {
+ const disabledCachingGlobally = true; // TODO: Update accordingly to config?.server_settings.query_reports_disabled
+ const discardDataEnabled = true; // TODO: Update accordingly to storedQuery?.discard_data
+ const loggingSnapshot = storedQuery?.logging === "snapshot";
+ const disabledCaching =
+ disabledCachingGlobally || discardDataEnabled || !loggingSnapshot;
+ const emptyCache = true; // TODO: Update with API response
+ const errorsOnly = true; // TODO: Update with API response
+
+ // Loading state
+ if (isLoading) {
+ return ;
+ }
+
+ // Error state
+ if (isApiError) {
+ return ;
+ }
+
+ // Empty state with varying messages explaining why there's no results
+ if (emptyCache) {
+ return (
+
+ );
+ }
+ return ; // TODO: Everything related to new APIs including surfacing errorsOnly
+ };
+
+ return (
+
+
+ {renderHeader()}
+ {renderReport()}
+
+
+ );
+};
+
+export default QueryDetailsPage;
diff --git a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss
new file mode 100644
index 0000000000..9b0a5a0a25
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss
@@ -0,0 +1,65 @@
+.query-details-page {
+ &__title-bar {
+ display: flex;
+ justify-content: space-between;
+ margin-top: $pad-large;
+
+ .name-description {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-small;
+ }
+ }
+
+ &__action-button-container {
+ display: flex;
+ justify-content: flex-end;
+ min-width: 266px;
+ gap: $pad-medium;
+ }
+
+ &__query-name {
+ margin-top: 0;
+ font-size: $large;
+ }
+
+ &__query-description {
+ margin-top: 0;
+ margin-bottom: $pad-small;
+ font-size: $x-small;
+ }
+
+ &__settings {
+ display: flex;
+ gap: $pad-large;
+ font-size: $x-small;
+ }
+
+ &__automations,
+ &__log-destination {
+ display: flex;
+ gap: $pad-small;
+ }
+
+ .empty-table__inner {
+ .component__tooltip-wrapper__tip-text {
+ text-align: left;
+ width: 320px;
+ }
+
+ ul {
+ color: $core-white;
+
+ li {
+ &::before {
+ content: "•";
+ color: $core-white;
+ }
+ }
+ }
+ }
+
+ .data-error {
+ padding-top: $pad-xxxlarge;
+ }
+}
diff --git a/frontend/pages/queries/details/QueryDetailsPage/index.ts b/frontend/pages/queries/details/QueryDetailsPage/index.ts
new file mode 100644
index 0000000000..9bb526e7b5
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./QueryDetailsPage";
diff --git a/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx b/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
new file mode 100644
index 0000000000..ab5255e3f0
--- /dev/null
+++ b/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+
+// TODO: This whole section
+// interface ICachedDetailsProps {
+//
+// }
+
+const baseClass = "cached-details";
+
+const CachedDetails = (): JSX.Element => {
+ return TODO
;
+};
+
+export default CachedDetails;
diff --git a/frontend/pages/queries/details/components/CachedDetails/index.ts b/frontend/pages/queries/details/components/CachedDetails/index.ts
new file mode 100644
index 0000000000..b50b73552b
--- /dev/null
+++ b/frontend/pages/queries/details/components/CachedDetails/index.ts
@@ -0,0 +1 @@
+export { default } from "./CachedDetails";
diff --git a/frontend/pages/queries/details/components/NoResults/NoResults.tsx b/frontend/pages/queries/details/components/NoResults/NoResults.tsx
new file mode 100644
index 0000000000..7897facb23
--- /dev/null
+++ b/frontend/pages/queries/details/components/NoResults/NoResults.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+
+import differenceInSeconds from "date-fns/differenceInSeconds";
+import formatDistance from "date-fns/formatDistance";
+import add from "date-fns/add";
+
+import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
+import EmptyTable from "components/EmptyTable/EmptyTable";
+
+interface INoResultsProps {
+ queryInterval?: number;
+ queryUpdatedAt?: string;
+ disabledCaching: boolean;
+ disabledCachingGlobally: boolean;
+ discardDataEnabled: boolean;
+ loggingSnapshot: boolean;
+ errorsOnly: boolean;
+}
+
+const baseClass = "no-results";
+
+const NoResults = ({
+ queryInterval,
+ queryUpdatedAt,
+ disabledCaching,
+ disabledCachingGlobally,
+ discardDataEnabled,
+ loggingSnapshot,
+ errorsOnly,
+}: INoResultsProps): JSX.Element => {
+ // Returns how many seconds it takes to expect a cached update
+ const secondsCheckbackTime = () => {
+ const secondsSinceUpdate = queryUpdatedAt
+ ? differenceInSeconds(new Date(), new Date(queryUpdatedAt))
+ : 0;
+ const secondsUpdateWaittime = (queryInterval || 0) + 60;
+ return secondsUpdateWaittime - secondsSinceUpdate;
+ };
+
+ // Update status of collecting cached results
+ const collectingResults = secondsCheckbackTime() > 0;
+
+ // Converts seconds takes to update to human readable format
+ const readableCheckbackTime = formatDistance(
+ add(new Date(), { seconds: secondsCheckbackTime() }),
+ new Date()
+ );
+
+ // Collecting results state
+ if (collectingResults) {
+ const collectingResultsInfo = () =>
+ `Fleet is collecting query results. Check back in about ${readableCheckbackTime}.`;
+
+ return (
+
+ );
+ }
+
+ const noResultsInfo = () => {
+ if (!queryInterval) {
+ return (
+ <>
+ This query does not collect data on a schedule. Add a{" "}
+ frequency or run this as a live query to see results.
+ >
+ );
+ }
+ if (disabledCaching) {
+ const tipContent = () => {
+ if (disabledCachingGlobally) {
+ return "The following setting prevents saving this query's results in Fleet:Query reports are globally disabled in organization settings. ";
+ }
+ if (discardDataEnabled) {
+ return "The following setting prevents saving this query's results in Fleet:This query has Discard data enabled. ";
+ }
+ if (!loggingSnapshot) {
+ return "The following setting prevents saving this query's results in Fleet:The logging setting for this query is not Snapshot. ";
+ }
+ return "Unknown";
+ };
+ return (
+ <>
+ Results from this query are{" "}
+
+ not reported in Fleet
+
+ .
+ >
+ );
+ }
+ if (errorsOnly) {
+ return (
+ <>
+ This query had trouble collecting data on some hosts. Check out the{" "}
+ Errors tab to see why.
+ >
+ );
+ }
+ return "This query has returned no data so far.";
+ };
+
+ return (
+
+ );
+};
+
+export default NoResults;
diff --git a/frontend/pages/queries/details/components/NoResults/index.ts b/frontend/pages/queries/details/components/NoResults/index.ts
new file mode 100644
index 0000000000..04bef19e77
--- /dev/null
+++ b/frontend/pages/queries/details/components/NoResults/index.ts
@@ -0,0 +1 @@
+export { default } from "./NoResults";
diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
similarity index 54%
rename from frontend/pages/queries/QueryPage/QueryPage.tsx
rename to frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
index ca93efe3d4..e9aa57dce9 100644
--- a/frontend/pages/queries/QueryPage/QueryPage.tsx
+++ b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
@@ -1,34 +1,36 @@
-import React, { useState, useEffect, useContext, useCallback } from "react";
-import { useQuery, useMutation } from "react-query";
+import React, { useState, useEffect, useContext } from "react";
+import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
-import { QUERIES_PAGE_STEPS, DEFAULT_QUERY } from "utilities/constants";
+import { DEFAULT_QUERY } from "utilities/constants";
import queryAPI from "services/entities/queries";
-import hostAPI from "services/entities/hosts";
import statusAPI from "services/entities/status";
-import { IHost, IHostResponse } from "interfaces/host";
-import { ILabel } from "interfaces/label";
-import { ITeam } from "interfaces/team";
import {
IGetQueryResponse,
+ ICreateQueryRequestBody,
ISchedulableQuery,
} from "interfaces/schedulable_query";
-import { ITarget } from "interfaces/target";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
-import SelectTargets from "components/LiveQuery/SelectTargets";
import CustomLink from "components/CustomLink";
-import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor";
-import RunQuery from "pages/queries/QueryPage/screens/RunQuery";
import useTeamIdParam from "hooks/useTeamIdParam";
-interface IQueryPageProps {
+import { NotificationContext } from "context/notification";
+
+import PATHS from "router/paths";
+import debounce from "utilities/debounce";
+import deepDifference from "utilities/deep_difference";
+
+import BackLink from "components/BackLink";
+import QueryForm from "pages/queries/edit/components/QueryForm";
+
+interface IEditQueryPageProps {
router: InjectedRouter;
params: Params;
location: {
@@ -38,13 +40,13 @@ interface IQueryPageProps {
};
}
-const baseClass = "query-page";
+const baseClass = "edit-query-page";
-const QueryPage = ({
+const EditQueryPage = ({
router,
params: { id: paramsQueryId },
location,
-}: IQueryPageProps): JSX.Element => {
+}: IEditQueryPageProps): JSX.Element => {
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
const {
currentTeamName: teamNameForQuery,
@@ -67,6 +69,15 @@ const QueryPage = ({
const {
selectedOsqueryTable,
setSelectedOsqueryTable,
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryBody,
+ lastEditedQueryObserverCanRun,
+ lastEditedQueryFrequency,
+ lastEditedQueryPlatforms,
+ lastEditedQueryLoggingType,
+ lastEditedQueryMinOsqueryVersion,
+ selectedQueryTargets,
setLastEditedQueryId,
setLastEditedQueryName,
setLastEditedQueryDescription,
@@ -76,15 +87,14 @@ const QueryPage = ({
setLastEditedQueryLoggingType,
setLastEditedQueryMinOsqueryVersion,
setLastEditedQueryPlatforms,
+ // setSelectedQueryTargets,
} = useContext(QueryContext);
+ const { currentUser } = useContext(AppContext);
+ const { renderFlash } = useContext(NotificationContext);
+
+ // const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
+ // const [targetedHosts, setTargetedHosts] = useState([]);
- const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
- const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
- const [selectedTargets, setSelectedTargets] = useState([]);
- const [targetedHosts, setTargetedHosts] = useState([]);
- const [targetedLabels, setTargetedLabels] = useState([]);
- const [targetedTeams, setTargetedTeams] = useState([]);
- const [targetsTotalCount, setTargetsTotalCount] = useState(0);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
@@ -119,29 +129,6 @@ const QueryPage = ({
}
);
- useQuery(
- "hostFromURL",
- () =>
- hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
- {
- enabled: !!location.query.host_ids && !queryParamHostsAdded,
- select: (data: IHostResponse) => data.host,
- onSuccess: (host) => {
- setTargetedHosts((prevHosts) =>
- prevHosts.filter((h) => h.id !== host.id).concat(host)
- );
- const targets = selectedTargets;
- host.target_type = "hosts";
- targets.push(host);
- setSelectedTargets([...targets]);
- if (!queryParamHostsAdded) {
- setQueryParamHostsAdded(true);
- }
- router.replace(location.pathname);
- },
- }
- );
-
const detectIsFleetQueryRunnable = () => {
statusAPI.live_query().catch(() => {
setIsLiveQueryRunnable(false);
@@ -163,16 +150,88 @@ const QueryPage = ({
}
}, [queryId]);
+ const [isQuerySaving, setIsQuerySaving] = useState(false);
+ const [isQueryUpdating, setIsQueryUpdating] = useState(false);
+ const [backendValidators, setBackendValidators] = useState<{
+ [key: string]: string;
+ }>({});
+
// Updates title that shows up on browser tabs
useEffect(() => {
// e.g., Query details | Discover TLS certificates | Fleet for osquery
- document.title = `Query details | ${storedQuery?.name} | Fleet for osquery`;
+ document.title = `Edit query | ${storedQuery?.name} | Fleet for osquery`;
}, [location.pathname, storedQuery?.name]);
useEffect(() => {
setShowOpenSchemaActionText(!isSidebarOpen);
}, [isSidebarOpen]);
+ const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
+ setIsQuerySaving(true);
+ try {
+ const { query } = await queryAPI.create(formData);
+ router.push(PATHS.EDIT_QUERY(query.id));
+ renderFlash("success", "Query created!");
+ setBackendValidators({});
+ } catch (createError: any) {
+ if (createError.data.errors[0].reason.includes("already exists")) {
+ const teamErrorText =
+ teamNameForQuery && apiTeamIdForQuery !== 0
+ ? `the ${teamNameForQuery} team`
+ : "all teams";
+ setBackendValidators({
+ name: `A query with that name already exists for ${teamErrorText}.`,
+ });
+ } else {
+ renderFlash(
+ "error",
+ "Something went wrong creating your query. Please try again."
+ );
+ setBackendValidators({});
+ }
+ } finally {
+ setIsQuerySaving(false);
+ }
+ });
+
+ const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
+ if (!queryId) {
+ return false;
+ }
+
+ setIsQueryUpdating(true);
+
+ const updatedQuery = deepDifference(formData, {
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryBody,
+ lastEditedQueryObserverCanRun,
+ lastEditedQueryFrequency,
+ lastEditedQueryPlatforms,
+ lastEditedQueryLoggingType,
+ lastEditedQueryMinOsqueryVersion,
+ });
+
+ try {
+ await queryAPI.update(queryId, updatedQuery);
+ renderFlash("success", "Query updated!");
+ } catch (updateError: any) {
+ console.error(updateError);
+ if (updateError.data.errors[0].reason.includes("Duplicate")) {
+ renderFlash("error", "A query with this name already exists.");
+ } else {
+ renderFlash(
+ "error",
+ "Something went wrong updating your query. Please try again."
+ );
+ }
+ }
+
+ setIsQueryUpdating(false);
+
+ return false;
+ };
+
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
@@ -207,64 +266,12 @@ const QueryPage = ({
);
};
- const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []);
-
- const renderScreen = () => {
- const step1Props = {
- router,
- baseClass,
- queryIdForEdit: queryId,
- teamNameForQuery,
- apiTeamIdForQuery,
- showOpenSchemaActionText,
- storedQuery,
- isStoredQueryLoading,
- storedQueryError,
- onOsqueryTableSelect,
- goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
- onOpenSchemaSidebar,
- renderLiveQueryWarning,
- };
-
- const step2Props = {
- baseClass,
- queryId,
- selectedTargets,
- targetedHosts,
- targetedLabels,
- targetedTeams,
- targetsTotalCount,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
- goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
- setSelectedTargets,
- setTargetedHosts,
- setTargetedLabels,
- setTargetedTeams,
- setTargetsTotalCount,
- };
-
- const step3Props = {
- queryId,
- selectedTargets,
- storedQuery,
- setSelectedTargets,
- goToQueryEditor,
- targetsTotalCount,
- };
-
- switch (step) {
- case QUERIES_PAGE_STEPS[2]:
- return ;
- case QUERIES_PAGE_STEPS[3]:
- return ;
- default:
- return ;
- }
+ // Function instead of constant eliminates race condition
+ const backToQueriesPath = () => {
+ return queryId ? PATHS.QUERY(queryId) : PATHS.MANAGE_QUERIES;
};
- const isFirstStep = step === QUERIES_PAGE_STEPS[1];
const showSidebar =
- isFirstStep &&
isSidebarOpen &&
(isGlobalAdmin ||
isGlobalMaintainer ||
@@ -275,7 +282,31 @@ const QueryPage = ({
return (
<>
- {renderScreen()}
+
{showSidebar && (
@@ -290,4 +321,4 @@ const QueryPage = ({
);
};
-export default QueryPage;
+export default EditQueryPage;
diff --git a/frontend/pages/queries/edit/EditQueryPage/_styles.scss b/frontend/pages/queries/edit/EditQueryPage/_styles.scss
new file mode 100644
index 0000000000..f4ffcbbfd2
--- /dev/null
+++ b/frontend/pages/queries/edit/EditQueryPage/_styles.scss
@@ -0,0 +1,54 @@
+.edit-query-page {
+ .body-wrap {
+ min-width: 0;
+ }
+
+ &__warning {
+ padding: $pad-medium;
+ font-size: $x-small;
+ color: $core-fleet-black;
+ background-color: #fff0b9;
+ border: 1px solid #f2c94c;
+ border-radius: $border-radius;
+ margin: 0;
+ margin-top: $pad-large;
+
+ p {
+ margin: 0;
+ line-height: 20px;
+ }
+ }
+
+ .ace_content {
+ min-height: 500px !important;
+ }
+
+ &__count-spinner {
+ margin-right: $pad-small;
+ }
+ &__page-loading {
+ .loading-spinner {
+ margin: $pad-large 0 0;
+ }
+ }
+ &__page-error {
+ h4 {
+ margin: 0;
+ margin-top: 28px;
+ margin-left: -7px;
+ font-size: $small;
+
+ img {
+ transform: scale(0.5);
+ vertical-align: middle;
+ position: relative;
+ top: -2px;
+ }
+ }
+ p {
+ margin: 0;
+ margin-top: $pad-medium;
+ font-size: $x-small;
+ }
+ }
+}
diff --git a/frontend/pages/queries/edit/EditQueryPage/index.ts b/frontend/pages/queries/edit/EditQueryPage/index.ts
new file mode 100644
index 0000000000..29c0d100ac
--- /dev/null
+++ b/frontend/pages/queries/edit/EditQueryPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./EditQueryPage";
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
similarity index 98%
rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx
rename to frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
index 87bc29c8fd..bb5286895f 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
@@ -68,7 +68,6 @@ describe("QueryForm - component", () => {
isQueryUpdating={false}
saveQuery={jest.fn()}
onOsqueryTableSelect={jest.fn()}
- goToSelectTargets={jest.fn()}
onUpdate={jest.fn()}
onOpenSchemaSidebar={jest.fn()}
renderLiveQueryWarning={jest.fn()}
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
similarity index 95%
rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx
rename to frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
index 452e06d4ee..a704ee2ef8 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
@@ -15,7 +15,11 @@ import PATHS from "router/paths";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { NotificationContext } from "context/notification";
-import { addGravatarUrlToResource, secondsToDhms } from "utilities/helpers";
+import {
+ addGravatarUrlToResource,
+ secondsToDhms,
+ TAGGED_TEMPLATES,
+} from "utilities/helpers";
import {
FREQUENCY_DROPDOWN_OPTIONS,
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
@@ -48,6 +52,7 @@ import Spinner from "components/Spinner";
import Icon from "components/Icon/Icon";
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
import SaveQueryModal from "../SaveQueryModal";
+import SaveChangesModal from "../SaveChangesModal";
const baseClass = "query-form";
@@ -63,11 +68,11 @@ interface IQueryFormProps {
isQueryUpdating: boolean;
saveQuery: (formData: ICreateQueryRequestBody) => void;
onOsqueryTableSelect: (tableName: string) => void;
- goToSelectTargets: () => void;
onUpdate: (formData: ICreateQueryRequestBody) => void;
onOpenSchemaSidebar: () => void;
renderLiveQueryWarning: () => JSX.Element | null;
backendValidators: { [key: string]: string };
+ hostId?: number;
}
const validateQuerySQL = (query: string) => {
@@ -110,11 +115,11 @@ const QueryForm = ({
isQueryUpdating,
saveQuery,
onOsqueryTableSelect,
- goToSelectTargets,
onUpdate,
onOpenSchemaSidebar,
renderLiveQueryWarning,
backendValidators,
+ hostId,
}: IQueryFormProps): JSX.Element => {
// Note: The QueryContext values should always be used for any mutable query data such as query name
// The storedQuery prop should only be used to access immutable metadata such as author id
@@ -153,6 +158,7 @@ const QueryForm = ({
const savedQueryMode = !!queryIdForEdit;
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
+ const [showSaveChangesModal, setShowSaveChangesModal] = useState(false); // #7766 implementation
const [showQueryEditor, setShowQueryEditor] = useState(
isObserverPlus || isAnyTeamObserverPlus || false
);
@@ -205,6 +211,11 @@ const QueryForm = ({
setShowSaveQueryModal(!showSaveQueryModal);
};
+ // #7766 implementation
+ const toggleSaveChangesModal = () => {
+ setShowSaveChangesModal(!showSaveChangesModal);
+ };
+
const onLoad = (editor: IAceEditor) => {
editor.setOptions({
enableLinking: true,
@@ -400,6 +411,12 @@ const QueryForm = ({
logging: lastEditedQueryLoggingType,
});
}
+
+ // #7766 implementation
+ // savedQueryMode
+ // ? setShowSaveChangesModal(true)
+ // : setShowSaveQueryModal(true);
+ // TODO: onUpdate for saveChangesModal
}
};
@@ -575,7 +592,13 @@ const QueryForm = ({
{
+ queryIdForEdit &&
+ router.push(
+ PATHS.LIVE_QUERY(queryIdForEdit) +
+ TAGGED_TEMPLATES.queryByHostRoute(hostId)
+ );
+ }}
>
Live query
@@ -749,7 +772,13 @@ const QueryForm = ({
{
+ queryIdForEdit &&
+ router.push(
+ PATHS.LIVE_QUERY(queryIdForEdit) +
+ TAGGED_TEMPLATES.queryByHostRoute(hostId)
+ );
+ }}
>
Live query
@@ -765,6 +794,13 @@ const QueryForm = ({
isLoading={isQuerySaving}
/>
)}
+ {showSaveChangesModal && (
+
+ )}
>
);
};
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss b/frontend/pages/queries/edit/components/QueryForm/_styles.scss
similarity index 98%
rename from frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss
rename to frontend/pages/queries/edit/components/QueryForm/_styles.scss
index 5a9ab0d49f..bc63146bac 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss
+++ b/frontend/pages/queries/edit/components/QueryForm/_styles.scss
@@ -3,11 +3,6 @@
position: relative;
font-size: $x-small;
- .query-page__warning {
- margin: 0;
- margin-top: $pad-large;
- }
-
.form-field--input {
margin: 0;
}
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/index.ts b/frontend/pages/queries/edit/components/QueryForm/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryForm/index.ts
rename to frontend/pages/queries/edit/components/QueryForm/index.ts
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx
rename to frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx
rename to frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss b/frontend/pages/queries/edit/components/QueryResults/_styles.scss
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss
rename to frontend/pages/queries/edit/components/QueryResults/_styles.scss
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/index.ts b/frontend/pages/queries/edit/components/QueryResults/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/index.ts
rename to frontend/pages/queries/edit/components/QueryResults/index.ts
diff --git a/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
new file mode 100644
index 0000000000..362af3918d
--- /dev/null
+++ b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+
+import Button from "components/buttons/Button";
+import Modal from "components/Modal";
+import { ICreateQueryRequestBody } from "interfaces/schedulable_query";
+
+const baseClass = "save-changes-modal";
+
+export interface ISaveChangesModalProps {
+ isUpdating: boolean;
+ onSaveChanges: (formData: ICreateQueryRequestBody) => void;
+ toggleSaveChangesModal: () => void;
+ sqlUpdated?: boolean;
+}
+
+const SaveChangesModal = ({
+ isUpdating,
+ onSaveChanges,
+ toggleSaveChangesModal,
+ sqlUpdated = false,
+}: ISaveChangesModalProps): JSX.Element => {
+ const warningText = () => {
+ if (sqlUpdated) {
+ return "Changing this query's SQL will delete its previous results, since the existing report does not reflect the updated query.";
+ }
+ return "The changes you are making to this query will delete its previous results.";
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default SaveChangesModal;
diff --git a/frontend/pages/queries/edit/components/SaveChangesModal/index.ts b/frontend/pages/queries/edit/components/SaveChangesModal/index.ts
new file mode 100644
index 0000000000..15fa4a05cf
--- /dev/null
+++ b/frontend/pages/queries/edit/components/SaveChangesModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./SaveChangesModal";
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
similarity index 51%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx
rename to frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
index f74a45f66c..a8cd8dc099 100644
--- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx
+++ b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
@@ -141,116 +141,114 @@ const SaveQueryModal = ({
return (
- <>
-
);
};
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss b/frontend/pages/queries/edit/components/SaveQueryModal/_styles.scss
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss
rename to frontend/pages/queries/edit/components/SaveQueryModal/_styles.scss
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts b/frontend/pages/queries/edit/components/SaveQueryModal/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts
rename to frontend/pages/queries/edit/components/SaveQueryModal/index.ts
diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
new file mode 100644
index 0000000000..c49d4c333b
--- /dev/null
+++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
@@ -0,0 +1,213 @@
+import React, { useState, useEffect, useContext, useCallback } from "react";
+import { useQuery } from "react-query";
+import { useErrorHandler } from "react-error-boundary";
+import { InjectedRouter, Params } from "react-router/lib/Router";
+import PATHS from "router/paths";
+
+import { AppContext } from "context/app";
+import { QueryContext } from "context/query";
+import { LIVE_QUERY_STEPS, DEFAULT_QUERY } from "utilities/constants";
+import queryAPI from "services/entities/queries";
+import hostAPI from "services/entities/hosts";
+import statusAPI from "services/entities/status";
+import { IHost, IHostResponse } from "interfaces/host";
+import { ILabel } from "interfaces/label";
+import { ITeam } from "interfaces/team";
+import {
+ IGetQueryResponse,
+ ISchedulableQuery,
+} from "interfaces/schedulable_query";
+
+import MainContent from "components/MainContent";
+import SelectTargets from "components/LiveQuery/SelectTargets";
+
+import RunQuery from "pages/queries/live/screens/RunQuery";
+import useTeamIdParam from "hooks/useTeamIdParam";
+
+interface IRunQueryPageProps {
+ router: InjectedRouter;
+ params: Params;
+ location: {
+ pathname: string;
+ query: { host_ids: string; team_id?: string };
+ search: string;
+ };
+}
+
+const baseClass = "run-query-page";
+
+const RunQueryPage = ({
+ router,
+ params: { id: paramsQueryId },
+ location,
+}: IRunQueryPageProps): JSX.Element => {
+ const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
+ const {
+ currentTeamName: teamNameForQuery,
+ teamIdForApi: apiTeamIdForQuery,
+ } = useTeamIdParam({
+ location,
+ router,
+ includeAllTeams: true,
+ includeNoTeam: false,
+ });
+
+ const handlePageError = useErrorHandler();
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isAnyTeamMaintainerOrTeamAdmin,
+ isObserverPlus,
+ isAnyTeamObserverPlus,
+ } = useContext(AppContext);
+ const {
+ selectedQueryTargets,
+ setSelectedQueryTargets,
+ setLastEditedQueryId,
+ setLastEditedQueryName,
+ setLastEditedQueryDescription,
+ setLastEditedQueryBody,
+ setLastEditedQueryObserverCanRun,
+ setLastEditedQueryFrequency,
+ setLastEditedQueryLoggingType,
+ setLastEditedQueryMinOsqueryVersion,
+ setLastEditedQueryPlatforms,
+ } = useContext(QueryContext);
+
+ const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
+ const [step, setStep] = useState(LIVE_QUERY_STEPS[1]);
+ const [targetedHosts, setTargetedHosts] = useState([]);
+ const [targetedLabels, setTargetedLabels] = useState([]);
+ const [targetedTeams, setTargetedTeams] = useState([]);
+ const [targetsTotalCount, setTargetsTotalCount] = useState(0);
+ const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
+
+ const TAGGED_TEMPLATES = {
+ queryByHostRoute: (hostId: number | undefined | null) => {
+ return `${hostId ? `?host_ids=${hostId}` : ""}`;
+ },
+ };
+
+ // disabled on page load so we can control the number of renders
+ // else it will re-populate the context on occasion
+ const { data: storedQuery } = useQuery<
+ IGetQueryResponse,
+ Error,
+ ISchedulableQuery
+ >(["query", queryId], () => queryAPI.load(queryId as number), {
+ enabled: !!queryId,
+ refetchOnWindowFocus: false,
+ select: (data) => data.query,
+ onSuccess: (returnedQuery) => {
+ setLastEditedQueryId(returnedQuery.id);
+ setLastEditedQueryName(returnedQuery.name);
+ setLastEditedQueryDescription(returnedQuery.description);
+ setLastEditedQueryBody(returnedQuery.query);
+ setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
+ setLastEditedQueryFrequency(returnedQuery.interval);
+ setLastEditedQueryPlatforms(returnedQuery.platform);
+ setLastEditedQueryLoggingType(returnedQuery.logging);
+ setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
+ },
+ onError: (error) => handlePageError(error),
+ });
+
+ useQuery(
+ "hostFromURL",
+ () =>
+ hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
+ {
+ enabled: !!location.query.host_ids && !queryParamHostsAdded,
+ select: (data: IHostResponse) => data.host,
+ onSuccess: (host) => {
+ setTargetedHosts((prevHosts) =>
+ prevHosts.filter((h) => h.id !== host.id).concat(host)
+ );
+ const targets = selectedQueryTargets;
+ host.target_type = "hosts";
+ targets.push(host);
+ setSelectedQueryTargets([...targets]);
+ if (!queryParamHostsAdded) {
+ setQueryParamHostsAdded(true);
+ }
+ router.replace(location.pathname);
+ },
+ }
+ );
+
+ const detectIsFleetQueryRunnable = () => {
+ statusAPI.live_query().catch(() => {
+ setIsLiveQueryRunnable(false);
+ });
+ };
+
+ useEffect(() => {
+ detectIsFleetQueryRunnable();
+ if (!queryId) {
+ setLastEditedQueryId(DEFAULT_QUERY.id);
+ setLastEditedQueryName(DEFAULT_QUERY.name);
+ setLastEditedQueryDescription(DEFAULT_QUERY.description);
+ setLastEditedQueryBody(DEFAULT_QUERY.query);
+ setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
+ setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
+ setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
+ setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
+ setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
+ }
+ }, [queryId]);
+
+ // Updates title that shows up on browser tabs
+ useEffect(() => {
+ // e.g., Run live query | Discover TLS certificates | Fleet for osquery
+ document.title = `Run live query | ${storedQuery?.name} | Fleet for osquery`;
+ }, [location.pathname, storedQuery?.name]);
+
+ const goToQueryEditor = useCallback(
+ () => queryId && router.push(PATHS.EDIT_QUERY(queryId)),
+ []
+ );
+ // const params = { id: paramsQueryId };
+
+ const renderScreen = () => {
+ const step1Props = {
+ baseClass,
+ queryId,
+ selectedTargets: selectedQueryTargets,
+ targetedHosts,
+ targetedLabels,
+ targetedTeams,
+ targetsTotalCount,
+ goToQueryEditor,
+ goToRunQuery: () => setStep(LIVE_QUERY_STEPS[2]),
+ setSelectedTargets: setSelectedQueryTargets,
+ setTargetedHosts,
+ setTargetedLabels,
+ setTargetedTeams,
+ setTargetsTotalCount,
+ };
+
+ const step2Props = {
+ queryId,
+ selectedTargets: selectedQueryTargets,
+ storedQuery,
+ setSelectedTargets: setSelectedQueryTargets,
+ goToQueryEditor,
+ targetsTotalCount,
+ };
+
+ switch (step) {
+ case LIVE_QUERY_STEPS[2]:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ {renderScreen()}
+
+ );
+};
+
+export default RunQueryPage;
diff --git a/frontend/pages/queries/QueryPage/_styles.scss b/frontend/pages/queries/live/LiveQueryPage/_styles.scss
similarity index 69%
rename from frontend/pages/queries/QueryPage/_styles.scss
rename to frontend/pages/queries/live/LiveQueryPage/_styles.scss
index 8e1198b7f8..1ec2385d09 100644
--- a/frontend/pages/queries/QueryPage/_styles.scss
+++ b/frontend/pages/queries/live/LiveQueryPage/_styles.scss
@@ -1,4 +1,4 @@
-.query-page {
+.run-query-page {
.body-wrap {
min-width: 0;
}
@@ -9,61 +9,6 @@
min-height: 400px;
}
- &__warning {
- padding: $pad-medium;
- font-size: $x-small;
- color: $core-fleet-black;
- background-color: #fff0b9;
- border: 1px solid #f2c94c;
- border-radius: $border-radius;
-
- p {
- margin: 0;
- line-height: 20px;
- }
- }
-
- &__observer-query-view {
- width: 90%;
- max-width: 1060px;
- margin: 0 auto;
- color: $core-fleet-black;
-
- h1 {
- font-size: $medium;
- }
- p {
- font-size: $x-small;
- }
- }
-
- &__observer-query-details {
- padding: 0 2rem;
-
- h1 {
- margin: $pad-large 0;
- font-size: $large;
- }
-
- p {
- margin-bottom: $pad-small;
- }
-
- .sql-button {
- color: $core-vibrant-blue;
- font-weight: $bold;
- font-size: $x-small;
- }
- }
-
- &__query-preview {
- margin-top: 15px;
-
- .fleet-ace__label {
- display: none;
- }
- }
-
.ace_content {
min-height: 500px !important;
}
@@ -172,10 +117,4 @@
font-size: $x-small;
}
}
-
- .targets-input {
- .input-icon-field__icon {
- top: 34px; // Override styling to include label header
- }
- }
}
diff --git a/frontend/pages/queries/live/LiveQueryPage/index.ts b/frontend/pages/queries/live/LiveQueryPage/index.ts
new file mode 100644
index 0000000000..354c0445d1
--- /dev/null
+++ b/frontend/pages/queries/live/LiveQueryPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./LiveQueryPage";
diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/live/screens/RunQuery.tsx
similarity index 98%
rename from frontend/pages/queries/QueryPage/screens/RunQuery.tsx
rename to frontend/pages/queries/live/screens/RunQuery.tsx
index dbceec8069..7715899b34 100644
--- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
+++ b/frontend/pages/queries/live/screens/RunQuery.tsx
@@ -15,7 +15,7 @@ import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IQuery } from "interfaces/query";
import { ITarget } from "interfaces/target";
-import QueryResults from "../components/QueryResults";
+import QueryResults from "../../edit/components/QueryResults";
interface IRunQueryProps {
storedQuery: IQuery | undefined;
diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx
index f314b5ecc8..07f1256436 100644
--- a/frontend/router/index.tsx
+++ b/frontend/router/index.tsx
@@ -36,7 +36,9 @@ import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
import NoAccessPage from "pages/NoAccessPage";
import PackComposerPage from "pages/packs/PackComposerPage";
import PolicyPage from "pages/policies/PolicyPage";
-import QueryPage from "pages/queries/QueryPage";
+import QueryDetailsPage from "pages/queries/details/QueryDetailsPage";
+import LiveQueryPage from "pages/queries/live/LiveQueryPage";
+import EditQueryPage from "pages/queries/edit/EditQueryPage";
import RegistrationPage from "pages/RegistrationPage";
import ResetPasswordPage from "pages/ResetPasswordPage";
import MDMAppleSSOPage from "pages/MDMAppleSSOPage";
@@ -220,9 +222,13 @@ const routes = (
-
+
+
+
+
+
+
-
diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts
index 834403705e..3190f09c45 100644
--- a/frontend/router/paths.ts
+++ b/frontend/router/paths.ts
@@ -53,6 +53,16 @@ export default {
return `${URL_PREFIX}/labels/${labelId}`;
},
EDIT_QUERY: (queryId: number, teamId?: number): string => {
+ return `${URL_PREFIX}/queries/${queryId}/edit${
+ teamId ? `?team_id=${teamId}` : ""
+ }`;
+ },
+ LIVE_QUERY: (queryId: number, teamId?: number): string => {
+ return `${URL_PREFIX}/queries/${queryId}/live${
+ teamId ? `?team_id=${teamId}` : ""
+ }`;
+ },
+ QUERY: (queryId: number, teamId?: number): string => {
return `${URL_PREFIX}/queries/${queryId}${
teamId ? `?team_id=${teamId}` : ""
}`;
diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts
index 89765f6481..bfad2ff9ed 100644
--- a/frontend/services/entities/queries.ts
+++ b/frontend/services/entities/queries.ts
@@ -54,10 +54,10 @@ export default {
queryId: number | null;
selected: ISelectedTargets;
}) => {
- const { RUN_QUERY } = endpoints;
+ const { LIVE_QUERY } = endpoints;
try {
- const { campaign } = await sendRequest("POST", RUN_QUERY, {
+ const { campaign } = await sendRequest("POST", LIVE_QUERY, {
query,
query_id: queryId,
selected,
diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts
index 14530e240b..079e929753 100644
--- a/frontend/utilities/constants.ts
+++ b/frontend/utilities/constants.ts
@@ -96,12 +96,17 @@ export const MIN_OSQUERY_VERSION_OPTIONS = [
{ label: "1.8.1 +", value: "1.8.1" },
];
-export const QUERIES_PAGE_STEPS = {
+export const LIVE_POLICY_STEPS = {
1: "EDITOR",
2: "TARGETS",
3: "RUN",
};
+export const LIVE_QUERY_STEPS = {
+ 1: "TARGETS",
+ 2: "RUN",
+};
+
export const DEFAULT_QUERY: ISchedulableQuery = {
description: "",
name: "",
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index ce6d89b952..e1a3880b99 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -82,7 +82,7 @@ export default {
PERFORM_REQUIRED_PASSWORD_RESET: `/${API_VERSION}/fleet/perform_required_password_reset`,
QUERIES: `/${API_VERSION}/fleet/queries`,
RESET_PASSWORD: `/${API_VERSION}/fleet/reset_password`,
- RUN_QUERY: `/${API_VERSION}/fleet/queries/run`,
+ LIVE_QUERY: `/${API_VERSION}/fleet/queries/run`,
SCHEDULE_QUERY: `/${API_VERSION}/fleet/packs/schedule`,
SCHEDULED_QUERIES: (packId: number): string => {
return `/${API_VERSION}/fleet/packs/${packId}/scheduled`;
diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts
index 641b5cf734..c519b9a263 100644
--- a/frontend/utilities/helpers.ts
+++ b/frontend/utilities/helpers.ts
@@ -910,6 +910,12 @@ export const getSoftwareBundleTooltipMarkup = (bundle: string) => {
`;
};
+export const TAGGED_TEMPLATES = {
+ queryByHostRoute: (hostId: number | undefined | null) => {
+ return `${hostId ? `?host_ids=${hostId}` : ""}`;
+ },
+};
+
export default {
addGravatarUrlToResource,
formatConfigDataForServer,
@@ -945,4 +951,5 @@ export default {
syntaxHighlight,
normalizeEmptyValues,
wrapFleetHelper,
+ TAGGED_TEMPLATES,
};
From c63e87fe1772804fac02a523c0df7e559f452deb Mon Sep 17 00:00:00 2001
From: Tim Lee
Date: Tue, 3 Oct 2023 11:26:44 -0700
Subject: [PATCH 02/41] 13485 create query_results table migration (#14205)
---
.../20230927155121_CreateTableQueryReports.go | 35 +++++++
...0927155121_CreateTableQueryReports_test.go | 91 +++++++++++++++++++
.../mysql/migrations/tables/migration_test.go | 60 ++++++++++++
server/datastore/mysql/schema.sql | 21 ++++-
4 files changed, 205 insertions(+), 2 deletions(-)
create mode 100644 server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports.go
create mode 100644 server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports_test.go
diff --git a/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports.go b/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports.go
new file mode 100644
index 0000000000..4949540d7a
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports.go
@@ -0,0 +1,35 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20230927155121, Down_20230927155121)
+}
+
+func Up_20230927155121(tx *sql.Tx) error {
+ _, err := tx.Exec(`
+ CREATE TABLE query_results (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ query_id INT(10) UNSIGNED NOT NULL,
+ host_id INT(10) UNSIGNED NOT NULL,
+ osquery_version VARCHAR(50),
+ error TEXT COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ last_fetched TIMESTAMP NOT NULL,
+ data JSON,
+ FOREIGN KEY (query_id) REFERENCES queries(id) ON DELETE CASCADE,
+ FOREIGN KEY (host_id) REFERENCES hosts(id)
+ );
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to create table query_results: %w", err)
+ }
+
+ return nil
+}
+
+func Down_20230927155121(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports_test.go b/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports_test.go
new file mode 100644
index 0000000000..cbd11d474a
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports_test.go
@@ -0,0 +1,91 @@
+package tables
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20230927155121(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ // Apply current migration.
+ applyNext(t, db)
+
+ // Insert a record into query_results
+ insertStmt := `INSERT INTO query_results (
+ query_id, host_id, osquery_version, error, last_fetched, data
+ ) VALUES (?, ?, ?, ?, ?, ?)`
+
+ queryID := insertQuery(t, db)
+ hostID := insertHost(t, db)
+ osqueryVersion := "5.9.1"
+ lastFetched := time.Now().UTC()
+
+ // Example JSON data for data field
+ osqueryData := map[string]string{
+ "model": "USB Keyboard",
+ "vendor": "Apple Inc.",
+ }
+ jsonData, err := json.Marshal(osqueryData)
+ require.NoError(t, err)
+
+ res, err := db.Exec(insertStmt, queryID, hostID, osqueryVersion, "", lastFetched, jsonData)
+ require.NoError(t, err)
+
+ id, _ := res.LastInsertId()
+
+ // Insert a sample error result containing a NULL data field
+ errorMessage := "Some error message"
+ _, err = db.Exec(insertStmt, queryID, hostID, osqueryVersion, errorMessage, lastFetched, nil)
+ require.NoError(t, err)
+
+ type QueryResult struct {
+ ID uint `db:"id"`
+ QueryID uint `db:"query_id"`
+ HostID uint `db:"host_id"`
+ OsqueryVersion string `db:"osquery_version"`
+ Error string `db:"error"`
+ LastFetched time.Time `db:"last_fetched"`
+ OsqueryResultData *json.RawMessage `db:"data"`
+ }
+
+ // Load the 1st result
+ var queryReport []QueryResult
+ selectStmt := `
+ SELECT id, query_id, host_id, osquery_version, error, last_fetched, data
+ FROM query_results
+ WHERE query_id = ? AND host_id = ?
+ ORDER BY id ASC
+ `
+ err = db.Select(&queryReport, selectStmt, queryID, hostID)
+ require.NoError(t, err)
+
+ require.Equal(t, queryID, queryReport[0].QueryID)
+ require.Equal(t, hostID, queryReport[0].HostID)
+ require.Equal(t, osqueryVersion, queryReport[0].OsqueryVersion)
+ require.Empty(t, queryReport[0].Error)
+ require.True(t, lastFetched.Sub(queryReport[0].LastFetched) < time.Second)
+ require.JSONEq(t, string(jsonData), string(*queryReport[0].OsqueryResultData))
+
+ // Error results should be loaded as well
+ require.Equal(t, queryID, queryReport[1].QueryID)
+ require.Equal(t, hostID, queryReport[1].HostID)
+ require.Equal(t, osqueryVersion, queryReport[1].OsqueryVersion)
+ require.Equal(t, errorMessage, queryReport[1].Error)
+ require.True(t, lastFetched.Sub(queryReport[1].LastFetched) < time.Second) // allow a 1 sec difference to account for time to run the query
+ require.Empty(t, queryReport[1].OsqueryResultData)
+
+ // Delete the query we just created to test the ON DELETE CASCADE
+ deleteQueryStmt := `DELETE FROM queries WHERE id = ?`
+ _, err = db.Exec(deleteQueryStmt, queryID)
+ require.NoError(t, err)
+
+ // Verify that both query_result records were deleted
+ var count int
+ err = db.Get(&count, "SELECT COUNT(*) FROM query_results WHERE id = ?", id)
+ require.NoError(t, err)
+ require.Equal(t, 0, count)
+}
diff --git a/server/datastore/mysql/migrations/tables/migration_test.go b/server/datastore/mysql/migrations/tables/migration_test.go
index 8de460812e..402a6d60f0 100644
--- a/server/datastore/mysql/migrations/tables/migration_test.go
+++ b/server/datastore/mysql/migrations/tables/migration_test.go
@@ -89,3 +89,63 @@ func applyNext(t *testing.T, db *sqlx.DB) {
err := MigrationClient.UpByOne(db.DB, gooseNoDir)
require.NoError(t, err)
}
+
+func insertQuery(t *testing.T, db *sqlx.DB) uint {
+ // Insert a record into queries table
+ insertQueryStmt := `
+ INSERT INTO queries (
+ name, description, query, observer_can_run, platform, logging_type
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ `
+
+ queryName := "Test Query"
+ queryDescription := "A test query for the test suite"
+ queryValue := "SELECT * FROM apps;"
+ observerCanRun := 0
+ platform := "mac" // Just a placeholder, adjust as needed
+ loggingType := "snapshot"
+
+ res, err := db.Exec(insertQueryStmt, queryName, queryDescription, queryValue, observerCanRun, platform, loggingType)
+ require.NoError(t, err)
+
+ id, err := res.LastInsertId()
+ require.NoError(t, err)
+
+ return uint(id)
+}
+
+func insertHost(t *testing.T, db *sqlx.DB) uint {
+ // Insert a minimal record into hosts table
+ insertHostStmt := `
+ INSERT INTO hosts (
+ hostname, uuid, platform, osquery_version, os_version, build, platform_like, code_name,
+ cpu_type, cpu_subtype, cpu_brand, hardware_vendor, hardware_model, hardware_version,
+ hardware_serial, computer_name
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `
+
+ hostName := "Dummy Hostname"
+ hostUUID := "12345678-1234-1234-1234-123456789012"
+ hostPlatform := "windows"
+ osqueryVer := "5.9.1"
+ osVersion := "Windows 10"
+ buildVersion := "10.0.19042.1234"
+ platformLike := "windows"
+ codeName := "20H2"
+ cpuType := "x86_64"
+ cpuSubtype := "x86_64"
+ cpuBrand := "Intel"
+ hwVendor := "Dell Inc."
+ hwModel := "OptiPlex 7090"
+ hwVersion := "1.0"
+ hwSerial := "ABCDEFGHIJ"
+ computerName := "DESKTOP-TEST"
+
+ res, err := db.Exec(insertHostStmt, hostName, hostUUID, hostPlatform, osqueryVer, osVersion, buildVersion, platformLike, codeName, cpuType, cpuSubtype, cpuBrand, hwVendor, hwModel, hwVersion, hwSerial, computerName)
+ require.NoError(t, err)
+
+ id, err := res.LastInsertId()
+ require.NoError(t, err)
+
+ return uint(id)
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 93c1e2703d..3554079383 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -685,9 +685,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=209 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB AUTO_INCREMENT=210 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20230927155121,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_device_management_solutions` (
@@ -1060,6 +1060,23 @@ CREATE TABLE `queries` (
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `query_results` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `query_id` int(10) unsigned NOT NULL,
+ `host_id` int(10) unsigned NOT NULL,
+ `osquery_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `error` text COLLATE utf8mb4_unicode_ci,
+ `last_fetched` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `data` json DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `query_id` (`query_id`),
+ KEY `host_id` (`host_id`),
+ CONSTRAINT `query_results_ibfk_1` FOREIGN KEY (`query_id`) REFERENCES `queries` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `query_results_ibfk_2` FOREIGN KEY (`host_id`) REFERENCES `hosts` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `scep_certificates` (
`serial` bigint(20) NOT NULL,
`name` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
From fe0589c9e5109fdd65b56d79dd8330b0685f7d71 Mon Sep 17 00:00:00 2001
From: Jahziel Villasana-Espinoza
Date: Tue, 3 Oct 2023 15:43:38 -0400
Subject: [PATCH 03/41] feat: discard data column for queries (#14269)
Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
---
.../20231002120317_AddDiscardToQueries.go | 18 ++++++
...20231002120317_AddDiscardToQueries_test.go | 60 +++++++++++++++++++
server/datastore/mysql/schema.sql | 5 +-
server/fleet/queries.go | 2 +
4 files changed, 83 insertions(+), 2 deletions(-)
create mode 100644 server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries.go
create mode 100644 server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries_test.go
diff --git a/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries.go b/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries.go
new file mode 100644
index 0000000000..69c5475046
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries.go
@@ -0,0 +1,18 @@
+package tables
+
+import (
+ "database/sql"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20231002120317, Down_20231002120317)
+}
+
+func Up_20231002120317(tx *sql.Tx) error {
+ _, err := tx.Exec(`ALTER TABLE queries ADD COLUMN discard_data TINYINT(1) NOT NULL DEFAULT FALSE;`)
+ return err
+}
+
+func Down_20231002120317(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries_test.go b/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries_test.go
new file mode 100644
index 0000000000..5d4018e2b4
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20231002120317_AddDiscardToQueries_test.go
@@ -0,0 +1,60 @@
+package tables
+
+import (
+ "testing"
+
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20231002120317(t *testing.T) {
+ db := applyUpToPrev(t)
+ applyNext(t, db)
+
+ //
+ // Check data, insert new entries, e.g. to verify migration is safe.
+ //
+ insertStmt := `INSERT INTO queries (
+ name, description, query, discard_data
+ ) VALUES (?, ?, ?, ?)`
+
+ res, err := db.Exec(insertStmt, "test", "test description", "SELECT 1 from hosts", true)
+ require.NoError(t, err)
+ id, _ := res.LastInsertId()
+ require.NotNil(t, id)
+ require.Equal(t, int64(1), id)
+
+ var query []fleet.Query
+ err = db.Select(&query, `SELECT
+ id,
+ name,
+ description,
+ query,
+ discard_data
+ FROM queries WHERE id = ?`, id)
+ require.NoError(t, err)
+ require.True(t, query[0].DiscardData)
+
+ // Insert without discard_data, verify that default is correct
+
+ insertStmt = `INSERT INTO queries (
+ name, description, query
+ ) VALUES (?, ?, ?)`
+
+ res, err = db.Exec(insertStmt, "test 2", "test description 2", "SELECT 1 from hosts")
+ require.NoError(t, err)
+ id, _ = res.LastInsertId()
+ require.NotNil(t, id)
+ require.Equal(t, int64(2), id)
+
+ var queryNoDiscard []fleet.Query
+ err = db.Select(&queryNoDiscard, `SELECT
+ id,
+ name,
+ description,
+ query,
+ discard_data
+ FROM queries WHERE id = ?`, id)
+ require.NoError(t, err)
+ require.False(t, queryNoDiscard[0].DiscardData)
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 3554079383..a56102adb7 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -685,9 +685,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=210 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB AUTO_INCREMENT=211 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20230927155121,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20230927155121,1,'2020-01-01 01:01:01'),(210,20231002120317,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_device_management_solutions` (
@@ -1049,6 +1049,7 @@ CREATE TABLE `queries` (
`schedule_interval` int(10) unsigned NOT NULL DEFAULT '0',
`automations_enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
`logging_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'snapshot',
+ `discard_data` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_team_id_name_unq` (`team_id_char`,`name`),
UNIQUE KEY `idx_name_team_id_unq` (`name`,`team_id_char`),
diff --git a/server/fleet/queries.go b/server/fleet/queries.go
index 867680573a..ca3bb8876b 100644
--- a/server/fleet/queries.go
+++ b/server/fleet/queries.go
@@ -91,6 +91,8 @@ type Query struct {
//
// This field has null values if the query did not run as a schedule on any host.
AggregatedStats `json:"stats"`
+ // DiscardData indicates if the scheduled query results should be discarded (true) or kept (false) in a query report.
+ DiscardData bool `json:"discard_data" db:"discard_data"`
}
var (
From be0ce917c113976f85e9ebae161b165cfb51cc73 Mon Sep 17 00:00:00 2001
From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com>
Date: Wed, 4 Oct 2023 11:31:26 -0700
Subject: [PATCH 04/41] UI - Add global "Disable query reports" setting to
advanced org settings (#14268)
## Addresses #13474
- small alignment issues with tooltip-wrapped text should be fixed by
upcoming TooltipWrapper refactor PR
## Checklist for submitter
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling
---
frontend/__mocks__/configMock.ts | 1 +
frontend/interfaces/config.ts | 1 +
.../cards/Advanced/Advanced.tsx | 24 ++++++++++++++++++-
3 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts
index c51d2895d5..9f9e8f638c 100644
--- a/frontend/__mocks__/configMock.ts
+++ b/frontend/__mocks__/configMock.ts
@@ -12,6 +12,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
live_query_disabled: false,
enable_analytics: true,
deferred_save_host: false,
+ query_reports_disabled: false,
},
smtp_settings: {
enable_smtp: false,
diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts
index c604629c0c..1d42508ee4 100644
--- a/frontend/interfaces/config.ts
+++ b/frontend/interfaces/config.ts
@@ -196,6 +196,7 @@ export interface IConfig {
live_query_disabled: boolean;
enable_analytics: boolean;
deferred_save_host: boolean;
+ query_reports_disabled: boolean;
};
smtp_settings: {
enable_smtp: boolean;
diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
index 61da4d5b8d..62f6d4d4b5 100644
--- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
+++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
@@ -18,7 +18,7 @@ const Advanced = ({
handleSubmit,
isUpdatingSettings,
}: IAppConfigFormProps): JSX.Element => {
- const [formData, setFormData] = useState({
+ const [formData, setFormData] = useState({
domain: appConfig.smtp_settings.domain || "",
verifySSLCerts: appConfig.smtp_settings.verify_ssl_certs || false,
enableStartTLS: appConfig.smtp_settings.enable_start_tls,
@@ -26,6 +26,8 @@ const Advanced = ({
appConfig.host_expiry_settings.host_expiry_enabled || false,
hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0,
disableLiveQuery: appConfig.server_settings.live_query_disabled || false,
+ disableQueryReports:
+ appConfig.server_settings.query_reports_disabled || false,
});
const {
@@ -35,6 +37,7 @@ const Advanced = ({
enableHostExpiry,
hostExpiryWindow,
disableLiveQuery,
+ disableQueryReports,
} = formData;
const [formErrors, setFormErrors] = useState({});
@@ -69,6 +72,7 @@ const Advanced = ({
server_url: appConfig.server_settings.server_url || "",
live_query_disabled: disableLiveQuery,
enable_analytics: appConfig.server_settings.enable_analytics,
+ query_reports_disabled: disableQueryReports,
},
smtp_settings: {
enable_smtp: appConfig.smtp_settings.enable_smtp || false,
@@ -172,6 +176,24 @@ const Advanced = ({
>
Disable live queries
+ Disabling query reports will decrease database usage, \
+ but will prevent you from accessing query results in \
+ Fleet and will delete existing reports. This can also be \
+ disabled on a per-query basis by enabling "Discard \
+ data". (Default: Off )
'
+ }
+ >
+ Disable query reports
+
From 38af9678f46e6f293e91e45241f5c1c6cef850f8 Mon Sep 17 00:00:00 2001
From: Tim Lee
Date: Wed, 4 Oct 2023 14:24:40 -0600
Subject: [PATCH 05/41] Backmerge from main (#14298)
---
.github/workflows/build-orbit.yaml | 2 +-
.../workflows/generate-desktop-targets.yml | 2 +-
cmd/osquery-perf/agent.go | 5 +
.../standard-query-library.yml | 22 ++
docs/Contributing/API-for-contributors.md | 6 +-
docs/Deploy/Deploy-Fleet-on-CentOS.md | 9 +-
docs/Deploy/Deploy-Fleet-on-Kubernetes.md | 26 +-
docs/Get started/FAQ.md | 5 +
docs/REST API/rest-api.md | 2 +-
docs/Using Fleet/CIS-Benchmarks.md | 31 ++
docs/Using Fleet/MDM-macOS-setup.md | 230 ++++++------
docs/Using Fleet/Usage-statistics.md | 4 +-
docs/Using Fleet/enroll-chromebooks.md | 1 -
docs/Using Fleet/enroll-hosts.md | 35 +-
docs/Using Fleet/fleetctl-CLI.md | 2 +-
handbook/business-operations/README.md | 1 +
handbook/ceo.md | 34 +-
handbook/company/ceo.rituals.yml | 2 +-
handbook/company/leadership.md | 8 +-
handbook/company/open-positions.yml | 31 --
handbook/company/pricing-features-table.yml | 2 +-
handbook/customers/README.md | 26 +-
handbook/product/README.md | 27 +-
orbit/CHANGELOG.md | 8 +
orbit/changes/13715-bump-go-version | 1 -
orbit/changes/13858-migration-tweaks | 1 -
orbit/changes/issue-13635-retry-invalid-token | 1 -
orbit/cmd/desktop/desktop.go | 5 +-
orbit/cmd/orbit/orbit.go | 1 +
.../20230927155121_CreateTableQueryReports.go | 35 --
...0927155121_CreateTableQueryReports_test.go | 91 -----
.../mysql/migrations/tables/migration_test.go | 60 ----
server/datastore/mysql/schema.sql | 21 +-
server/fleet/users.go | 2 +-
server/service/device_client.go | 13 +-
server/service/device_client_test.go | 2 +-
server/service/users.go | 2 +-
terraform/addons/saml-auth-proxy/outputs.tf | 4 +
website/api/helpers/strings/to-html.js | 18 +-
.../images/logo-atlassian-140x18@2x.png | Bin 0 -> 3289 bytes
.../assets/images/logo-fastly-75x30@2x.png | Bin 0 -> 2830 bytes
website/assets/images/logo-gusto-64x24@2x.png | Bin 0 -> 2649 bytes
.../assets/images/logo-segment-112x24@2x.png | Bin 0 -> 4656 bytes
.../images/logo-snowflake-117x28@2x.png | Bin 0 -> 5184 bytes
website/assets/images/logo-uber-70x24@2x.png | Bin 0 -> 2455 bytes
.../assets/images/logo-wayfair-110x24@2x.png | Bin 0 -> 4589 bytes
website/assets/images/quote-icon-18x12@2x.png | Bin 0 -> 814 bytes
...estimonial-austin-anderson-1440x810@2x.jpg | Bin 0 -> 918249 bytes
...ideo-testimonial-nick-fohs-1440x810@2x.jpg | Bin 0 -> 915504 bytes
website/assets/js/pages/homepage.page.js | 9 +
.../js/pages/vulnerability-management.page.js | 7 +
.../styles/pages/articles/basic-article.less | 12 +-
website/assets/styles/pages/homepage.less | 326 ++++++++++++++----
.../assets/styles/pages/query-library.less | 8 +
.../pages/vulnerability-management.less | 165 ++++++++-
website/scripts/build-static-content.js | 25 +-
.../views/pages/articles/basic-article.ejs | 2 +-
website/views/pages/homepage.ejs | 88 ++++-
website/views/pages/query-library.ejs | 3 +
website/views/pages/transparency.ejs | 39 ++-
.../views/pages/vulnerability-management.ejs | 16 +
61 files changed, 919 insertions(+), 559 deletions(-)
delete mode 100644 orbit/changes/13715-bump-go-version
delete mode 100644 orbit/changes/13858-migration-tweaks
delete mode 100644 orbit/changes/issue-13635-retry-invalid-token
delete mode 100644 server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports.go
delete mode 100644 server/datastore/mysql/migrations/tables/20230927155121_CreateTableQueryReports_test.go
create mode 100644 website/assets/images/logo-atlassian-140x18@2x.png
create mode 100644 website/assets/images/logo-fastly-75x30@2x.png
create mode 100644 website/assets/images/logo-gusto-64x24@2x.png
create mode 100644 website/assets/images/logo-segment-112x24@2x.png
create mode 100644 website/assets/images/logo-snowflake-117x28@2x.png
create mode 100644 website/assets/images/logo-uber-70x24@2x.png
create mode 100644 website/assets/images/logo-wayfair-110x24@2x.png
create mode 100644 website/assets/images/quote-icon-18x12@2x.png
create mode 100644 website/assets/images/video-testimonial-austin-anderson-1440x810@2x.jpg
create mode 100644 website/assets/images/video-testimonial-nick-fohs-1440x810@2x.jpg
diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml
index 8ad3432ad4..41ec1816c1 100644
--- a/.github/workflows/build-orbit.yaml
+++ b/.github/workflows/build-orbit.yaml
@@ -7,7 +7,7 @@ on:
- 'orbit/**.go'
env:
- ORBIT_VERSION: 1.16.0
+ ORBIT_VERSION: 1.17.0
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml
index 012df82902..05c91be3f3 100644
--- a/.github/workflows/generate-desktop-targets.yml
+++ b/.github/workflows/generate-desktop-targets.yml
@@ -24,7 +24,7 @@ defaults:
shell: bash
env:
- FLEET_DESKTOP_VERSION: 1.16.0
+ FLEET_DESKTOP_VERSION: 1.17.0
permissions:
contents: read
diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go
index 3507ae6e5b..19992886cf 100644
--- a/cmd/osquery-perf/agent.go
+++ b/cmd/osquery-perf/agent.go
@@ -551,6 +551,7 @@ func (a *agent) runOrbitLoop() {
if err != nil {
a.stats.IncrementOrbitErrors()
log.Println("orbitClient.GetConfig: ", err)
+ continue
}
if len(cfg.Notifications.PendingScriptExecutionIDs) > 0 {
// there are pending scripts to execute on this host, start a goroutine
@@ -562,6 +563,7 @@ func (a *agent) runOrbitLoop() {
if err := deviceClient.CheckToken(*a.deviceAuthToken); err != nil {
a.stats.IncrementOrbitErrors()
log.Println("deviceClient.CheckToken: ", err)
+ continue
}
}
case <-orbitTokenRotationTicker:
@@ -570,6 +572,7 @@ func (a *agent) runOrbitLoop() {
if err := orbitClient.SetOrUpdateDeviceToken(*newToken); err != nil {
a.stats.IncrementOrbitErrors()
log.Println("orbitClient.SetOrUpdateDeviceToken: ", err)
+ continue
}
a.deviceAuthToken = newToken
// fleet desktop performs a burst of check token requests after a token is rotated
@@ -579,11 +582,13 @@ func (a *agent) runOrbitLoop() {
if err := orbitClient.Ping(); err != nil {
a.stats.IncrementOrbitErrors()
log.Println("orbitClient.Ping: ", err)
+ continue
}
case <-fleetDesktopPolicyTicker:
if _, err := deviceClient.DesktopSummary(*a.deviceAuthToken); err != nil {
a.stats.IncrementDesktopErrors()
log.Println("deviceClient.NumberOfFailingPolicies: ", err)
+ continue
}
}
}
diff --git a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml
index b06e54ffe5..d15e474222 100644
--- a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml
+++ b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml
@@ -1045,3 +1045,25 @@ spec:
purpose: Informational
tags: crowdstrike, plist, network, content filter
contributors: zwass
+---
+apiVersion: v1
+kind: query
+spec:
+ name: Get a list of Visual Studio Code extensions
+ platform: darwin
+ description: Get a list of installed VS Code extensions. Requires (fleetd)[https://fleetdm.com/docs/using-fleet/fleetd].
+ query: |
+ SELECT split(user_path, '/', 1) as username,
+ json_extract(value, '$.identifier.id') as id,
+ json_extract(value, '$.identifier.uuid') as uuid,
+ json_extract(value, '$.location.path') as path,
+ json_extract(value, '$.version') as version,
+ json_extract(value, '$.metadata.publisherDisplayName') as publisher_display_name
+ FROM (
+ SELECT file_lines.path as user_path, value
+ FROM file_lines, json_each(line)
+ WHERE file_lines.path LIKE '/Users/%/.vscode/extensions/extensions.json'
+ );
+ purpose: Informational
+ tags: inventory
+ contributors: lucasmrod,sharon-fdm,zwass
diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md
index 73a7ccad11..7724b770b7 100644
--- a/docs/Contributing/API-for-contributors.md
+++ b/docs/Contributing/API-for-contributors.md
@@ -9,9 +9,11 @@
- [Downloadable installers](#downloadable-installers)
- [Setup](#setup)
-This document includes the Fleet API routes that are helpful when developing or contributing to Fleet.
+This document includes the internal Fleet API routes that are helpful when developing or contributing to Fleet.
-Unlike the [Fleet REST API documentation](https://fleetdm.com/docs/using-fleet/rest-api), only the Fleet UI, Fleet Desktop, and `fleetctl` clients use the API routes in this document:
+These endpoints are used by the Fleet UI, Fleet Desktop, and `fleetctl` clients and will frequently change to reflect current functionality.
+
+If you are interested in gathering information from Fleet in a production environment, please see the [public Fleet REST API documentation](https://fleetdm.com/docs/using-fleet/rest-api).
## Packs
diff --git a/docs/Deploy/Deploy-Fleet-on-CentOS.md b/docs/Deploy/Deploy-Fleet-on-CentOS.md
index 9910b7d7a4..173617fd27 100644
--- a/docs/Deploy/Deploy-Fleet-on-CentOS.md
+++ b/docs/Deploy/Deploy-Fleet-on-CentOS.md
@@ -19,12 +19,15 @@ vagrant ssh
### Installing Fleet
-To install Fleet, [download](https://github.com/fleetdm/fleet/releases), unzip, and move the latest Fleet binary to your desired install location.
+To install Fleet, [download](https://github.com/fleetdm/fleet/releases) the file named `Source code
+(zip)`, rename, unzip, and move the latest Fleet binary to your desired install location.
For example, after downloading:
```sh
-unzip fleet.zip 'linux/*' -d fleet
-sudo cp fleet/linux/fleet* /usr/bin/
+mv .zip fleet.zip
+unzip fleet.zip -d fleet
+sudo cp fleet /usr/bin/
+sudo chmod u+x /usr/bin/fleet
```
### Installing and configuring dependencies
diff --git a/docs/Deploy/Deploy-Fleet-on-Kubernetes.md b/docs/Deploy/Deploy-Fleet-on-Kubernetes.md
index 2314ac2ce4..5498218280 100644
--- a/docs/Deploy/Deploy-Fleet-on-Kubernetes.md
+++ b/docs/Deploy/Deploy-Fleet-on-Kubernetes.md
@@ -93,6 +93,8 @@ If you have not used Helm before, you must run the following to initialize your
helm init
```
+> Note: The helm init command has been removed in Helm v3. It performed two primary functions. First, it installed Tiller which is no longer needed. Second, it set up directories and repositories where Helm configuration lived. This is now automated in Helm v3; if the directory is not present it will be created.
+
### Deploying Fleet with Helm
To configure preferences for Fleet for use in Helm, including secret names, MySQL and Redis hostnames, and TLS certificates, download the [values.yaml](https://raw.githubusercontent.com/fleetdm/fleet/main/charts/fleet/values.yaml) and change the settings to match your configuration.
@@ -117,16 +119,24 @@ For the sake of this tutorial, we will again use Helm, this time to install MySQ
The MySQL that we will use for this tutorial is not replicated and it is not Highly Available. If you're deploying Fleet on a Kubernetes managed by a cloud provider (GCP, Azure, AWS, etc), I suggest using their MySQL product if possible as running HA MySQL in Kubernetes can be difficult. To make this tutorial cloud provider agnostic however, we will use a non-replicated instance of MySQL.
-To install MySQL from Helm, run the following command. Note that there are some options that are specified. These options basically just enumerate that:
+To install MySQL from Helm, run the following command. Note that there are some options that need to be defined:
- There should be a `fleet` database created
- The default user's username should be `fleet`
+Helm v2
```sh
helm install \
--name fleet-database \
--set mysqlUser=fleet,mysqlDatabase=fleet \
- stable/mysql
+ oci://registry-1.docker.io/bitnamicharts/mysql
+```
+
+Helm v3
+```sh
+helm install fleet-database \
+ --set mysqlUser=fleet,mysqlDatabase=fleet \
+ oci://registry-1.docker.io/bitnamicharts/mysql
```
This helm package will create a Kubernetes `Service` which exposes the MySQL server to the rest of the cluster on the following DNS address:
@@ -156,11 +166,19 @@ kubectl create -f ./docs/Using-Fleet/configuration-files/kubernetes/fleet-migrat
#### Redis
+Helm v2
```sh
helm install \
--name fleet-cache \
--set persistence.enabled=false \
- stable/redis
+ oci://registry-1.docker.io/bitnamicharts/redis
+```
+
+Helm v3
+```sh
+helm install fleet-cache \
+ --set persistence.enabled=false \
+ oci://registry-1.docker.io/bitnamicharts/redis
```
This helm package will create a Kubernetes `Service` which exposes the Redis server to the rest of the cluster on the following DNS address:
@@ -245,4 +263,4 @@ Once you have the public IP address for the load balancer, create an A record in
-
\ No newline at end of file
+
diff --git a/docs/Get started/FAQ.md b/docs/Get started/FAQ.md
index 68be6059e9..5490690f1d 100644
--- a/docs/Get started/FAQ.md
+++ b/docs/Get started/FAQ.md
@@ -2,6 +2,11 @@
## Using Fleet
+### Can you host Fleet for me?
+
+Fleet offers managed cloud hosting for large deployments. Unfortunately, while organizations of all kinds use Fleet, from Fortune 500 companies to school districts to hobbyists, we are not currently able to provide hosting for deployments smaller than 1000 hosts. If you are comfortable doing so, you can still buy a license and host Fleet yourself.
+
+
### How can I switch to Fleet from Kolide Fleet?
To migrate to Fleet from Kolide Fleet, please follow the steps outlined in the [Upgrading Fleet section](https://fleetdm.com/docs/deploying/upgrading-fleet) of the documentation.
diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md
index 3d2b1cef6e..cb302c712e 100644
--- a/docs/REST API/rest-api.md
+++ b/docs/REST API/rest-api.md
@@ -1829,7 +1829,7 @@ the `software` table.
| page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. |
| order_key | string | query | What to order results by. Can be any column in the hosts table. |
-| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. |
+| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. **Note:** Use `page` instead of `after`. |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. |
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). |
diff --git a/docs/Using Fleet/CIS-Benchmarks.md b/docs/Using Fleet/CIS-Benchmarks.md
index 500f0b9843..12ecfd59f0 100644
--- a/docs/Using Fleet/CIS-Benchmarks.md
+++ b/docs/Using Fleet/CIS-Benchmarks.md
@@ -12,6 +12,37 @@ Fleet has implemented native support for CIS Benchmarks for the following platfo
[Where possible](#limitations), each CIS Benchmark is implemented with a [policy query](./REST-API.md#policies) in Fleet.
+These benchmarks are intended to gauge your organization's security posture, rather than the current state of a given host. A host may fail a CIS Benchmark policy despite having the correct settings enabled if there is not a specific policy in place to enforce that setting. For example, this is the query for **CIS - Ensure FileVault Is Enabled (MDM Required)**:
+
+```sql
+SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.MCX' AND
+ name='dontAllowFDEDisable' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.MCX' AND
+ name='dontAllowFDEDisable' AND
+ (value != 1 AND value != 'true')
+ )
+ AND EXISTS (
+ SELECT 1 FROM disk_encryption WHERE
+ user_uuid IS NOT "" AND
+ filevault_status = 'on'
+ );
+```
+
+Two things are being evaluated in this policy:
+
+1. Is FileVault currently enabled?
+2. Is there a profile in place that prevents FileVault from being disabled?
+
+If either of these conditions fails, the host is considered to be failing the policy.
+
## Requirements
Following are the requirements to use the CIS Benchmarks in Fleet:
diff --git a/docs/Using Fleet/MDM-macOS-setup.md b/docs/Using Fleet/MDM-macOS-setup.md
index 55fe98e886..c526212cd1 100644
--- a/docs/Using Fleet/MDM-macOS-setup.md
+++ b/docs/Using Fleet/MDM-macOS-setup.md
@@ -32,24 +32,24 @@ Fleet UI:
2. Under **End user authentication**, enter your IdP credentials and select **Save**.
-> If you've already configured [single sign-on (SSO) for logging in to Fleet](https://fleetdm.com/docs/configuration/fleet-server-configuration#okta-idp-configuration), you'll need to create a separate app in your IdP so your end users can't log in to Fleet. In this separate app, use "https://fleetserver.com/api/v1/fleet/mdm/sso/callback" for the SSO URL.
+ > If you've already configured [single sign-on (SSO) for logging in to Fleet](https://fleetdm.com/docs/configuration/fleet-server-configuration#okta-idp-configuration), you'll need to create a separate app in your IdP so your end users can't log in to Fleet. In this separate app, use "https://fleetserver.com/api/v1/fleet/mdm/sso/callback" for the SSO URL.
fleetctl CLI:
1. Create `fleet-config.yaml` file or add to your existing `config` YAML file:
-```yaml
-apiVersion: v1
-kind: config
-spec:
- mdm:
- end_user_authentication:
- identity_provider_name: "Okta"
- entity_id: "https://fleetserver.com"
- issuer_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
- metadata_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: config
+ spec:
+ mdm:
+ end_user_authentication:
+ identity_provider_name: "Okta"
+ entity_id: "https://fleetserver.com"
+ issuer_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
+ metadata_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
+ ...
+ ```
2. Fill in the relevant information from your IdP under the `mdm.end_user_authentication` key.
@@ -63,7 +63,7 @@ spec:
2. Under **End user license agreement (EULA)**, select **Upload** and choose your EULA.
-> Uploading a EULA is optional. If you don't upload a EULA, the end user will skip this step and continue to the next step of the new Mac setup experience after they authenticate with your IdP.
+ > Uploading a EULA is optional. If you don't upload a EULA, the end user will skip this step and continue to the next step of the new Mac setup experience after they authenticate with your IdP.
### Step 3: enable end user authentication
@@ -85,33 +85,33 @@ fleetctl CLI:
2. Create a `workstations-canary-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: team
-spec:
- team:
- name: Workstations (canary)
- mdm:
- macos_setup:
- enable_end_user_authentication: true
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: team
+ spec:
+ team:
+ name: Workstations (canary)
+ mdm:
+ macos_setup:
+ enable_end_user_authentication: true
+ ...
+ ```
-Learn more about team configurations options [here](./configuration-files/README.md#teams).
+ Learn more about team configurations options [here](./configuration-files/README.md#teams).
-If you want to enable authentication on hosts that automatically enroll to "No team," we'll need to create an `fleet-config.yaml` file:
+ If you want to enable authentication on hosts that automatically enroll to "No team," we'll need to create an `fleet-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: config
-spec:
- mdm:
- macos_setup:
- enable_end_user_authentication: true
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: config
+ spec:
+ mdm:
+ macos_setup:
+ enable_end_user_authentication: true
+ ...
+ ```
-Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings).
+ Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings).
3. Add an `mdm.macos_setup.enable_end_user_authentication` key to your YAML document. This key accepts a boolean value.
@@ -119,9 +119,9 @@ Learn more about "No team" configuration options [here](./configuration-files/RE
5. Confirm that end user authentication is enabled by running the `fleetctl get teams --name=Workstations --yaml` command.
-If you enabled authentication on "No team," run `fleetctl get config`.
+ If you enabled authentication on "No team," run `fleetctl get config`.
-You should see a `true` value for `mdm.macos_setup.enable_end_user_authentication`.
+ You should see a `true` value for `mdm.macos_setup.enable_end_user_authentication`.
## Bootstrap package
@@ -156,20 +156,20 @@ Apple requires that your package is a distribution package. Verify that the pack
1. Run the following commands to expand you package and look at the files in the expanded folder:
-```bash
-$ pkgutil --expand package.pkg expanded-package
-$ ls expanded-package
-```
+ ```bash
+ $ pkgutil --expand package.pkg expanded-package
+ $ ls expanded-package
+ ```
-If your package is a distribution package should see a `Distribution` file.
+ If your package is a distribution package should see a `Distribution` file.
2. If you don't see a `Distribution` file, run the following command to convert your package into a distribution package.
-```bash
-$ productbuild --package package.pkg distrbution-package.pkg
-```
+ ```bash
+ $ productbuild --package package.pkg distrbution-package.pkg
+ ```
-Make sure your package is a `.pkg` file.
+ Make sure your package is a `.pkg` file.
### Step 2: sign the package
@@ -178,25 +178,25 @@ To sign the package we need a valid Developer ID Installer certificate:
1. Login to your [Apple Developer account](https://developer.apple.com/account).
2. Follow Apple's instructions to create a Developer ID Installer certificate [here](https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates).
-> During step 3 in Apple's instructions, make sure you choose "Developer ID Installer." You'll need this kind of certificate to sign the package.
+ > During step 3 in Apple's instructions, make sure you choose "Developer ID Installer." You'll need this kind of certificate to sign the package.
-Confirm that certificate is installed on your Mac by opening the **Keychain Access** application. You should see your certificate in the **Certificates** tab.
+ Confirm that certificate is installed on your Mac by opening the **Keychain Access** application. You should see your certificate in the **Certificates** tab.
3. Run the following command in the **Terminal** application to sign your package with your Developer ID certificate:
-```bash
-$ productsign --sign "Developer ID Installer: Your name (Serial number)" /path/to/package.pkg /path/to/signed-package.pkg
-```
+ ```bash
+ $ productsign --sign "Developer ID Installer: Your name (Serial number)" /path/to/package.pkg /path/to/signed-package.pkg
+ ```
-You might be prompted to enter the password for your local account.
+ You might be prompted to enter the password for your local account.
-Confirm that your package is signed by running the following command:
+ Confirm that your package is signed by running the following command:
-```bash
-$ pkgutil --check-signature /path/to/signed-package.pkg
-```
+ ```bash
+ $ pkgutil --check-signature /path/to/signed-package.pkg
+ ```
-In the output you should see that package has a "signed" status.
+ In the output you should see that package has a "signed" status.
### Step 3: upload the package to Fleet
@@ -212,42 +212,42 @@ fleetctl CLI:
1. Upload the package to a storage location (ex. S3 or GitHub). During step 4, Fleet will retrieve the package from this storage location and host it for deployment.
-> The URL must be accessible by the computer that uploads the package to Fleet.
-> * This could be your local computer or the computer that runs your CI/CD workflow.
+ > The URL must be accessible by the computer that uploads the package to Fleet.
+ > * This could be your local computer or the computer that runs your CI/CD workflow.
2. Choose which team you want to add the bootstrap package to.
-In this example, we'll add a bootstrap package to the "Workstations (canary)" team so that the package only gets installed on hosts that automatically enroll to this team.
+ In this example, we'll add a bootstrap package to the "Workstations (canary)" team so that the package only gets installed on hosts that automatically enroll to this team.
3. Create a `workstations-canary-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: team
-spec:
- team:
- name: Workstations (canary)
- mdm:
- macos_setup:
- bootstrap_package: https://github.com/organinzation/repository/bootstrap-package.pkg
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: team
+ spec:
+ team:
+ name: Workstations (canary)
+ mdm:
+ macos_setup:
+ bootstrap_package: https://github.com/organinzation/repository/bootstrap-package.pkg
+ ...
+ ```
-Learn more about team configurations options [here](./configuration-files/README.md#teams).
+ Learn more about team configurations options [here](./configuration-files/README.md#teams).
-If you want to install the package on hosts that automatically enroll to "No team," we'll need to create an `fleet-config.yaml` file:
+ If you want to install the package on hosts that automatically enroll to "No team," we'll need to create an `fleet-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: config
-spec:
- mdm:
- macos_setup:
- bootstrap_package: https://github.com/organinzation/repository/bootstrap-package.pkg
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: config
+ spec:
+ mdm:
+ macos_setup:
+ bootstrap_package: https://github.com/organinzation/repository/bootstrap-package.pkg
+ ...
+ ```
-Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings).
+ Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings).
3. Add an `mdm.macos_setup.bootstrap_package` key to your YAML document. This key accepts the URL for the storage location of the bootstrap package.
@@ -255,9 +255,9 @@ Learn more about "No team" configuration options [here](./configuration-files/RE
5. Confirm that your bootstrap package was uploaded to Fleet by running the `fleetctl get teams --name=Workstations --yaml` command.
-If you uploaded the package to "No team," run `fleetctl get config`.
+ If you uploaded the package to "No team," run `fleetctl get config`.
-You should see the URL for your bootstrap package as the value for `mdm.macos_setup.bootstrap_package`.
+ You should see the URL for your bootstrap package as the value for `mdm.macos_setup.bootstrap_package`.
## macOS Setup Assistant
@@ -273,7 +273,7 @@ To customize the macOS Setup Assistant, we will do the following steps:
### Step 1: create an automatic enrollment profile
-1. Download Fleet's example automatic enrollment profile by navigating to the example [here on GitHub](https://github.com/fleetdm/fleet/blob/main/mdm_profiles/setup_assistant.json) and clicking the download icon.
+1. Download Fleet's example automatic enrollment profile by navigating to the example [here on GitHub](https://github.com/fleetdm/fleet/blob/main/mdm_profiles/automatic_enrollment.json) and clicking the download icon.
2. Open the automatic enrollment profile and replace the `profile_name` key with your organization's name.
@@ -281,45 +281,45 @@ To customize the macOS Setup Assistant, we will do the following steps:
4. In your automatic enrollment profile, edit the `skip_setup_items` array so that it includes the panes you want to hide.
-> You can modify properties other than `skip_setup_items`. These are documented by Apple [here](https://developer.apple.com/documentation/devicemanagement/profile).
+ > You can modify properties other than `skip_setup_items`. These are documented by Apple [here](https://developer.apple.com/documentation/devicemanagement/profile).
### Step 2: upload the profile to Fleet
1. Choose which team you want to add the automatic enrollment profile to.
-In this example, let's assume you have a "Workstations" team as your [default team](./MDM-setup.md#step-6-optional-set-the-default-team-for-hosts-enrolled-via-abm) in Fleet and you want to test your profile before it's used in production.
+ In this example, let's assume you have a "Workstations" team as your [default team](./MDM-setup.md#step-6-optional-set-the-default-team-for-hosts-enrolled-via-abm) in Fleet and you want to test your profile before it's used in production.
-To do this, we'll create a new "Workstations (canary)" team and add the automatic enrollment profile to it. Only hosts that automatically enroll to this team will see the custom macOS Setup Assistant.
+ To do this, we'll create a new "Workstations (canary)" team and add the automatic enrollment profile to it. Only hosts that automatically enroll to this team will see the custom macOS Setup Assistant.
2. Create a `workstations-canary-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: team
-spec:
- team:
- name: Workstations (canary)
- mdm:
- macos_setup:
- macos_setup_assistant: ./path/to/automatic_enrollment_profile.json
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: team
+ spec:
+ team:
+ name: Workstations (canary)
+ mdm:
+ macos_setup:
+ macos_setup_assistant: ./path/to/automatic_enrollment_profile.json
+ ...
+ ```
-Learn more about team configurations options [here](./configuration-files/README.md#teams).
+ Learn more about team configurations options [here](./configuration-files/README.md#teams).
-If you want to customize the macOS Setup Assistant for hosts that automatically enroll to "No team," we'll need to create a `fleet-config.yaml` file:
+ If you want to customize the macOS Setup Assistant for hosts that automatically enroll to "No team," we'll need to create a `fleet-config.yaml` file:
-```yaml
-apiVersion: v1
-kind: config
-spec:
- mdm:
- macos_setup:
- macos_setup_assistant: ./path/to/automatic_enrollment_profile.json
- ...
-```
+ ```yaml
+ apiVersion: v1
+ kind: config
+ spec:
+ mdm:
+ macos_setup:
+ macos_setup_assistant: ./path/to/automatic_enrollment_profile.json
+ ...
+ ```
-Learn more about configuration options for hosts that aren't assigned to a team [here](./configuration-files/README.md#organization-settings).
+ Learn more about configuration options for hosts that aren't assigned to a team [here](./configuration-files/README.md#organization-settings).
3. Add an `mdm.macos_setup.macos_setup_assistant` key to your YAML document. This key accepts a path to your automatic enrollment profile.
@@ -333,7 +333,7 @@ Testing requires a test Mac that is present in your Apple Business Manager (ABM)
2. In Fleet, navigate to the Hosts page and find your Mac. Make sure that the host's **MDM status** is set to "Pending."
-> New Macs purchased through Apple Business Manager appear in Fleet with MDM status set to "Pending." Learn more about these hosts [here](./MDM-setup.md#pending-hosts).
+ > New Macs purchased through Apple Business Manager appear in Fleet with MDM status set to "Pending." Learn more about these hosts [here](./MDM-setup.md#pending-hosts).
3. Transfer this host to the "Workstations (canary)" team by selecting the checkbox to the left of the host and selecting **Transfer** at the top of the table. In the modal, choose the Workstations (canary) team and select **Transfer**.
diff --git a/docs/Using Fleet/Usage-statistics.md b/docs/Using Fleet/Usage-statistics.md
index 197318904e..30a0e3f492 100644
--- a/docs/Using Fleet/Usage-statistics.md
+++ b/docs/Using Fleet/Usage-statistics.md
@@ -126,6 +126,8 @@ To disable usage statistics:
3. Uncheck the "Enable usage statistics" checkbox and then select "Update settings."
+Usage statistics can also be disabled via [configuration files](https://fleetdm.com/docs/configuration/configuration-files#server-settings-enable-analytics).
+
-
\ No newline at end of file
+
diff --git a/docs/Using Fleet/enroll-chromebooks.md b/docs/Using Fleet/enroll-chromebooks.md
index aa33a3993f..c00ec23aaa 100644
--- a/docs/Using Fleet/enroll-chromebooks.md
+++ b/docs/Using Fleet/enroll-chromebooks.md
@@ -23,7 +23,6 @@ By default, the hostname for a Chromebook host will be blank. The hostname can b
## Debugging ChromeOS
To learn how to debug the Fleetd Chrome extension, visit [here](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Testing-and-local-development.md#fleetd-chrome-extension).
-
diff --git a/docs/Using Fleet/enroll-hosts.md b/docs/Using Fleet/enroll-hosts.md
index 6d012a9545..8b2127cf18 100644
--- a/docs/Using Fleet/enroll-hosts.md
+++ b/docs/Using Fleet/enroll-hosts.md
@@ -314,9 +314,27 @@ expiration setting. To configure this setting, in the Fleet UI, head to **Settin
> The fleetd Chrome browser extension is supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). It is not intended for non-ChromeOS hosts with the Chrome browser installed.
+### Overview
+Google Admin uses organizational units (OUs) to organize devices and users.
+
+One limitation in Google Admin is that extensions can only be configured at the user level, meaning that a user with a MacBook running Chrome, for example, will also get the fleetd Chrome extension.
+
+When deployed on OSs other than ChromeOS, the fleetd Chrome extension will not perform any operation and will not appear in the Chrome toolbar.
+However, it will appear in the "Manage Extensions" page of Chrome.
+Fleet admins who are comfortable with this situation can skip step 2 below.
+
+To install the fleetd Chrome extension on Google Admin, there are two steps:
+1. Create an OU for all users who have Chromebooks and force-install the fleetd Chrome extension for those users
+2. Create an OU for all non-Chromebook devices and block the fleetd Chrome extension on this OU
+
+> More complex setups may be necessary, depending on the organization's needs, but the basic principle remains the same.
+
+### Step 1: OU for Chromebook users
+Create an [organizational unit](https://support.google.com/a/answer/182537?hl=en) where the extension should be installed. [Add all the relevant users](https://support.google.com/a/answer/182449?hl=en) to this OU.
+
Visit the Google Admin console. In the navigation menu, visit Devices > Chrome > Apps & Extensions > Users & browsers.
-Select the relevant organizational unit, users, or group where you want the fleetd Chrome extension to be installed.
+Select the relevant OU where you want the fleetd Chrome extension to be installed.
> Currently, the Chrome extension can only be installed across the entire organization. The work to enable installation for sub-groups is tracked in https://github.com/fleetdm/fleet/issues/13353.
@@ -330,6 +348,21 @@ Under "Installation Policy", select "Force install". Under "Update URL", select
> For the fleetd Chrome extension to have full access to Chrome data, it must be force-installed by enterprise policy as per above
+### Step 2: OU to block non-Chromebook devices
+Create an [organizational unit](https://support.google.com/a/answer/182537?hl=en) to house devices where the extension should not be installed. [Add all the relevant devices](https://support.google.com/chrome/a/answer/2978876?hl=en) to this OU.
+
+In the Google Admin console, in the navigation menu, visit Devices > Chrome > Managed Browsers.
+
+Select the relevant OU where you want the fleetd Chrome extension to be blocked.
+
+In the bottom right, click the yellow "+" button and select "Add Chrome app or extension by ID."
+
+Visit your Fleet instance and select Hosts > Add Hosts and select ChromeOS in the popup modal.
+
+Enter the "Extension ID" and "Installation URL" using the data provided in the modal.
+
+Under "Installation Policy", select "Block".
+
## Grant full disk access to osquery on macOS
macOS does not allow applications to access all system files by default. If you are using MDM, which
is required to deploy these profiles, you
diff --git a/docs/Using Fleet/fleetctl-CLI.md b/docs/Using Fleet/fleetctl-CLI.md
index 474218d6a3..bf173307dd 100644
--- a/docs/Using Fleet/fleetctl-CLI.md
+++ b/docs/Using Fleet/fleetctl-CLI.md
@@ -202,7 +202,7 @@ An API-only user does not have access to the Fleet UI. Instead, it's only purpos
To create your new API-only user, run `fleetctl user create` and pass values for `--name`, `--email`, and `--password`, and include the `--api-only` flag:
```sh
-fleetctl user create --name "API User" --email api@example.com --password temp!pass --api-only
+fleetctl user create --name "API User" --email api@example.com --password temp@pass123 --api-only
```
### Creating an API-only user
diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md
index dc5a65260f..5c6f8c710e 100644
--- a/handbook/business-operations/README.md
+++ b/handbook/business-operations/README.md
@@ -217,6 +217,7 @@ The following table lists this department's rituals, frequency, and Directly Res
| Vanta check | Monthly | Look for any new actions in Vanta due in the upcoming months and create issues to ensure they're done on time. | Nathan Holliday |
| Investor reporting | Quarterly | Provide updated metrics for CRV in Chronograph. | Nathanael Holliday |
| Applicant forwarding | Daily | Whenever an application notification arrives in the BizOps slack channel, forward this notification to the hiring channel for that position. | Joanne Stableford |
+| KPI roundup + weekly update | Weekly | Update KPI spreadsheet with BizOps KPI data by 5pm US central time every Friday. At 5pm check other department KPIs to make sure they have been updated, and if not, notify DRIs and the apprentice to the CEO which KPIs have not been updated. | Nathanael Holliday |
Fdr$VXr1_6-5cwXzh
zd#FfmAie6J^FNJcT;l*(->Pz|gC(ifw`wcqz4Ojnzq7?y=qGvQK4vX__#cxG_cry0q
z%7iC&FULq>?r$#bni#29Jg1lr&&~kXZO?dk0v1XAk@2E=VaJ3(Y}+{64xa3Qnr>|L
z=IH$k8Tl5f%b)>a$)f+EG>)2Ht-I9XIV@w0RPPtpf@W9qE?>6EU0bHr&nnm*#0ru)
zh%Z%E-?ySVUx;iRV`SQA9vE8+TSE*eY3Hrq0V*6~v4=L~=@kIrSn$tJ{-}(2K_d
z&esT5)${KWMh__YtrjwT(yw8fBMFs=tI=YY$8*qGv0_jB7L`+wstrOyG-nb=9hnt)
z=dML284fSWRUw%_l{4qlXumPxhbv@k*%B4SoeTBDkPY|`{1K9EaoK8J%1vv5ln@%k
zb-!!8tB0~|3J7ohRT;|DYUEmrxcl5kSLF96_I%!Xo-2Hcp+4d7-Gv}WhnDVQgbjVU
zjbpNBTKz4|)rj>D!hsBws{)$u9z4WnnE(Vnh;l?_%g@Skd|#{DPW)){t10{+ke4T8
z^Ug~jCcwb6m}QC?DG@T(bb$kYBYg2|%I_=l%fExlK;q?IA`FhUE4FGP&4Kgr;7}&K
z53MWXhbcr;O;l2D7`jx&8?bq39@eBvui#-WzuX2_Io0o+P&0|$+P7TmZo`S{0cn)k
zhXD#)V`aHEn2N*$tL2i3Q#;TFT7|S@CZu=u_usm}1mO>wSGw(at_IE+cr|Z8FOv{A
zhYR8c>W=T&U4%sP8cJds9Va(-0dnq>;!H0|u+e;X&oZTpvjCR>tKM9lATh}Q-49RD
zE-1OpM!dbT=#NvT_AGNc3YS$0*lENXT-pStkPXxde4ka_h`nrQir+|$LBca8ZY}pm
zaKJ?mG_{Hn;SXaQTipmU?~7=Fbq`AjuLSt(`qNGQA(~NYVFq9G#z#kNg+H
z$HcS~4??8dv8pFSvDX0qvVHKX$WNA@YouR^hwSJ=Gkh#}t7
zP_?BnB#T}g`sDM0uK|cHyY_jk
z*5mMM;gGwT`yQ->dzkR=$Ndtaml&!|vSVpc^|NlK_b=6%-xkU0k~y^7FQlg%uT8Qj%-6gv
zpEyLUe`;mW20)68khh~aZr?u;i?5MoTf=;eRFu;~|22w-lf4$3vyO=A8s1S|Vp33lMMY#io3X1UbAKgJ1fqt_zz9
zX>r;Ll~_mGR4t}+z%wMF3u&B(2Hq@^RdF+Ir6Id%L`}ab!l$A?i18>R{ge-ue$*xVz)8
zfUaMT2!vYITUSBEKzj29YY$MV%MniP;?)fG!b$-UT^bvwe<9a01iX}4h<@|3=T?xZ~NT=zK!3J%9A+XvG8C^H+A=3LgE(@GEYOe
z-~QsIyY@MGT7>}x{kJ}r*PnOQh>a>u#=nD>+hv$cHFI8Om80^<#w8T8wZ(3&sZZ34
zHfVD|*kJZWji1TBw2_b?xoQ!MZ(G&_JLgY*5Y|ihpAoK~VKIp(vG=&wNJ&w*yc3wVuc_P)$e(I0Q%nj14&4uM
zN*sjU?IB0>8Z`ZK9!hr~&>QRdv#Em~*E(L(I}j-b-|DWppMyA8AS*@F7mrmuFgm`Q
zkIsCDwo5Xe-v;(7#MgwqYBLoG1tUg-U>2Pds~Zxwu=58{U7p@zsNRmJzRS~z1x4Pw
z+*yDs(@yNcSOJ%7%VfY-*!4x#HZ(jHY+Z#L)`sR(o(L;px}v;#MYpd9WDt(lH3Jal
zz)`Uh4nPGTDKa(Bo~h5t__Mn4O(D7-w|3MZ5n~4s+k^9>>(0O7+aEZLMFDcHMsL)$
z-VqVnRLzUNrM&FyhoO7(cp9vf3j9S~`zo@WHqovZnWfOKD-p052!40!RjE{!u1w)c-?_B**-XTci%M@33Rd#K?_7@zDd1ke08Z
z-Yo@eG&vs0`%oqY7Iu_Eb=Zr`PC8=U1CJp-toyHo3V*qEIYmO1Boqh*!8|GRO#5c6
zXJ4VvVzYZJ)l`v3!OQ#V2#3MpdapAPso;FoD>%8p-K$z%#4jr-!6@?anY=OQkrqVX
zzw9dxX!+xDWKMd%MYNc5pCVi*oXmmy1iRsjtyj
zVQrupYA7`>9WMtjz27UPjt*CCCy5NaQ-hDC%lYP?7Ek^Xqzd~3{rRO3mXm>HurLlA
zx}7#ZWD=^yRR^_IVw@Ys7r=P3w8tLkC%Jp_UbmvlMf{>}dpvl$H}6QyBR^alI-w%k
zFQhJ|oExB)qC)j9ah4Qof;}cQ5E`SeCCd?C4u$cERt)@Y9~td{_C1@x0nguKl?adlhTV
zT8{HCs52;b#kDyB@=4nzS>{Ik5mb6bu(>e+HFz+Sk!N3Y_MxGGRbW(;zRZ1sbFoHA
zL=0DG%A}i*iu=Y+n4X%5)1RSfemA$f>0tk7`$$(qc=#hhIZUiO#>c*d*AjDcxu@5e
z$l$=;;QhJ3SrTiV3&uE5a+8POkbWC3fq9U}iAc`i;dB3EO8
zSLvZseIKjd;CeA#?YnpofkEQ4+TV&O5Pb1P-G-vHy6@a$`aLoi@zv
ze<4{m&oq>>dS)P@zzt!+^?38-!<)2S5WkJ)YKwv!
zR=|<|e#(^1Ipa~q$(Ve$ni*WK-~|}sM?u8&b-^W
z9Jcdnz0h2^AR-?0K;`Z^NoRaK7hWy-Ngo6VthK<`z1|x#fyJKlifIB)nfv$#5S>YZ
zJgh=ixntsdQ|==P_`)|6KvL@fyK@4Tu(0Q
zGVrBX6;UCqv
zY*P39{YN)5suZIsW2wb5=k#t0tQmLkV-dGUZh4s1om(mlhIO9ZVKm~y3(gz1k;UKm
zT|4vPMy>KC`Nbpqe(>XBYHVFI40Y?K~--d|pQrReM3AEdLt+9RmpAX&Z~0qj7q
zXY|O_)y_&;JCAhD_^ji!Cpo(WlZ?S66^}Y|72v~cmfYN0LIcbL($VY({2=Eez^Pxf
zKClCB#QGI3w~HSb>x_mECS1|+6
z{R4-WHvk
znYmvyBn$%T+d&}X|Lea9d-ZM$dQItK72$4B6FE`caZ{kVN=MU;O
zu?Bj)QabdiGXd~=1VQ+k4)>=%n9lc0gospbT-VDhC-v2ip)_Up`8%pzVN3BBGG?!|
z8jYZJ#2pQfaAeU3InT9L!2LzM?hxN<(F9abw{mtVtm<#o(^XJ(7keut8`3H=!7($K
z5PHSD%#WU930)Ba%qV~81${Va%U~aHkvMKq-qfP4adniI7G1-G@aVF8$I2H48U8D~
z<3a&R=;G^-eE6SWLR65@5>pHd+^a`B@asb4B6`4WYmuNGizCms
zpBXR@1%QaV`(kG`3i}P*Y7N~DjgZ6vI0(wX;`0#{fP!Fu_GzyK9n54*1EcM9Y0%6=
z8QkCyiPUpiird0~GQZs5u3AAO^eg~6+&jH(Uc{IdM_7>rD?&z{PoFww`5kwGYP)li
zI%xQ?ekZ?+9Coz!0~o}@th(`QIMJmJ>Y939GL?t0%aM08B;cr*am9R3Vw~kz`Tj=^
zIo0YirHFT{uc`w>k>`#oD~I-(;tg0suHT-&GKKYNtx_$*KR{s;xEX8zBTlnEL>6Hi
zx8~VLPF^;g8!~(cu<3~`S=q)OXcQxV2<>3O=6;!li;Vu`1>|yN<5BgT9mPwZyv6~`
zjJ0|dyqb!F<>C&uR3&vqdaDbEj7Mz90px>T;oimtxXrCA-=1FOi^Ej1epSr|0e{@f
z_f9$JgHfzsJP(@>151$&F0SOzrCF5z$c?>^p@}$ijX73K3Be_!AgJPjial;{Fd+4~cQICz5$TG`25W)rwV*ck-w5lTAq`N848
zLF`*94oKN2XIa{nJ)6%2R>&|(t`R+NG?j(YcQFa~!z2IXfjw=9W@ki0yboKdV#sZPeI(9wsd`RU6L9j&al#w|C_|8$+4lLcb-jkw(
zLJV4LALC7P&{+@bZog2Uls;7E+W?BMA>zEtn^pFTr`=T>n;yJ*Xp9^t%aTNwLeB>V
z+S=#d)?Q!7fqGzfsUx7=0|N$8p!FxZ5ybCodEm{|s{AId?WagR?CKR72#h{hKYNsw
z6_+JHNq7g)TB_B!BQ*zaJe(&Z69nzP)}-=oQR+md(RhRbmFs_WF{CbD_3K-`G!BmT
z4lJVt+2!Q9@MRnsN9p%Bx)e3G;@@*xYrO!*MG59m%Fb4p>-bQO!3oFUIWbtW_sSaY
z0sm2}i7S+oE(q01(Y+oZ5t3+!*p{1FD7QA6eOIOinwSUa-mfSnsHwQ}=pFLR$;Z
z`g|m$7NpKeKQdm6`9sffXcf=L_IUkV)-u|9G?e!ia!t9XQ`%QE&2Taz$ExC9h
zewk(UaoZ=gvjjpVJ^%1zTiM5?v*_Ik&5mx1H*8?q8c&|Ct?>T*e7Rr-VNBN3?tQ)s
znWc-JJFNC=AJP)^`b+N7s-HRxclZzhUWF{@+jx_kjA;o$KbJ}WtOBje8;2z>)c#i-UcP77b3Td~7`>1mQgenRdod}g8L&$L@W~a1Nq8M--LK<)JI0kPIeNi*vF0jll`~IDA@!AonNr
z8q$%otFk*x7Yg(6!Dh+Bj+?q8Y{JkF+rfxao1G+OYmF7B)0#8aO^ItPn)*WEg6MKPCZ>FP%b
z60>4@-vv23!_7K}#b~UB>z=3g`JPQ?;MUkSRatb`FqMteqhbe`}wJ9$TMb$UdMG=O0wEB|koR*|#&sl>jYfZN2ss#ApFU5v#{k
zTcIDC_@pZMuA`m4%)6|l8UPTz-=7P7nSpACKc8T4n`G{1+3boo|GN26^rLjtSH5mA
zbM~Rx_I=Nnh;C$ELkQbeqsH@8OvROTBc&3`(wil6I8F%H
z7N%4fhA1$7(R5Rl$~M}5GDBRqni%fu{;-dfBrx2&FEBggcQ7n%l1|
z@!DyBj1vuKk}vMv%x;XmLPbS+{6z4n%zL~QFZp>>C(alS?^vZ*NcFH+fs{lNlz?r<
z+0^$7Hg-?kv+ee_JQdv&ghS4CFQ@;qGp<-a7-Z+j;h~9X&6-Q6F)88gtg!BZGZgCp
zBJH9vup70k8g=;6?w73tIy9YH_yLh+7p
zbVY@C;d96_BAqqQ#*IrUxk%b+pQazcJfnx3V({U|BawnL^zyKBCN{sv1b->jGwvGR*81aScc0M}5AxtD=D|ThTS&2$8Kr*F)xphE
zVqlkGrXLXbTgx(BF9K!Z*!)V_%K=+LCUni?rm@z-()Z-ZtNo#x?O*8({DzH*CDIn1
z_B!|5F^s*~q?quTA2k7%3$FdAmQw5?PCirbeVc|JXq}y^*nE>KFC&*my@~xqDM$`k
zdiAGIbQx9;M)c(Lg9NL54ZFD6QkOGy1XJz@A1Ts?T#z387WvEd
zf`^Yn$s3$JI{79IG0(aB!|;70;uEr}Uxv2wrI>X4dG%r3$#|xlb`RshXco+M5thLl
z;`o_nI|7Ztfqb+4iub{#+*L9%8{Mz^DbAWxx+0>9ZsjbHF*MWFJKflnu&yUR7f1Y%
zEX&z_IG5ZmeG#>i+uGzz;W?5!m=DzI#DI
zx#wwap(4md>HKKEV7YOdS(OsJZ!J$kLVhM1`CD7&6c>4tcye~Xadnkmqkt?Nb
zb{U2lbJ)thKeC<@Wzj*x!Hay_JnYx}hle}@>gNV&X{IV4W1d@ed^UV)YH(_zTeY}3_A-5II!Wa*!qa{{^ni!swVMivfv!kOJ01(Vlca~XI&kGX
zqVL#q5&VScuyZW5w)OqM8X
zk!(fwkQq{Gu}iWJi6O=yyBMYHjD5&5$}$-1n8A#h`8=o3_xn8e?|wb^pZ8x`UYeQf
zIO06$2!p?bA3Ae3&)1E$u@w$h?%eS8r|NdHPk2W~3w3qYicEeK$
zv~iZ^Ht+f?gEP^-jxp&+0#o(4SIp=%64|m6h^ZET&=E*9Z5#CkabW$K*01PO#viV9
zTrRseGUY$R*aSn6h98VAGMeVxmmKl?x~q%~x3kDJC>P_~RkUWD!cU!Ep|wxNnNFO`
zcTrtML???@b0+$=%Budpq2?|;89`d~AT)Xkx|nXT)XssLm-HXOHp1~KXR+&{vd
zW*ERwm3*JyJO~W`XJNF`qy3^@4|Lcv&PfoFrYI-J>H;6RLGa!eu{7=7Ni2-N^;;84$yK-%2U
zHBeCoRp6b2lRkddqZCN*nJwqT%K~$JEpdQ3`ko=H?f0pQV;r9uM24>9s=7ntDE%
z3TRGmY%w8=LHTvUDs3jL}g3-u0gcLXkFQY8_uzs
zIR^;UwCdDuyXzb3GB}DVAjL4dMCU|ynz6diV;IQq2f*3pNH91$cfW=L!2#c*0DYGG
z_>Y>8iD5AMFUg_ZUYd{!61~p+jN*O{7I%_Gz2xj}s`L3&xK6}dc;}2lBM>99MVdd%
zWNc)xHJ0z(1nj1dgmIVgo6WwLvQQ{+7?7=yglvR8xHFD9c)J^8vG890WDMk%HW9g@
zl9w9m8{FPHpjP#Rek{$3D~U=bY~*b)ac%n(O_FTbUx7Ntrp-9?xD8BmChb+dbcLOD
z`xHX6pxSl&sC`^48U-W-i(j}*xT5{o!E8&Ep1RaCi3uLaf^>S-5?rDd|Ho+YPcQL5
z@CA$ef5R_kt1Z}}w~fC2_Dtz38Kq8KEKvkD9
zL2=b+BGGbjMS|mhOa6*#ZUIt^?fPa>Yz#uuN6B^Gx7w`+(coEBUD)Hh(Mnn41ho)9
zuD@QY0+H(~Q)tA`jJ+5VI1`i)N73eH+xA<0SwirN
z{_^-#AD}s&Igw7p8Av~$qJEi;dtj2msJ>t3D=?fM9V%VhS+9uR9onDVyIQ2WoUun!
zRFwU`KB<4=`sUKJzo9coEya~HwL=T#S)2<=%wKL*YPOeZN|U$)7XTM-vv9?DKFu=v
z)`M3&xzJ?k0LM!W{dlP@fC10mE(dw$77OzsR>{3#Fp7`@iZ=K;BrtBvnXzBe(XkI)
zi6&;>VjXM(o}Kfk5t%*Nw(=aj1;KL8y4zwBLyGi;K7_y4@WtD&j6
zpkEUScop36%a7f)J5Ub@EJ4srYcq)vOn1yx%gCNC*SAI)jw2%s`|k<#$vhmU0Jn6VIg
zyAbvN;&=Rua&T(Q>qgS8!WFN=sEXo9%t6hEIwYxeB6AaiRH>|H3N#nq8dry2{N-<`cpFSU
zLXg)YW@RNW2jgup94|VfxH#wO$R=xjq{#f$LSB({j!
z3t|I>#x&?lo_yS_jbqUETZN8Bj`{8(VYHH6Jlz*qUVdZwlUI0TGB)m>b6cfsa269M
zp`^9q@Q7Gbx4m=b&e2IdDjp$8_2-S1rTF!Lh5qeEFp7Hr;Qa3vEL)75bXmPR;WBq2J3!f@@%a}8xg@bqrQV&2_+XY_7N_f%H{pHT6~
z{54azQnSYu`~#1=ilapTK2ZbGW$Ckrl9
zC>$Iaz*xJ-^X$qPanAotkTXeA^e6Uos;1~YNDEw&H2m7+w>oKqolySyH)#9NEPvg{
z!%KWwH>7vEM?x~J;{3PtmzEp0cJ=CB!GjEENHdRUQ(Nneb@FmrPvCalW?6+(des|$
znyBK(5h+C;%_*M+yA>iK~_UNLu3Hd7qETG0Q8oTX4;I#e_+W&z1YS
zTDsS}V?$3Bxf}2j1|FLydWL%W%c&;g1Sprz_Uld5hDtPpcG$tABcA3AknjDL?Eh
z7n0NLLCi~33}!ga2mDaT3)3c2{fN#$37nBDe2elPEylkrk+
z1la^%>+@HF@`O`&1iR)e*$bEopO|vLJ(|q&TYD2f2f9mFRyK~%TXvmN|VNGUq
zeyDl}cO0e|b8e>ftTB6a;n#^EnNhW(IwrF1Wre1mxwNkzi`Y{8IVoujdFpg+$b8p?
zMKi@tPv%aKUuHcbmkGEO1gph5(V9Z-@oCVA3&vlwx@PMGodAK=EV*yEqCM!aOfayx
zDYTZW*zc^Zw~_@~J|Pb!ryVGH&dZ&B^6GolK8by;ozxuJFn`-O{rx@cI1<^p_LUsQ
zEKrJzDMI-MY-@`okxNV4N~_l9l9v}Hd%^VP?{H+a6cIs`+I&6JY16%>-ZA&X6!
z54Xh2vd4B$o`=ugf?r4c|Dws=|xb=K?R4gj%@Ho=WV|q=Bq}lq1(4bn~SaP
zn@Qdb!EN(iI$J%nhfh2CwN5ONL?1G-E9vxfT-6d%y*5WfotUu6K4M~>|M%qhE_n=6
zbarKjk=&`Em5G1BmijzgP!8Yly8Woy0mTYY0;yhXYX$QB;xjIPYRwe7k?L&bs0d*s~-pv_<
zjB;81hDX%W%Xx>zjS!W^Cx;nJP6ZW!xe-KHd7rY&U6-1n%F@I=TtxjK{K>{@p34om
z&&pmFGG{D9NO|CNLKtlFJq|QcJl05FA&ChZgOiM>pt|2Nq741_wkBg^a1-Mk%7+%$
zK4~`k^Ue&vQ3zm0Rau0(bvxg5$I|9uVQ3lHcQJuB=LMT{_TopPY+0>`LLU^^`TDfN
zv0#4uCC#Nk_AwIA7Moph;>!@Du`9+#WQ|qt-{5dgASY1^!w9s&>C7{aD{ZlqePumb
z$gKARp(-*kOfkdmy@bkBu;V!brxC3m!cCsa}Otfih^H&y?5N#eKe8@bx**K2jObD!RZ@6>d$RlgZBOmYmZ+
zU+KyBjUmv3(m|$*QaTx6gm}0e^pXSS@Q0{u-+zA~vk|2Oz_~ygzi}Wk_+XN+7TcBS
zHs))8?s~xXJE2+`Xx2?A%-5c`WCyt8Au|HGGDwsXcl!wu69sV3WIXt`WF2N)Wh;+7
zK8^eAqLw#Qwad(wPPwrs9xein$RB$smVld&J$d_0APyY!J_*10(!ax!C&sF2wKKna
zQpG;qY~uf%5riLDFrNPhSg|u6dUXcn>ZDs$yc)0B3-s?=p`ZY&P)U62Kqge71$?9?
zqDk4upVES+MtF$Rs@IQ@3$W09`Fjej^U2rO7_Ro{m!
zZ)N19Ze=SFp-~ux%i_hUwcde1uOYP_ImU&}JHTKXBDeiKPEG|T(m^N_Aq(ZW(XF}z
zT1AI@-gg}3ZwOt~j&tsh!XlNkXj1|r%ha$ji_gG
zZWjd=G&_5hbjQ5?Sr?KmOQevS{$ij_DI+Y=el_*_%9t5w(?u0c4RKA$D3k2Q)+>p^
zw@I_pk=H^$gx`4f4V%cK+niw
zTpQ+g0BL>olPM%@p7Ysv;5+~Z!0ks^&?DCX=aCimE4+zHUbyzT^LesnTZTI<`k-*oPvj1D9@+Jll^>q{z;{xQft1gyNpUo5?`641Ig-0dQ6TV=Fr$b&wS){>XFpGX
zAY5v8kXbY(r+Sd;>RiCE+!VfQENP(U4=8ii1&lb-74rPv`KLewN4K!KsMw;9SJSi0
zzWQ1Z-zW!)pSIG>`9&H1eZTp`Jk|53CaN!-Ka`tbNO&@5Pk^^QCFLo)j4ReB&6su7
zzib5~OK2vjIbOjNV5*_nCmX_$nZovj$?%xhvTyA=>&CSYwSfv|NX;6HBg%vny|OaN2sDtpDaWzVzJNCwQX4n&PvX#o{`IKMoe0^B
z0e2+m(#*$a<7^!McZv$zds}i&Qby};DPOB2f*7;^I*titYKVRv$9iu>&%Nkh#Eu+%
z_bw+^z!3@9Pj-_SoH7I1j6f3pL<9t6j_3qwPVMafumq-Y3uCLAA7qu%tnqft%f~avGmVv53q_L}Xa?Ar@Ef1NZ{~Z2R5coIXNhxg7xJn&dfWyPhkk
zM>Gh#V&9w>Nhb-9t}?W0W@Etv08{eQJX0D>%3wYHG=VGvixd0-g>~cPn=xK2U^&yi
zuu(Kd%oc!szO@1vhGd1ISDcuf(%9|yE57Myf1B)!Nd3}|JRGJoeIU$S;}{L;qAd<
zOiBjDkC3rGT!sEh=k?xpN>Tx~hU*w{l;gtdFH0KD&Y;{l^?84yF28}#g7vWp4>hME
z<8We}TG$>Ic;~&v1M4v(_`*CNk2ak@ZL%AxD?cW2PoCe>^4adUK{g}5iv!8{eU)kK
zEa(}BeqpIgcZ?w$0|0pe0g|c*aAfAYvhJQss8%(FfKuwb$JOh8?bG0bpUQKzeT@^*
z@fnDuTSy`=IgP&061Z}nUfCeT@a
z0goZUB5<6>h#O8H1uhq$V3sjIuGzZfW4DZO8fDCT7`91}=zrgN0EWrKp#oi}o&An;
zud1~|Z;niZ0|k-%;?r0NRQlvCuO3q$JoT8ON@Me73=m!3co@a!nH@ksdBip786wm0
zjA-COcDLK$H2M)j$6_`)E6xY<&g}fbgMJZu$9wMq>U5UpH80j-0Sq@}pD4caBCpgm
zcsKpwDkau|y140op%ulfR~D_`$#nn1RUADc#`m+A1_Ta$Y3;2~g+YSD`4|rqM_}SX
z?M#7)^#c#UwKtlr8UstC@F6{DCdG&p2`Yjo)O6WRyJ*c+F@y9g{UJdU#
z`P$s^ESHi7s6qJPeQnzrk}MeKQc6M+<1YW5i>YSz@ZNqIu28YYEAlxlB%svKX!yny
zO~kg4HwVAK7{cH%u%Y-%l{%8*Ju_Q@nOAg5{9_+|?RxE&}
zo~?#yPcXhp816UH9)A6@@Pgjd`rta2pW7cM*LF^c
zNkA3d?`|INTBOpN%=zx)$wazU!bl&4ZS$Smm!o@mBlYFFX_A2%BM@3u^2^0g_cQ4;XPU$d6$NI`02}aFdv}07)
zsRx*gMA~-T7ocd6X()qUxF_k%Fn6*A#Zpg4@08p;!f|HrnR;O;_F12&3oB6Tq+~tn5vb(PDMLnko40s}pKDzfudE=o!?Alghgs06^g?=F7NzR=7Q{`F|Ye_)Wvc{jxalUJq~l5M^I@rU(wB=RHnY3=$F
z9Yob;{{R;FO9I3-_It2z*uUO!BsgWNWUUS+>f(|GsL1TBt_n2wiA1ZJmwcLS0F>dK
zvPx-aA6ippxrr0}l|DkCHoE_C0FxT~ApJQ3_;4I<1(2DA?R$eG2WYvgKsfr6;4g8H
z%5vLbuWm~ROz@^IGy1lGU&$5rbNYWchA}$eS5gwL)qu}Aocpik4d1Tei#s6K#CSE_
zN^b?p$nNQok5lwk@vsFjNXh5!%eV+C^m)g*z2h5kCJK4~$%OtNl$KrP%yc$GVAci@
z6>o7lt^jAE-pzXkcf)6Pn%CL4&Bpn@gv)WhY}rTZPf?Gr2F58o{}VOZXQVGQ9KHXs
z=xLHT9E*`u-$H)p7y)=TQ*Hpi`x&|UwE-{`8-uisFl1Cu!>ik(YH!q%vNWk*E?~b!
z(ux-!y~!f3al0!I4nv1GRmvA{yFb-oAG;1Dqb0L`=R*LCb8KQsDFE4AUQBV}o9GcB
zB-mSGy$B}6h+7kkJ56afB#bdP_eaxtOG{oaHE_JBS+?XlVX7&_X+Q4Vi?rc**Uc3U
zpF-v-#oOgvz$ud_Yv)+2iHC#33qY{ePz(&LLr^$kDKZ;RcL3f|{BR-~?dA0wQLM9)
zK_U)N(IqLD25^53FHC4o6PSNr33P(yzN^Hw)z>|Y_{5e`W>C3
zF5?ERhl+P7+a
zJN%06c@IhIe<9uc!XQVT_D#_6teVL{U?~;GxvZ+X8lpoe1)h*i~X;vEK&f
zoP@god6hBfckzPvzhBT+w>kdHN__{`T=>z`JC|C?#^~`wa(PpJv8b14i`iGo0&F8T
z#KZtoGi&zH54$~n|zw2`s&(AagJP5
z;_F6af_z47m^*J*fGSw?^)c4a_?2JWB#d0O&*YK_%EoX0&}dA1!OpKhlh5nz80#PI
z;(aP*yjVKZBb&yqJ7H{p^f+`9Kf7!%tf4HF^wl$H?xhvErqA!KWifSN^+CaQk3i05
zuHUBv$qJXtl+q^0Ss?NW3^nTMM~iDjG-#?w+e@F$dz0CmL0xoxSuRPGBu0PikTsE>
zN+?%KlGQu|H_cRLE~qE($m?+PJbSL+f99V
z?Ql$HC32ZeD+z8et3Gd2O*yYB_w8kC0f&{B)%fu7#v<4j6U;VT@A4mm;e={|QOwW*
z2O^=%c_C6%99qYTpS48Ygkmo}-fzBelEEMLwUsdWMG^0td#np9`LjFa>Jz^Tgb@Y{
z6jJ2p)@3#~PryX(e2oo}+?~FB8m4GVb63m`wukX$wo&k1K1~|bRU@p?O3eGmkD+K*
zaQyW@`O%TH)3t4yGGlKqfR~S6uD=-!d=BhSx;5#KZG%-Nx)BV@rQk?R_ebWih6q<;Ftk4k
zu|+$H_Fu(1rw>*%AG~l(po4#3`wZ8e+p%$=R*TYfIRLr66O3Iz$Zd?hIb%MSP|C;Q
zjN8Co{%mKv1ll6|pU2iXH5CpX&?O^N-yd;4rx6>KXgTGNCAu|YLg
zM7?0!(pXjW615`US#uoDHSoKxu=6S~Q{@@Hl&rp$h0VUw-;r>!{XSk)bD3eaUQn;%
zB7E%MiQpi{pXY%{G$$2`ajc*OYLY}v#_S>Ne>i?7Gc^J!?-G$I3kwYgQ?7Ohn7X1Ja7qT9m7VG?zA*7Jl!@uWg!}Bgv+7^q
zLgL(^RIzaP<4bw1ATkj;oD@V;IJ>kuNWlu}Uobw$8GB%l<}|MkLdSABfEBnC^C1zm
zgCNne)jedW`~AC$(&p!|YRQg#7XQazHY^+6muqovy}bTjn|a0=&g=D#xxpAx+J5LF
z2N_CgNL_pru=()`T-ztk4$v3g1_{f!FD3&3E;eNSTcysHDBh7|%q>?nvBiX*&lnn7
zIVtjc&-$1wV*pY0F!oh?zyz{c^x-mNMMRcB0K09Hu>+%mr7Am0tz&i!*k_Pt7h#&VPD%L_D;n-t;VAENe3-mIOn@a9#1PVB}!W;+I
z`R;JW!{ALDu!{5vw&RRMGp2v1EB%c?ph!FWZY!M(tN1_wO;Qb_eal9qyuE@1*=FJ$
zO-h1ok>A61j^|7ObLI{4Ln{QkBhef9?Wy5HcccPrX}aVU^~9^oe@giU%-Bd15>k+I
zW}vEYVUS$xZM@%ajDR_kIWq+AYM-KlLI5ZhXZ(97UbS3X()RhKl{i+L9^Kff%Ke!$
z0u|D;S|hgrlw{cWKguf_`Pi`Ut%Jr7ZhceN0MrYJXGqB9(-_}`kCD+N3Z`m6ZX5M?&f#+FOdNY&MzJ~p!!%q{U=#I|D+cBb$ezS8fzP7-z5GC68saHdj&)5tmYBLhI9
zq2?*RHg!6-KdU@XUg8=|gn4fOVW_|J8CTY0w(AGe?CV>v!gwjm=8a?(hhYy;ea&Q9
zr=Q%z%N6I4Vb!~RYN
z)v^4z#~#iydsKS_cxQN{0m;gp0ZJ(p4LvCk8iWI6Hm)+dL!)TVcE
zZ@w5=Y&ROdZUlQHK$VpZq?a7vbx_GU4`VS*d#eX85MD}t8(`M~?B!vU=HL6FIQU&nk&n6ZDi5{IP<)iHk&=Il&~QPe!e>}o`G
zc_w@Z&9K}v?jLrDbAUmSHAMqiJ^Ndy=awPoZikWNf&%WEdpXuPZ;8ACrFg8Oh`eJb3A}oW!O!)HAV4M%6>gn5En)Z*Bn<~
zJa+8l3-rE@Il^kA*3P!U6z&%(PTNm`9Wm_L=9RxpacIOZtBZ?LjmeXU7JxGe1
zffg1!mkTVmJfgS~Q8u{D=fiZWifj_MZzvmtyMiMwUKD$=eFcdx&969;s@Ceru9nCp
zn_V96nRI4jSRN1HK7+1VBL(J3p7u)Kj!g>SKpLPblRhQ_O%xQ|CWlHSfzInS0WBi4
zWKebKvtCc}9w^W5`$94{Yn)(&bgK@iP
zWqdUqNE*U)kxzl8NY8}5MCx<~62KO$V2}glSr*~rlMy{bLSZfrFc$p?Dv|ZE=eC5I
z>XXsu->}UuhNK}2yeZ20DPo%oY-IcAkxd8hHFmYKYsS&w6(8ChngQS~4*|bDbKW?-
z&K7$&A3F$acKRS)>`+(JdWHZTk0swW9iRBl`iEog;kR5*zb1)F!M8Uj0+=vx7tyrZ
zIDc~Kz32_*7fa``@E@tI0KP?$hD(MUKKuc5(aZ7Anhrl@zsGob`v}dvKej;pjWOy9
z4RpCH8F#6fQ{k@-;)ABw4`N4`oeV)Wk#1IS%sjif`vS{KN73Xm045+u(uq
zEEuE|QIlWI4DYSFbtf^znWk#I;58M~rUhQm!0ZSWoVW&tq5p6+u=d_U(GiAYAPMVj
z7Ea1OOnA<}RLprEnR%wV%hS(ysAW9xZ`$&VI1(%Ksp`=ZRj3*LXFGFmJ+rwK_*gc^
z^|AFf9c64J&}+pGKY-6{trw@^bRn5oDd2y}tM`7~SEQNo!UNu$uDvW?$}uU=<}Ik;
zb!;b%%VB`!I%_a;sI4B^z!n09M?{$<92hKr=wsKFDqtri<0xv86AWC}gTEywOPz+F
z@6Mdx<3gE?v@l`X$Ptv3L{_E1atJ$jVGEIej5O|Dh#q1f<6|ARR)9R|Wi_4|0R-W5
zJ_LK-0me{kzy|YUR@ZC^GHwx>P=YR}XOOg&)$rf`Yvn0TqMoQBMK8O%h+QS-M{
zG{mnLh1oK!A@Da$oLN{h%U>hrI?EP(hA&YqxRL@HuT$DZSxchau|PKk`Htm^D7xRYZU{#qK7%&@&<@;
z2E*JTiR^klb`&!INj9<$nk=qMahPAZpmEP{4B9D#Ogpjbg_OCOpnI8O1Z@hP1L?Aj
zM3oP_!nvG2ckj1mSGuy>_pF4$Wvxbju-oZctD)@wg5HX
zd=zQl(=xEFi|aM()g55hpX@^l{9zVu(O5T+6V(_ehoMi{(4G`)G6T?FUP%H^Pf;3J
zOJl2&=ar;MR@&
znthQMz|BW>BojXRMtukaWWCB0!E8=%^{xLq-@QHjoIhlj&!V97WAf35_#fd%!v%R8
z&y80@c(GhmY`hpVpLIG6nZ3nbt{aRHW(R#wcR7ui0z-w@94xSA_t(-Q>LL!+Q
zhe*A^hy;K-AcTkGBp$#W(3lg!V2fJgP)F$l(J}D*1&uj}w{9ept?Et!qt+&l$uM4E
zdo+Mw1&uu?7_n_^_n}eXDsgcA@2>*D7Y1Ee#Lux!ry1C~sctBZ27lm=;))oUL|tr1
z`4}A4i>UAu7!8j47{*J6T?|!H#z_BuDw!!P;4*hbERly-wPNF}CH?k_2|vG^X^S%x
z{5Ov4=B&;=rGOi$oK2nr}=I+}FK7LoWru=AY7IYz-`
z!(uLZz5iF)bd*=3qW5q=XY@~Jp{BgkQR3RW^Z7Xx@~6bB(2k&sP2`(OqD<>cf
z-7OIiF6z&YM3Pr6&V=ptdc416HrBW}!?_PfUJ-n5??|IayNWQ~ZW*r@)I!Y9>qN#@
zhVq_T8MU(JmhAo4vBBOyGm+|;Ek{|5cE@);mHabYWvJw+lJIOMkcN@D9N#9|cG|~y
zDAacSfL%<+%Wik!fKa}9>tH@H>Sq237I~^xQ}eTUF(_NFT;@R8v>=7sIr&B)09-UZ
zzdz2~nz2VTfz`KrIqsjulqJL8xDU`C+$U(@|KPVRuHIdB+4`jJqCwICY)=!n3j}W*
z^R;+W3x~w`0K4)~X#Cd)f)QyP=nF3dN_>_raa
zuUse?CJ!N;Z?6duWOS1+P)|`$^h@er0rVYF>!2t+%^>LC1%vr_E%A!X?wZE&_V4PG
z|4Z?*-`c-twO*{)dNM6gur`l7*A1@;C9R`9$)&={u`nIW2!y6GQD%VAS9hG9S1A?E
z=pTLKW^1Iu`#aVC-2vIIr(z^}@7{S+J?Qd&j%vMjncW=9n&pink9FsP%2Aw!yt?;}Fe1Fo!G
zAizAEQ1|k~66)a<%F}7ABSvX(F>(v>4@Wm}ot2{SJk_D%MXA#m1>n2jZxk{*)<#)9
zHh-~Ays>xZwL3ND^iZc-lAD87!xnCVkx4~wh9K>cmq-)V@H?knEKnaQtx4Pd
zpTFp~BOTxLJ-~G`mY)26FLm)zfcL_-8J-9y556y1swB_ftIs|y7|F2mOAL{@Z33PG
z=AN%T@7hn*&U(%D|;DZhc}%~rIeiP9O&?p7rG=p1ggFKp|A&9=FW)E*9_B?_;s
z4vngbaXvkaaU@XK4L9vh+NzPN6OiU>HrJ0XS93&U6p$n`wIpyUrdweCUrigFE#x1@
z(FPc_agsnp!~>_e@L)Ruwk)F${Ms=@c_xLDd+n*Xr1R^OT;5!T
zR_;vV0;PBOUGssX%nzeY-{~Rz(`d?P(tvkzb3zF)ZD0PTYw}#dlvkv6VEymcz{beg
z>XIGsOe(%=z`o@3T|Kh5wnc*rYNEX`FvO5KX9xkuJOOA`zY4$nWVo+%Hf~ZwJjx++
zf!WusF=koEaQKH~=0cWGg>>??sBBhbb;&v64591wD0QG3)s?3nG5xsFPLp^3DSlvJ
ztq=zT3MO{7R`!Qw+j2lOJ9%sSQIF)7!jGAI0;IULU)uJ&1Z@KkC2)XSX^1=id6s{w
zo$qv%$TU`@tw{pM3VBoFRzV#DjX<|X#Zcu$WBp@BmN=HF+VIL)zgpf1F0z`afz|qK
zbJJcMh!SOQufsmD2;{OkzwP}ryFVfuku=hR-DHn_-j)h>VO#hS^T^XYeSY1yP&|Du
z)~d0SUgWrT07KR8zVtoCY{~mV*@`JN1<(ztH#&^3J2YrZ>}~a{1MVNVt!g8*}AtqKfSzG=G;BqiesGtC7IC>quy&LMMQaJWw4Sn
ze=DL_b3-421InH+`HUBdGc2z-Vlbk9Ant}$3Y^h9op~8$dmfbij%%BmC$R@#S>G4Y
z31Dg?vnrkfs}n;
z{+-YGG;P{Vqm+>uH_1Yjcky~Vqa>&!MgBJsg5}P5F0r|*`-@)_F9#;j*%IToD#Y%e
z(>^m$Spho}xKl153wnKnhvOgmK7KuJYD|a!!*Rz_yGRX3?fRS;Dg&8O7-0OZjxH&2
zkwY)K*p+EYJg<{Jzc89A|F~+|3k)$|d1_5PH-!-}ZY=({{w3AD&y53*#Hv7{T>z(S
z?^x_U(E^e*8(+NfZ7!4nb|&H`S?MWG-vUIfA0@3ax2ZX3*EyWQiT8J=
zr~UZo@23tsuXP1Qpr|72AC8EqCr?g_ZEW8TwI1ayMIhG@(?^4U>(n?Y_X-p;JsLh$ZBm54
z+9II+GEtW4Pw2JcwV?a+3pE<655&bPWJO~1%tw)LrLTnNUChicAC=(
zmj_Ms@JC2RAGXi3S@~d#Z(}sYnC))NaZvJEzhj{QRmnET-#NBM{;UYc|EJSl2#*Bzgx6kV+VrO_oS*z#=37cW~8wjMbp$a{eH4zS390*@tOFp*yb&=^dJk(Fh{;W^QzhYP
z%&39Gz!f%;LuP>m%@YRzO2o)lP{J60W9;X_AaI1Xvfbjd@7^5cbgV&A(*n!Q=8*OW
zbAznsX+aMI0>M!B-b}m(0Q81L;$%6}INY4{b$sH6!)?ksjLCXCA)vH*#Q#1vx$IO`
zs0DvP*e+dq7d7J8HSxWX(ybdw%e(JOkG*SrAI$4cz1#clBz{*_f9eovm)3fMBf-?j
z9oKu{z<=SNuqN~IPc~&o(FU|^EwD^nUVzd*PZ1{_
zKo@r2!I65+c(I14ZM@>m#1nTm`fRdx4!SRW1HsmeD{VJ93LX)Xr>nF;^80cdEa44rbt
z%$5j2tY@$C?wu~$F4`7>AmK2XVF_-)sZLyu=;dAEWEJqY-$s-WK>
zVz+y_?8`Nm%^>kYX9J`*V$aKAP8|>OE=yPDcGjhNV`;sfjQI!S>IN{n(jeUapnKkI
zlsF7qd65k)zPj9Jr>1ex6#D92%X!`W8@
zHnX;7{Eno;9+y+fx+xhsyHdn&J>Tq7zKOEU2{heQRHlAyW*Rm`7Bws-Z=qVZg
z#p&65$^_=qF9Me`>S}aziA*+%<#F%+O{iHyG&LU*Fw#_)kx5jy8(36IG2jojl7$3w#=^(IBSYZQp1G-eGt-*jhLLD_+mUR*
z5C(0R=1yvpbE0O8{GTEq5WN4G1w@vY?|#f>1cFuaJ{i~&5LwV~9a%;{sARyfgd7$X
z?EIkPcg32JtvKh5j~Sl^djKlWk@^jHi-j!7gP~$`H-M`N2G`|H+8Qq=aIB*S`!)_J
z!Ry(aR>tarcwp4P_J8)%Qe{n#Thiu&whCkwFz1V;Cc$2%x;Mc|^Nlp3@%=Z}zRDPp
zSsgP1n3L5-(HLpBIt=(nTc>HCwye0kemiGm1hH=`O`KFqU^wRhze{d6TX77^$9Vo%
z*yD+l>$)KeROjfW}7Lr@rM&wqW2cAAWt#ezbZ_i7b$Jd^9|~^t_m25x70APVkrHa
zOKP=W1zD3?8SKjQ$oLcMZ9csL`KTW_%*ea9q$dkRx@3qc9~n!dh-#Ic=9mAoH>hZg
zF@Ltb%*I!7$@-vKTYYKG3$T&{RWc$`K&}Kyt6F80zCFs1vN28hRkBJGr|#e^c-Ed_
zXsV%VVMN-!d0#^AtoM(^{CeEtq#o$Sr%P(U$mGGU7Mwd1T=o~fTLz+AvS1nNQjV#G
zf|AWqCL?7VC$J7MC$R0j{!v~n@9^*#X`K!2PakiZ3Q)w*ycg_fy0R3O;ifgM_ph1~
z?J;mx%|loR^j!prv0}$R*YFad7>_URq5dpm--<>@Og%m0*y2?CijNgh^)_o4H#P-u
zFv0I53P)$lXW@c*Zm>>{qt-CTV3qO1L_%tuk{MO4;tW_-Sh9~osARb~hO
z(q07>K8(!p2U$_HL!v3Oqyfo1D48whO$AQ5LsZ{AIql1YwL&C`B1;dmCIPn?jN1~h
zkiyBlowrfYwkw&yO-a1V_c}+c9$@}0+~Tcc*NbPqsJ}u2+t{giM;dq*iZa70=rTcK
zaU0BnYA2*D9cfL|8sm#Nk4F=#8aJa8f~NDu{|y)lr<
z(+g9Lo>^(3d#(cZ>z?u@37Z`@>JNahWRx?gBFu9{{Kna?q)v%llTCB^&T=6sUY))t
zlK+QXCpk_)B7N%#u7T9QSdr2OGd%16uf#fOKFY|>9?6POh_5&($BEgxseDA>n{EZz
z<=f{qpMy7<86uF45eDn&;&JfZ15^-zQcl3{ohtaR{%en>?&>rghh7ZN4?z`vTCF((
z_0W3O%odn;?Jyl^UzGo_ItMpT(iOlKcB{OWxMc*prG=!eUk8!YcZ5dk8RnsvtvY6{
z!)mq)Y~m9`%9Eh7VqycLT(x-TRyy!x$xDhOFAUceEK{z8rV1LjYl1Tb)ltoitks{L
znu^O0Mt96$OYM(!D&sYPT2P`J_?l3UwOsNcpfH}|$6jtoD=fOd0_?u4H>Sl-SRXi?
zTD3klT$a97xeOd2gsd(-ptUYSlkZ!&EdB~$1&Q=$bu)`ZpPlt{$EDB{_%x|SAW8b*
z1QU1UKp*%60Y{|-t~S3oxD6L*&(7;?k6(&x+x$|B!~DZ>^R2mwPdlC*2PPEcY!@S6
zI8Rbo)t=lo{Z{X_obxeWSHz;({^NmKwV4$mcbu=YdwZVdf2v13RQpW#4+}VK5~h#t
zh&6Q&Qn)e-^v^g}fWXZ|PGdGn{c`_Bw*LE1-jV2V
zYZ1toD@3KH8ZFECYbAQWR9jNzB`2MUjLqz#v?o;@Tm;J8$Kyk9qy+Wa0XaI`@Wn$Z
zKLXQZxascQix+?TtisTLxqdbUt&%|)mZKEA8kF*PZNv4N@Zk6#&hw6c_7BHiw#&zv
zGa*5rdlkoMjl{X#qRC)@fn4L`B5lVFoJG-kuYB-!BZeSmzA>{OCAZ))y|1m
zF0r8PIk8%}1g$nOm9eb@cEnM1bewm8T*}|a@IL^dXv_wnKt7YF
z>tdYl!Q|0N-~rC6?7n@uV%4wlg-iVy`y^Y8K46!oUgM-ePZy7}7+~(~Sev
z^G0+^(sMYZed*~m-AX0Vrm;agGyd}q!alazaNzgsP+-+`({wboc-CB75r>>$1!ul5
zP{-16z{+OxknoQ!-8-)4Jo>YKz*L|9Jr943b%$&@?q63;&kWhN(6;SpmwF;ZLg~N#
zeatIHMr6YN8I!>_-2NtYH|21dj##iOM)xV5k$KC^cVvF~nW(>DNYhxDPPsBe(nfP&
z07sh$&QyMGQ;EBtHjFsn+tkkAm(V9b2Ge@0iZb)J9%IPwfZ{Az=FJ7u;dBfe%I1w7FZLj6?|i$(4jFfjz|}PTez6SR$;9C@(1meXRV1*poI*!#9
z`{8s?q3v`4&?u}Eo
z2#$@dQQNtHdOF_-jjh;81p3p%UOwqbvk0;4F&Qf=VdDtnac91*9Xuc-@RcgN&5J85
zcHvI$r?E@m7bL+)b)1b+kGOKU{rDu3gxYrcTH%la9;>U_AAVz=Z7Kem+S02r$b!yh
zfqY?23wufO*3nvP8q%~Ser;x5j>sn^uyT(i#-A)d_Lvo%+FkNk62uLYo$B4H_nF=*
zkV>E=^v4C+a^DYgXaJ<{$UE>1KcbGMAFQj-!m*Q$JXKt0hy8x=X^Rg`=jv4bUEFzG
z(~p{dd$CA~6`zcIU@wKYRJ^=8Fg}-j_O~ON7CMp_kT2haFm0{k_v`i_@U-@wzJfO-
zZy}pUC<@MS<%%Y>`MC4AGs>i}ql0yG8s;*O9b+s8ceCTGWwl}ADh;3)iShGu3^Oh0
zHMRvxg`W~9T+XzpggnEM=st#h`}x$7WJ=jR_j3I$1OJ%_ym)9Iav`PtsBBdNzB?0_
z0ym?5Vs$U{^>2bopeWV)L(~^a=rsB5O~7}tJ$6C;Qe8QVKV+rfimvBXE3!(YE*Rrv
z(w=&%BVKZ$ueSs}U}uuAb#!zicmO&oPfy5{xAM|&;eABH9&_j)b6gQ{p24vEPlX+|
z?irHRoE8t$x*ck@6Jz1&0<6VLCdE@Jcg3G*(BYCDWyudSLA;+yMhOnj<~;G#%kIGk
zfVY7H?{(qr^uWsvbqKD{y>qV>)&21ki?C_7F(ya5NViT#KIO3+dzI`{m~R&z{3Mhg
znF5}+A*$!hm3JY?G{x_p0sIHNAlecTWb$cDd8u#p$o%6x7qglwpGDZfsfxPt+B|vV
z>$m-N??&*-lyRMAoVb~(^Jc;M_@A%6uA2YGZJG4Uev!+WO;c=HKsGB;&UL-rH$9xS
zzz}Sgn7C1Re=mwF);+%%3xcjjeq|loH!6Ya8sG;+2*)%{&-F^aR;&ZdLt?(g^7hJy
z=cMTJs&M9`3TLO1-AhRpYj~u7)l%K
zf%dr5g%>$K$ZN_+Q|v(
zPhg@geGDqQQt66~57F^Th1tD}^e8Tkg{+VWTYZP~L+s}OHA)p;e|Wh~+IMss)eLq*
ztqP@Pnhu+BSf48)OW;PxZ8gH0)9T=?lFaX?(2Kj?uv&?qws173oGVq2~B
zUtEFgy7aXhVux}?*zXY!7QJnC%|Z_Y#ij8C-%3oU@yv_4%lASkaFf7VPkRAOA|2?UP*{ajl5})l5<)EJB34mOA?MIG63Q`3%4rC5h^3sxl$>)M
zR!$>_u{qnY*m`|tN3b~D>N50A(FaX%2uS|s`jUBDOn^QW%D
zEO{Bva=nw4oc#BFn0557*9l@kps;V3nU>(`^MSn!XQAx8Czx-yBo$Vr^hJ6vyiG0(
z6Hv*FRj8q~q^Sa}A>fr{DY!?O13Q)OUA{Er)%|+F->_(R9#d}8M>%_sT;*6t{Hjz|
z{uLj?7eNOm)2H)Pq<^bOcJQ=H)eA}rW`OVBJ4zy{dadFrZBt=#P8ox$jhF98tZH5B
z{;w&L5b_%WKIz2UuLX>6@yKTOP7i$6K4;8K1{Op_JM*X`0O)oa^zFrYIwhmYr#1BL
zHz^St77A^$h5JtXAguWa%<0?6RW@+S6h+1RZ4qI%AGe#zBN8@DgZB6`6co)Emv$VQ
z=)Q-3;>7Di!@Lzh(tni`eQtpa>f?;NpMjRl%7dOEdg`S~A6iHF(e}5Slr$ayzeIt1
zrI2HUG<96!wDWE8*>AR3!%PO={9y|r_A@2#;Lk9MG!ah;WfXl}YF^!JbyV_1?0Lgx(jo78>)q7i+#|@FD^C&BmCPOA
z(~Vip6Hw6ldV%)Pf|9$;R8b8$;eFiOh{|10Ow>aWlY*q=ys4jp!qsuUu%Z<6U~@Vcov#}{qo-#T*T^@
zKj_6G@QUigZie*(HplxEm30UQRg2zFc^9!mg&J)_UVvji0HxCr4I|Up83AnriVi3XaAIoR)(_ASIdEsJZIjy|ckLfo5IfRzM!@iYkYX$;
zi|`z!qK>E4a^sv?1;DWsW?uR@3yzVnlZ$YA;l2RPB@Q;efY+C%`H?m7dL;CWB_PHK
zYcq0pd40^6i!*?Ay|$0@)~3@WH7EGz9L|AvVVzE3&uIc%rFKAd!iXr;xiSs_?M!Tq
zS@buAR*ccXrTA)bM@;4$r_+W8DOko@Cw*3_wBIo3GS)dPT=%nUh>RO
zfjz;IPscZ|V~Ep|`xjuUQ>g_)prV&pMR3AP{2dz%LAtWel6M!|;{>{+c
z*5v)$mm{AE1Q=jUM`F~g*L{3_YW)a>w9HTyyBmJjr-LW6|BilD1SXVF`LT-)l^uXU
zYB7imBu3DHLuR)v20%v)jVsw$B<&<{FE4^nS*lJimA6QXVB680%2|YNN(nH`#HPvB
zfZj4(cEM*a_d7Eqws6u#ivr+j8vJ44Kd=vIRpF-C&`q8A|2q^Bxcd!Gn=_+=Uq+Z(
z@189FH)-a-$M^rksO5igr4=-C>0KaC^9mdEW*H2i20q#s9{cZ9o&C)%Dswk7M)z6v
z-+jZ^8}$X09p^Mo)(G8R+bwKxw&9&dDEKLuiQcPcWX-Ake>|uZ$)8}e>xO-I>crst
zq5`OTKOhz{WRjlimMTOC@Y{(xp2t&ocLFIWdBG6bLEt~9MO6v
z?nmtoV#Y{=GWS&M;^eV=s@=zHS_BU&0ykzjdD1fBmay3zV&{G|EK{Mm&Fg7b2D4zw
zwhW%RvpqgFUG)h<>cXAA$etnFZ{?zTv)pHmt6CHAqWj9lHl_BCBm3N`>D-g4rjZp!
z2;mJr68pEVubq&qywbt6b}8Y5`T@N1YPghHJsh+0py7dm+&PEGH(HtXkdI>994g=1
zSjqSpd4YSw@T{;#?P<_v{aCCyR-Y|w+kBR1I!1rWHcII;^Y0s?QPZbaKtF9bDuwI4
zUl|ywF-BtF1V7OGb{MN4K2AHi9*Ue*@=kwSTz|3#mNmy4^;s%39~5}>9)A%3F4+GW
zUDKTwTya3MJ+zrYw{YO^^V+=|x2$fy3-HY&I8n!5EnrpoKd}$!{sa4gMFb)%lNF?4
z0h#A+)*J{9;0RAaI!Z~Fx1NHh)Hnx{&fRx3PvNGnijE(Rdxlk9K4|Fie&;{1r~R-&
z*wfc>1cCwV!wF@2Y~Z8ZmV@AJ>2LdU)R75vL%^EJt`G!%ojN7AEtZlLcrgfj(`J4H
zvEk*5d8#5dY2xbcEc9`m3T)rFF1pVbkjQQ`;rAX=3|Pn#ta(c%2Vk#HHTyrM3Oxa9
z9>Q@%ZVrAiTq0$j*8e^U!6!}pDa+WUn70UZY0t_
zcs)cb=R|edt>=DoFN<$C9#Bo)D{PmTlCVeuV@1+Vm-Pz!hHqA%?#d)6zCY)6PFMH5
z9w^xDDrrV^w_?Z|`<t-`g1Zoa6%AUSwl@wa((
zbdn0klgsqV+~?=%Ri#dZl`kx@AEWw{$F5SDh{Wr;L!<32vtjb`OnPRIv(({te|?7|
z+|8GSRX|)JQ<_<5`pKiRN`OR}dZMSE)mE=vG
z^b|-FJ3!4eeql;l+N_(H^$>oh`P^JbX>}!ZCDjqmb^nGw8S|*0!m@7M6sf@YmCRQA
z|Dc;yJTr`so|MA9?Ai$IFX(to@GZ_-^F-yQZDJg)M1O3auy`28#~19!Y#wthPP3c{
zLb(!uj;6Y~iNQW5>
zi~-KAVX6ZF;-NS8S@73ApJvojHcibRs*YE_Tv{QS_5sn$_4M=0XvlKS~`Cp7hrjxZ8s}J6C
zk((fm;Imjs5G_p_ePZ49{56MNIY;oFmshweGP2~C~E8N4gT*dJaD7M@nsC6m76VaUis!gYv_up5r@WxTW3l*wM2i0g2EOF3
zrZzuSf!gqX-N!lq{!CugBtw091N)}8;DRTo_C`Wo`B#X5XVL;XOR`%<+ai&$_|RBv
zYw`Pz8Nm2qmlU0Lw`O3;$W?W+=~74kP{Q%1wS^;96B%noW^Qiij)S7pz|x%A*X9wC
zwD@jCmjtzYvrYbixkJ)x?t|sTVq&i}7B-r-`nV`|2*nn14s7Op*HQrf09dOFlS_9e
z?dR(Er8lq6_J#wA-*Au4<;0@(_t>T9BOPk^_|Ny!MJAr}tD18d(6`R&CqW8Hzlsza
zYI5H=CIob9S~97rCaar0g;Bj-WF&p{Skk!d^7p-y+#^KI#Ixt!XUodp0Re4GqWtLM
z61uc60TCv;-)+w!Va$=0Sz{NZ>uh03#8(e|QUlNp^u?OuqBC!K8O$ioAClZJsr
zRpZV4hL0R7?E8~vWi9lT!x{^VOksyjM3vx`2E^OHM~zk85aR~qy1#37)d3P^lwgPO
zTj{#pflq#Gpi2f4ft6=D9S@jOX>y@b7pgM%RFt$%V)$0NCd^Y4%Z^h#4PTXyr@--a
z!y!c(*E!Cb*qWo0nl8F#^RP=1*-KUcj%@iTo8p);+a-Bg*2d_H!yn&@;m8T(qq*9E
zzhRigdnC4gqVFCF*2A|vD^wmN9x=|CpXtN-6Vf6rNjfYk+fRev9)|B=@F@c$m^U88
zzuuj~AJ)KVSR(I^e|$ysggTph%69#cX;|&0%SBhDcgKE^)+ACz`jTD2=plM+av_jE
z&cEcNgs&vGroZZn8@70|H?)TC&%Mnwx;#^=ADf~Z=*!VK{ZY-GdkLzLm{sFWm)}{A
z1)x=tXl>1Ar@pB_sCdI0zJ1uCnYMv<(z7U>ePF*!!xp-(AfzG{ECI&k~HVwy~
z@T9pqkAalG4JIuWx0yS
z%NB0#4uAj7vE8_VQvP{ABY&NPHRtjms2AmY38T_4zD1pBNZ*j81+x6N_ch&)yireG
zbWCMT&^a^nTqi3eec|%_cMFpyq^ZjMhRJX^^HR~|pLC=NsW*9O<5-AjxiKyTLS
z$}z<*>5x(JvzmefM~wR1a(3%V`b#h+o85(1a|TA%n8I|>on2THUPCXB9b&5r*0-J>
zo5;NFIqapOy3oYruVmF@+EYeMA$Qv0MylE7_qgjhJJj+ak
zfhTQy8T=$Dx@fOFkr|wLQYm?$vq98yI-G`01mBU(kN=)EdTcElw}^p{n^%aRqiL2sa2SJ~Ix1)P
zJgje5fimhxyriLL2>Vg4HlW=)l>IyQh)0zt7~5s-!66@woxqTZ+`Ld2}
zHAmuGyUmSc5|c~_^yOop>aW%Ns9>0RA
zLWJoU-g(Ak@#^~Y@}wh;an#-_`x6P4mEh){758ovF7^iSP8bT2#N-C@ytx=0*9A=A
zbMgM4#r(aVjXb5&8HM}8F5=|9n{x~YreuLFjjqP8G?gxRJoVsZ+t<;rvT49lAe_kx
ziBns_q#<^u<6w-qgr2FAam0fcruo*H>$_{71yor|#j@h=fqlewtO-2}
z9G3h4nuK=Ip#V@xm_oJN*^G13NphU!MbptbZKYy|cKB+tD%SSu_o`?7QqcPT{5b9
zawV_Xq-$1BkH-J=s%P^v>}k=hBxgjZ~hP0G~3vEm-c>Won(U#wLJ+
z*hi&^AUlQ$syM|K?LM!Qymy7QSUay?y~=^B*_>yY1R>#b&|7Z}$BPi2eWWmOeddH3=uvo>(WBkIyzag{2nt
z4uN=P_bg4Ihw}kr#1UEa>pK`ZoD011dVi5SeMKQBH%-Wzx(LsJeC4s7NGLKLT;sIOg8ecb
zSQmBSf#yc@D)z}Nui
zf+hb|d+hVBmwyo5!PAo_`R4TqOB&u8430)M^m&sPomy&G!2G&S$9`idsNR1ouFRK=
zAgsdi6t0gdUFBdLf*M=O#^{{I^{=T0UD2@%rh=aK2GSqT{5|xlFK=G!Om9fD)CBu5
z|0dQZc^{K=ayuJ}{uV6ZNMf7(w&h|?Z^33xz1&Jc>dJ$|%@~Nz&}8fZrtHPNYm)Id
z+TYm{MMMO1D-ONhqJjMPdH!F_mH+dH26-G4ChGkTRp?+20Ia>z8w&U9CoROSj;3s_
zMnNDd9KCoUEB$9_%>R52FJI0{Q4dtl8=%JBmVtb9O>>FeeG@u#XKNNPWg*%5z~$?y
z#wyxNfYaB9$oY8T0R;q!%H5q_4bh@zJ3q
z@W~ht0uFg_=i5~Vs6|u-(fiVWI;8(Wbmq^Qm~?KJ3=WIqe=|$l@9&&FrpP>K6Wmbw*Yt%zU7a3Y=;^`5)}Q|RZay+d+B(R
z_oGE@&O^azl^T;TEs0ECEcD|?v-H&o;}k*)5{xj!8y5}Zga3X@iH
zGOK;bDYeB)(NE5w+VL_WuzS0+D%i69MXwLe29MBn)4GAoq~%t9>Q2pj25F5j!+$Az
zU`PYSdos6ly>E@wqmKch9ghrp%XS}$?|`1mWS{@}Sb0qXvhcsA_>-zE7yE6kQO-St
zoz&AR4hM6(L*|owSKFQbNCxCP)QY=Ymhm1;KILL-{E@SkyS;J~f)9iw-G8FAfq6?B
z@$bGqH=({hT`&t9p+h@jl~BcCJHzc2D0xg7DYbNGLHRFzj-eeM
zo$1r5uVnFm?e0FnmDFFC2=iD^?PMLc``hTk)G2WGL$&w>9PX4*X
zMcEQ&J?I{?{fG6BgpWyK5eDP79$bHbult$BR)b31F4x>|{Et>^YZe=2U%q$lTkJou
zt4rfG$(NK8gpMfFXGwSguz)+f+ltbPKlz$HudDH()B7R09!O;A_`<8f-4h_|9g>s6
zku_Y?bFMEW75eotPU+aRd(fGhge8xVmYqNPug$?VL7Drsi`8W?l{$g<L7UXH~mcTXiS)ory#mg5*>Ch~S+>8z`Ee)mazcCBxF^e?t
z_OA1FIR8=oL%}@Xqj#rQ^09WRkX!!fBaV}1T>24C>zap$^i$66M(E-<2gPvaQO#c^G)HU0^?3P9{clIr1c%Jqo+94wtlad@%;`?7R|)2PF4!T5^daQylg=6
zM<`we}n>dMB4${|#(D;FC-T>%L?`|C+uuhT=sA@IGq
z(?H}138nqYSu*MqE*l?Ir9OWGEFk&}(4v&-2NR+MV9KvUUMhSp_^MK3MdcH#np1}b
z5f3ftfYPF+7;e~v^}A~49j}cYAd;_597Ho1SU|1xjPcLP@{*Pf=ELL1XYb?{SNjhC
zkyp^%Twi3R=5$6Kqr875rwli1v?{5q)xhozWKzIF#D8d`JaspXd4D8z1q*j+GanrC
zpu4gY#{EYi;?iXm@o#%%$68)8k5Z#2C?w(~6Qr9DaEr@IyPWW)ACk&mBku8^KR@*D
z=n{-m)#$OUw?C!8>+1?ISp{?|ANcO^mABj|>DU5Q?v5h2TrL_*00Vyy0383iqbYWc
zhWy=k&v%eGZqRggzm@*FP3nB}52vWQp~nvC4a@WCZxLoLxBRVXj>0dF^8Og+XAXg)
zErVKQOeFQEzmRr2_>IDxHb?A?G{_!$vsj_Q_2hIMdc-MY6R*u|JPh`BLdDXA8ChLb
z`|&~8@6d-dxd#9J!JiLqV+*MU3J=6!$7l{?WTaj(G2j_T+4x|l1`h%vLbZ>^hJaKL
z66<>B%7%!?-%|EWn63N_n2+xwM~0``R;*TVU{gt~4+l7!L$41==jx6M;0;nfmzJsQ
zaUEaPO?RrGlsZOZwXS^~!Qz+W7SJM19}>CaKf(xvj!%X{
zA42`dkG_~bvE_CcVf?nrmO;nVhG!nEVASb}etLTEbjd#KF3*tITH;wxeb(YIA$}?_
zHry(Z^>~Mjf0N2=^Fak0Di?brN8I*Kpj$pQ`CT|o|Hep4`FXE^i5!qLu?DkWK8_)S^e%1loS?4U<}=jk%5`M#vwSh1
ziG{=S8=!D)*jF4<`VZ_-P;@+i){CSq2SG?>bwb{velq%72y2y;i$!Rg{94}zwOgt88luix9q|n1A`z1oRmGqRLFIR@yRIIN
zyz^3hBKCdbN&Qy3+Ya0h2oz+T1DPc8cRCEor)$#{o0^a0(TF0XVQ(?Uq-!ZIsft=L
zP&MRVLk7os_(d~Ia`*h)Z`E9_F3ph+4?!RFT=bEeS^G@GhakH$7hPyLlz%!h2vs*B
z^R;C*WZ1#mM%SP9Eau>q@|SH1KTpay!nZQg!+ftKQ=
z!I89|U#$-r2l^F6dOW0aFT8Ueq2!17yO^K-JTCH8C@@{v
z-mHT5yvgimPOLYphk(>dAhhvNd*e2|?5h0~niYw;Xy~m=`NGA`hxY?uOv`AbCnrqPmpV-z~qdJk=`CwER_@&G$7$9l^ZA8RAtF`LO$`x0>Kcu49g
zX*!t&`9!0b>sZ?x*KtpSRUI|Ik;+FOl#kk1x8S+BSwmt0=S4M}(DEfvILGw({|TiK
zse{?%|3p&$7142WERyFyH(z&{oaS34{DXUKPx<4mwDY#a(n8vy%)bT4x8hCWRxe#u
zWN}X(?@H$?Ko3jl8@H_fT?PMFjeO=z6iE8x%gp~er-SooNqBwWsB)7Ecm-F_=oWQJ
z;Rassf9!$m!^Xgn`(Nu#qrWI2A%W@bnT;m;w-f%Q6S`E{8ezgLJn%c!C*#mhdbg=9
zS{o9cwzG?3h`h9UHvv7*MdfXRrTszn>VeqM8AKw?c&m-viXgpqR<93@$hq{d1SZ20
zUyjbL{|#0F1)PX43(>vjc3-qZAix%WIrPDL1uyA;Xj7=kfK5le(#d^Kdt<7iy7NQs
zK-za~5>4mMO8ZxFV^kfs%i9Q~+8}x`I+jgOnHT+q8oWu#s@q3eMz~7Av5}U*6smjl
z?9&ao=!v+%DHC+W(DHfw1Aa78bJ%Pu>0Vu~dhfH}lB&}MO3w7pHSftccTs@^{$R#0
z{g;CfQ`nPP66Mtp9r)ng4<)L7f*=zr(;ybVe55{4PH%wUHK850VgLSF=^5TkpeGk+
zc#~J2;j6xPO*MK5M=Z(#U9s8QDvVOx$^~;Gp+zWgel?!9$4Y1AKHHi7{s7|FRj~|b
z48KN0YRN=&ANHmq_gN=q{1x0xzxJ%;Fa4neRUUok0-7ecGUGgtNwS)eP}vdG=L(Fi
zF=oL|Sfh(n3jkHrz;i$nwF7o?rzApuQ#pq2pn6rRRg3))VW%~T#^-k
zG-D{`oZ~+FViAKQOg|iMU$w6w&vs~NVuO9iAaogsEOy(s{ZS7*td=Y4db>-lgq7oaANkKwz7dk#e1lU>SwYN(dLD7CE=g!Nk2(ik+R9D_s7y
zi}!se(t+KU38foHqe%Pp_k~eEL%xRN_NWh^V%~`Xnv$hu`FOkBS4+ml%r1D>YQdbQ
zK4E3S?!~h|^RKbe`n6(xvcHF;7BT!e?uP4^9x*F!X-2Rtu<`24WV_l}fS5qcEfAw*
zjiG9&2<%RMScn!h>etL;VjN9g4tfAsGdJ@Ez$j`Rn{yTimKj|tg3fj3
z`%S6Xq{IO_S0-VV6I0_+N7Uk;^1)jx`}RBq7Ek5?ma*Jk(hY32G6a(r
zFaqrV6C}|oE)BMgnPtZ}(R
zm{R$v82;Q+C?Jqi`g5IEs+wJt5~9@{ww&e(fq!jkB%})AZJ1yP6}A_%PGRqAufqj=
zr=1*vb9?18-9tG;%DXnY5GL2lI6KiALI(7R`wZ@$Yk-jU=+~=i+&ZzT+Ook$Tom;T
z1OH{A@E_oa_5z%+VYX?=UO2Y=8TQcr)DNn*}e=?2!lI>uw{Nn%B+I?02}KA)~?(L|9KfXDbK
zSTVUMwidNKBGZ{{(-!l)31QZS2zLg;Lx83z6(PoB7dpNahNi1jW+t@EZylMhwD_9_W;1J^wj{Ix!;%rx&Hz4d6A=v;I2SjlM0YP6;
z3la>JSQlTLVBHF@fPhFt$mg?ReS>{0#bh*Moj}h-;Q)W4WAo0gf)!zpf#-1C$jarh
zqx%?8craQ6HSN@Vtjql^|A-nU_k~+Mf||oKMdAE;^g(VFiMouw$~X#dV0S+RitNRz
z6I0D5AW_!%P!x^3ZM2S<3DlI2v}(OKZaH3L%l`wri8^Omftz-8_5*i+%TpEcjP3uI
zNaz36^!8tT99|2(zi|bedM)x&(P@zF^>$72cEW!rL>t1jq2H=;V+vm@IG5`Q2loi#I=T&U}zzMMx3blE3_(KC!An1ZFe9XXR?ws+3yy{Cdd!
zBTv{o=EPg@tNU7qmZ7&f6~A`S{u2TUsmA4l%cp&XH#(+bHjZv2_lHg6_xa-GIgB-M
zQT!_U$i#mv4pUb;x4FBZQ+S*;9TUWR^94~L(wJLkDVadKcH0{S5Ea+a$$%*MI%UNF
zsDG1{_(ZXbsjH2K>TP|1f%2xNy;_G7Ez!8)^QA>03~45VGpn}fxX$z8;s+T9lz0d{
zBBgtms?5>xAwA0vba8?X&a%a0pRxx=NjcxZDu|BsQrZTh}YEHg6r9#NK?FDC_8YES3t7%UH+b9lXmPk>tOAUL445)NorDH)gxEyc(mX
z?>QvmKw?U(KR<2s^4nJ$qHEkR(H|QuWPX$y=`34T|3MWJ*;PT9Y5i9rAq?uU2xflL
zRhj(%Fk#W(iVG)!h{TLnr*bYI&W5DB?SV*V+I8_BG1%Sum%$rmLa?
z@HvP0t;fv$_Z{o9k88NdoZ-DstYz=Kw8HZTFfenszqi4wQ_}%P^yAG%5PwkOD&2>9
zklp=6u0(n5A}sq&^je0RFlGKGc!j{Ldh&6CQY`ooSA!xgK=&Qu`87e_EGkMuh>;JIuk*^lG0?;)O4XYHq6@
zb>LEciYfl}t1I=L%u*0Ld6FPII>?Ew%pLT@lrvQnk4<@r8~36wfZ`fAb+^Kn%w(~o
z(!|P>^xQg9jP|8p-*w>LX9(Um=6Sm>Zx9Vgeu$8f9U4U}8d_~>_4@%^oo!jiO3(AX
z=7FD7nG~+~Q(wJnnru7-?)#S6XjOR@kfCp6v)eWEDAmz
zimmoAR8*w1v9B11qB3{fv$#=z1oK!3G+88;)m!@IQ38f~Ez;1ernfZVM%!}yP_hfY
ztDl8Ak+St$(1#hnz$Z6)u5pHaBmtaM$dR|OoOj(I%C%e2ufM2~$
zq75PM4S5
zkozfqUj<(|Onl;!IgLKkp5RH&)ZP^KoCs+aYvn50Mi#AMFI*<%rq^I_lZtCQy1=-n
z&d4c;(1C>F
zt~+IavpDd@Jj)w0az1jY2eMz2uO_#%Ik7Gtf>oBs_|Ic*6|$HG6-@SVbmR(ai$&D;
zGwD0c*jkoj4qxRu-;R)aSJB3X__3U0ORbeJMe8(mCW;ZwwN@8gv2tA3^<7h2o5+It
zyr-M+x=Osch1i=r0A$5Fji55cz{y6@GwS;@g%?B@;jBls&W)G1R+D!@as@J-@~Q^E
zaY^^rP^L`uv#4f3ccK3+<6PljK!-Uy=~vcErvRScg{si+d*5GOkrtO27s2R7`YnQK
zWW$@ecWae#WA9%}T*s5APP}WKX(_4yKznoz3qe@~aDthhHlp2~6R}^vB
zFM&CW=UmB5k$Zk5?^S@n^TLgFs4VUR8nb{4A&U)Ne}s6R4IIp*+BXV>dlRSJDAcWn
z=bQb*;<_O47XuNv-h5{aS;$A{cN?Dz9>
zOV1t>`$L8n{F{4ArWXlQtc)peJ`(6|H}$`Xiwo7+9ntWM1BaC6TU)HHfeD$|QvYqq
z;eni2=F4sO@|sBktX|R-b$!ndg=d!+XNOmf41aVJk};hb;YZ?()efpJnlGug{M_ZU*e@!H=tN@;T5HPx&QJR2o*kS;U$jMxEfV7!MmDxJ
z>DxG{ufEl0Ig%J6ta3M05eXH47dL8`3_ebYj`dJh!%&{I@(;ynta1PO-L)!)JfKN@YmKake>
z6U(gF=xLx%YrV9hA#fuqOrfP!K!rJPP0A}n2clM`e-mc|-PnGP
z{;u~(Cr=*{+sb|}i%#(iWTpHNwKQD3F&wAkA-`Qknjlv6m*vr~89ZCQ=?}fby~25B
zC=2+B#u?CZPV^T65#_}-ztK*xJagrp)oiALh3PVMskj#Wi$C=1pGdZTMxMMeaB33o26
zG}{r^Tvep%2OuU1UkoUeQU1&gDf>d5u=-np_r^RM4SEP4PNidg_nJSy53{s-@X;#4*3V^Vb*y&`+v|R%R}&S*pw_o<K;kZR%;Mv<2XWDptYb0y2`6GEmaLeM<
z76r_csg`_syo3MS7?tmMi0v+r{Jj$fI52vv8M{F8YaI2Kma``F9B?c@(+q+}N`n&s
z)rY`h<`T_!F?Axdnw!^&!b`aoy!ZRiEo19Tm?6g{F}^sPD+8b0Dz#Kw01CxRg1B0orJ
zZ)vF01aYJLN=?-PgrtVcX&)kPW@kIFXl;3>
zlqWq|)4}K6kI3}}d+Rgo?3M5y-!#10SSC$x`u^QY=7KWhy@>aMNs`$gLi2)3EP@2ja=dI@v|;U#T%TQKgm
zXiV*C@{Hojxw8)AU*AARnz(Z`kQ)Cp;ig19nZoXMZx}NA<@&+9rozsw>>lS~iN~c%7-WveqDC5HTJxeFj_jtJ13SKxXCnkk6zzyH~dmAH#bnHXH&5y5rr
z;hcXFY_bwWU;%t3jNI*s(eu*3V*>}?u49kdCkiQ%;MUzbpI_|tzABu({RCV^e34AQ
zD&!!h5r{Ro_u#JXxe?EFT-p^PKw{dcidZpf{_KW%I4C%*Q_F_$Z1YaVhV6QyvTLRi
z+sqCk*%bO;Qy!XG#{H=NdwtE$O2sg*$wn?6`y-;HiA2`J$G6aST!tG_daI~a@`RSZ
zzG6up>ahYu8UGyQ9jq;JZ2x8|KVEph@On+`2Pm<}F^9DI9vHnatLiB~VICf<1Nnuu
zIHX3K_3w#Ag%v{{wh#_w9i?0g7T>^&i}yN%d4s5kn8z~zz%Je&K?CNql4beBEuYzl
zVFYRkzh%LreLA=t=Zi^%Pa6*V@+#P@fj47v6>0+~*M(1C?^$Z<3H15=O#r?{nI>H0
zO@pH$q}QiYupvm06QxB;so1<&4K5y26w<7uSBCdtmwhzE`{D=!Ku5VT%456Zn-B)B
zkN{!KRKrFuX)-R=lRX_l1Zt^o!AyI1$iP(
z)>cStP&C>iZ+uSLN7$OY;{XTkN@Zv6fW?ElpV-qdTG0_6VTp;^!H5z?Jad>{I(tK!
zfI{6L;;b4%!c49YAeoIxPN$qT*tAy%{d_1FqYdFckiH*nMnsS=_85A6=)0{vcmWCo
zS;qTOMnRCCmiRXCr_5v*?!-QJ)F`kq+#2BZdGxT^qJOIeLLe;C!#d9Sk25z=bdEjT
zeZXOvg^f45Y1iIG0!Y_g7@o0yge#FV;7b!y&k#?!B+$>@u#
z3=N4$e2tLDSU+pc7YDS9|3!}VFKFn$6tn;SPVgq5SnlNL#G&kK;YT05-`_+{e~U#W
zdA%LnvK2dTG@ZmGaU$krxhPIUz(@8hr1k&%*NywnuQnvs(ip(%NAH^ne_>9fF3|6Ef}IrXy2x`p
zq4w0UfQN^R3Xw3oa}@i!x3PLHd!$MB=>A{_an0N`(BvNdGI#8m?}?aaMA(-V=>Xxo
zsXWGuAPkJR9TE$fY-y3ZI8g^O2Ba@f>rU*M!>0LNJ5@-$j>HmIE(}_=&i1WF5*+su
zlc>-`OD`|v{ra4_9!pl@uk_H>{7F=QXi7`WEBoXK0qh2RX?sw-yVyD-r)^WfRPc6~
z+!DTR6U;V9H7E6@?cWToi1c4v=VDFn{&N2Cs!KD`uEXcP2c6wVNbpX!Yr9<8W2zO8
zy9s2?AIzq#kj51Nps^T9V2Sr0?NnhxzBrE3nSvHUs=9+klb7K&2{V5uqtHH*nOn|C
z&U(U>qpRQPM2WZ`6;`MveJXmN-HmX8tEv(^ivtvVMAg4V5~PH9n#XZVIrDG_3E)@~
zDI7fYL|FsdT0l)ont&eWQ!qIJny8eeJTI@|(G>rqj>?SuM_muLsimZUtW{ivh)0Km
zJl{!rLLyC9f7ul2?-{qi6{X>*3lmllCStE
zy;0@c`tJcQ6Y41j(e@ue)fPzgKv@JfMy>4X4QaH^LAM_4OLYS#B!r~D?)k;7@1)gn
zNfoR`y%BEUFv0O`>mmDQIWdm>jV9Xs=wyK<2J^7*fd+^=o=3*Z4|(fs8@zabatz!$
z^a1}8l9)oxksT)X(sI!lFziiE_Z{xtYelkTr)y2Pc|ARJgL^srk}bohu6i=tH_nGrz`O`hH_x(E+MZa$`P%&e%;NHKV08D7i{0x
zjWfn_E!s~RAZ@FG7g(2WqGT;)!-Eb9cHh~!Ha#txuw(sv@nYo-Wz8xEBQNm85O7y1
znfZ|%8)sA;=n?kqNPzztlU4^>fv&?2;{d)`B^G!ov4g(76vZw`5Cn&pVx=PfKs-yg
z$|_!5)HyII=XEbZR4O#Cfi(KkP9^~`;G5c34KE;bKh9^<~+(9wY2dE}3cA$SlIJzKi@
zv~Tic3*y&dmwl@zOscC@hi;n}j75(*YJz(0^ys;mHv?cVbu@viFlFf)KOZb8Rx?fK
z^88G7q8J#bRAog5@3oRxj>E4MSip6Ogua>Zd=?O8=X}{O7Sd%btWL7H)K~VAXt6GO
zsK;2;iBIx-CzkuDJoM|zqj_6bn%wVxv1cFO%(&+01FUQh9j$piyvI8gc1|_a=9(Ln
zT|r^5$QnMBkHO)=6<=+y`HJzowT-)rx8Vuu4ZZ}}KNU_tj#8v|fj;u#%qQ5rAw4ni
z@3VJQ^!4XJ$wr`SpK{?UvXhqvC*60&84KHS^Yb-p0x;VyT-&jeNecvx#NYt~HbbRQ
zo_(U{*)`#94zZ;p;L3r&YLsE3B1}<7_XS^Q+=r_rJc&MxifaisG&PTQNdM^rr~&Im
ztT*)gm%3YOdftuuRq&-QcVTnxpFV5q)lad#{Ajk$yxPf&TqhnA&}4NKO?l>R_@}bC
zeBo!(6!$UbT~pB0!1M2Sz@(a&xGpj&B)lOu<@CjcMWS)FFUez7$-~F~@c3RIl_N+N
z0AoaZr}6^oYB;fWZv6Tp8m;jm+AaeU
zi0Qm@Bx6PZ4@hOkHi})^DnAB?t{vP!Kej=9GnqNZk3CKP-O0wFKD>)F6^EpT_IgkA
zbxSA`OXhFQmep}TnSSvvx%o`JBKp4nM1EU7eK9h|-k*@p6?)6<&xtw&@rPg9<`ArB
zLP~eZF>{rzuz%V~1BFE?sMs%)8%`bG4&3UGnyw|R00!ae32nIuzUTY2WCBgSUHe`s
zxD&^t$xEcw&DUCzeybdMx&_=;bXvK|itM45hzv)m15s3LJUjIErjbus9V))`p(FQv
z&4#lUyh>&LF0eJ^&@D{qnP2+wiV-1n_|3Q)@=}MUL(x|*PmMf8jDO>qC9>^hgrufv
zkYzzuob9i=6LVXlro}E~t{QI+tgFB@(QuY^H^<0V>Fh<{-QH5hNHwH7NUAa`?vNeI
z#I(`C;S&10xw406&&zscIT<(^F|Jbzf=O0SnK{cVo&d|Mduo9ZeCTGzjGlHM76**A
z2RQ~l4AOM0+^uf6+k0<0Os1=?l8DyDZbO0l^8*Pu4D}c~!)C8l`(1mDJ&r1kg??D_
zD8d0e)yA&;pDvp43j39&U9ge3$M-OoT2H@Qxfu)kf}$bX6LOuzRn$4YcKguoRR&b~
zsc&S6LcBML-U5A>gun>lS_LRN&6>NJD)2X_f7X
z=_wx~{2V1s#0&MpwoQ6MK|Ml2wU=xT%rW}iNWcOE+cog)lKblWzfRxxgrrI-1MV+W
zK*OIXTj@X0+E%o|Rg<0BQIZzwEHF74K46>^WQW0eR^B
z<$dX?zKtl=1@G1omPoE`!PD>HB}W*53~mGZ%!AH!`zLDt1vB2}HP{$Xzkz3`W}2nK
z0ZA+oxbJrq!QCTd`imiqqhR3>CvIi^{ssojWUt%(OGxz`tP=mp>h$}CS9dx*-=WTR
zvHrSp!CDH978n(yr%Iqz*MT!+p|9_{2t$P>>d_u8_09b9CoB0ce#4n30iA$cBw35l
z@-kR2v3vb0zD48Ia9T{1{>z6RNNlb!i;?^PF!tV2O?6+lFio*gELcDwDk@DunskB<
z1ccBOrA0xyfOH9jfQTp}MS2ZL2}p^Bju0#qktQuj3rGne^dvw+lIPnzzjxg4j{Cl2
z-20D63+J5dz1LcE%{6B(Zs-LCd
zfu(YZWWD=WIKFH&MoZ?J$PA3FY)p1Y9BcwY$BrNo|C%d>bohqv9y$8+TBBcX4rQ68
zIdnQZ*c0@>XM@`8)ktv&Kjwl&+N|DNzQi7AuQFegP|NbkbF(PJN8OSq@&g|6%$&?G
z$Q@p~f47&n`ZOuzt;^&auGP9neVU)T?w|If2_0m<=6v}#9Oo}bficMJhZyf4DJQ4I
znV@L9CGP-JXezbo3fesb&)(2{0jt<^hRSF9
z5uKm+Yt-o1iWo5@zN1gP-yZ%XcyeY#nYSt}ZCo-3&(!$O7h6LU+sUMDQx$XLG2_tJ
zy0gxaKCYba$4U;Lo^k=uJ61QKHqi^3-XzZ)g5+>Y;~$
zVp_xs@j4+F9ZQmQ;a%z6L3^J|Y3)+L>KD1skpk3vm;5yN&Vf2#>
z;usdvSsVW?e6A9q(7_na3A?rodSg4eIj94g(N+{@?7`HpqU&+Mt=^}pOE!8gLZ=?=
zTElCX);abB&$4X?
zJiN&t6}@B@zGpv5TtvzI2AXz>J(HWOjO3gr7_>Xdp!%I`hFjmyx|=PtG`1!E{2yK|
zD?6OUS+f`F%le;FRy_~Qzcx;Afy@pMUrw=lA853GG^pwLh7u9py`AuJb_uaBxTtT+9j*1|C0@2R?
zmwY>Km8ilbWB;)5Ck@Z05|H}T%1$?BY2Y4DJcYO=PlaN(hw9{&RE3g|4MA!<8-ev?
zP+e3%FD+Z~>%20AK*4=l7kP5k+95eo7Ph1Qqz#4CO!1KSxQn<|ZswbS>S!dILWnWR
z6o@uRK~i^0(R`l%rmg#o>t8nG6$Ry%++3SW~$yhtqN
zIK<}@?hCVx$P9hmi}@QfNVB^DD9MXY)Y|((0_WdPyaLAx?sslP@nmB43qbDi>+MIo
zfM&2`(+WZ_Bh=1HyB*5%#^c8@yd*kUO6iRIV6P0fySVtd62SaBpag$v$iG;tf||
zn1fvV1WXS7YLetCcHLKSoAcTN02?dfRyy|FY!407vJjulWbb7PrZ7b0A6CSrcA6hk
z=mT17_yPuV@0Nyau&QGXC;MH*WBmJ@$quT>TVN05JlF~=2Xldl6K6G%YNgAp;1?4{
z3+)oT@$A|rD2X)(=`s1iUceq$+EiKj?tt<)3l!OMyf4J13z_{LcmmlubQ54|oM(}%
z-)#>UA_JzlFzGf&MTNS#@OlO
z$@*6=pY!yrVA18{K*3odi}xf3^W-S%KmP0{Aagw2&!w&jY%b8CaiSa5Oaf!H3veQx
zlapiuJ;H9__cF}_SbEU+Df>d&4(@E^M^Qw~CqqR@YEkNL@wrYvMmi!C{wzEMXD%H^
z{3|6g-jntZhZA%-!k>=o>(GV`mlu8b-H$2Qmx5l&yraXrAuw6AUuSFDb^=1{6Q7fm
zesSCt1!%9a&PCLoK8$d*t8&|aBJHwJEyVTLX
ze3=mwQCueh-@8wejY>MR3@iN!ttK-P#iL1<4{EQw^M4B$MeiayyQZ96kB|0Q8s&ZG
z?PK3jwQISI6a*G-Pmc2T^>HV2XYZJMwLsk&Va3HudkT48;h8&qVpd(LfGfwXK2yOZ
zcN5*tg$3^32MvVBRk?UUIVWSh$*s#>jgp@ObOs)~FIal2zNp2?D!yFt=tXIr=i0wM
zN`#>?qJ1%jZOONM#`4&fEgwZh6Wi&4Z`8NvUr&23;B9DT>~*&F9(sM0nNpnGr|&l0Guuwj4NN=;j#%6ye?5^&8#y$v>RiDI
zG9{0doV|Da_7hdC)=YM62uqVzDN?$zim8m5fS%oSm-PlSN}>!+gZ{&JIG*pB;Q+k?
z0HIWVAqGy?4w7OK=NaAod>+FTo%iI{9FAxfI!K>BxmlGgGd+B5+ik8guiB~0~^K)`rUq0kOq1UMS
zH9z2ShxBEWMvIpp6EeOWjUi#FI5?o0UFQaazndymfnawj_BZx5C#+}twR3~}0}T_v
zAx);;9>a7bv?eq)SG){8u*6$T*|dAd*(
z>ht*|y!Jukpx?5xjPADd)jrcfC9+XO%#2yj*uafi%89c`9p|?7E@j)N7>H6lENRU@cJ%Z9M&^*~&lX>mz