[Osquery] 7.14 bug squash (#105387)

This commit is contained in:
Bryan Clement 2021-07-20 04:21:39 -07:00 committed by GitHub
parent 32206b9284
commit dd159f1c9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 162 additions and 63 deletions

View file

@ -53,6 +53,18 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
sortField: '@timestamp',
isLive,
});
if (expired) {
// @ts-expect-error update types
edges.forEach((edge) => {
if (!edge.fields.completed_at) {
edge.fields['error.keyword'] = edge.fields.error = [
i18n.translate('xpack.osquery.liveQueryActionResults.table.expiredErrorText', {
defaultMessage: 'The action request timed out.',
}),
];
}
});
}
const { data: logsResults } = useAllResults({
actionId,

View file

@ -34,8 +34,7 @@ export const useAllAgents = (
const { isLoading: agentsLoading, data: agentData } = useQuery<GetAgentsResponse>(
['agents', osqueryPolicies, searchValue, perPage],
() => {
const policyFragment = osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ');
let kuery = `last_checkin_status: online and (${policyFragment})`;
let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`;
if (searchValue) {
kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`;

View file

@ -55,8 +55,8 @@ const SavedQueriesPageComponent = () => {
const { push } = useHistory();
const newQueryLinkProps = useRouterNavigate('saved_queries/new');
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState('updated_at');
const [pageSize, setPageSize] = useState(20);
const [sortField, setSortField] = useState('attributes.updated_at');
const [sortDirection, setSortDirection] = useState('desc');
const { data } = useSavedQueries({ isLive: true });

View file

@ -9,9 +9,11 @@ import { isArray } from 'lodash';
import uuid from 'uuid';
import { produce } from 'immer';
import { useMemo } from 'react';
import { useForm } from '../../shared_imports';
import { formSchema } from '../../scheduled_query_groups/queries/schema';
import { createFormSchema } from '../../scheduled_query_groups/queries/schema';
import { ScheduledQueryGroupFormData } from '../../scheduled_query_groups/queries/use_scheduled_query_group_query_form';
import { useSavedQueries } from '../use_saved_queries';
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
@ -20,11 +22,29 @@ interface UseSavedQueryFormProps {
handleSubmit: (payload: unknown) => Promise<void>;
}
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) =>
useForm({
export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryFormProps) => {
const { data } = useSavedQueries({});
const ids: string[] = useMemo<string[]>(
() => data?.savedObjects.map((obj) => obj.attributes.id) ?? [],
[data]
);
const idSet = useMemo<Set<string>>(() => {
const res = new Set<string>(ids);
// @ts-expect-error update types
if (defaultValue && defaultValue.id) res.delete(defaultValue.id);
return res;
}, [ids, defaultValue]);
const formSchema = useMemo<ReturnType<typeof createFormSchema>>(() => createFormSchema(idSet), [
idSet,
]);
return useForm({
id: SAVED_QUERY_FORM_ID + uuid.v4(),
schema: formSchema,
onSubmit: handleSubmit,
onSubmit: async (formData, isValid) => {
if (isValid) {
return handleSubmit(formData);
}
},
options: {
stripEmptyFields: false,
},
@ -62,3 +82,4 @@ export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryF
};
},
});
};

View file

@ -37,6 +37,16 @@ export const useCreateSavedQuery = ({ withRedirect }: UseCreateSavedQueryProps)
throw new Error('CurrentUser is missing');
}
const conflictingEntries = await savedObjects.client.find({
type: savedQuerySavedObjectType,
// @ts-expect-error update types
search: payload.id,
searchFields: ['id'],
});
if (conflictingEntries.savedObjects.length) {
// @ts-expect-error update types
throw new Error(`Saved query with id ${payload.id} already exists.`);
}
return savedObjects.client.create(savedQuerySavedObjectType, {
// @ts-expect-error update types
...payload,

View file

@ -37,6 +37,17 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps)
throw new Error('CurrentUser is missing');
}
const conflictingEntries = await savedObjects.client.find({
type: savedQuerySavedObjectType,
// @ts-expect-error update types
search: payload.id,
searchFields: ['id'],
});
if (conflictingEntries.savedObjects.length) {
// @ts-expect-error update types
throw new Error(`Saved query with id ${payload.id} already exists.`);
}
return savedObjects.client.update(savedQuerySavedObjectType, savedQueryId, {
// @ts-expect-error update types
...payload,

View file

@ -216,6 +216,20 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
field.value,
]);
const uniqueQueryIds = useMemo<string[]>(
() =>
field.value && field.value[0].streams.length
? field.value[0].streams.reduce((acc, stream) => {
if (stream.vars?.id.value) {
acc.push(stream.vars?.id.value);
}
return acc;
}, [] as string[])
: [],
[field.value]
);
return (
<>
<EuiFlexGroup justifyContent="flexEnd">
@ -256,6 +270,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
{<OsqueryPackUploader onChange={handlePackUpload} />}
{showAddQueryFlyout && (
<QueryFlyout
uniqueQueryIds={uniqueQueryIds}
integrationPackageVersion={integrationPackageVersion}
onSave={handleAddQuery}
onClose={handleHideAddFlyout}
@ -263,6 +278,7 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
)}
{showEditQueryFlyout != null && showEditQueryFlyout >= 0 && (
<QueryFlyout
uniqueQueryIds={uniqueQueryIds}
defaultValue={field.value[0].streams[showEditQueryFlyout]?.vars}
integrationPackageVersion={integrationPackageVersion}
onSave={handleEditQuery}

View file

@ -40,6 +40,7 @@ import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown
const CommonUseField = getUseField({ component: Field });
interface QueryFlyoutProps {
uniqueQueryIds: string[];
defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined;
integrationPackageVersion?: string | undefined;
onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise<void>;
@ -47,6 +48,7 @@ interface QueryFlyoutProps {
}
const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
uniqueQueryIds,
defaultValue,
integrationPackageVersion,
onSave,
@ -54,6 +56,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
}) => {
const [isEditMode] = useState(!!defaultValue);
const { form } = useScheduledQueryGroupQueryForm({
uniqueQueryIds,
defaultValue,
handleSubmit: (payload, isValid) =>
new Promise((resolve) => {
@ -65,7 +68,7 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
}),
});
/* Platform and version fields are supported since osquer_manger@0.3.0 */
/* Platform and version fields are supported since osquery_manager@0.3.0 */
const isFieldSupported = useMemo(
() => (integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.3.0') : false),
[integrationPackageVersion]

View file

@ -12,15 +12,19 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { FIELD_TYPES } from '../../shared_imports';
import { idFieldValidations, intervalFieldValidation, queryFieldValidation } from './validations';
import {
createIdFieldValidations,
intervalFieldValidation,
queryFieldValidation,
} from './validations';
export const formSchema = {
export const createFormSchema = (ids: Set<string>) => ({
id: {
type: FIELD_TYPES.TEXT,
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.idFieldLabel', {
defaultMessage: 'ID',
}),
validations: idFieldValidations.map((validator) => ({ validator })),
validations: createIdFieldValidations(ids).map((validator) => ({ validator })),
},
description: {
type: FIELD_TYPES.TEXT,
@ -69,4 +73,4 @@ export const formSchema = {
) as unknown) as string,
validations: [],
},
};
});

View file

@ -5,17 +5,19 @@
* 2.0.
*/
import { isArray } from 'lodash';
import { isArray, xor } from 'lodash';
import uuid from 'uuid';
import { produce } from 'immer';
import { useMemo } from 'react';
import { FormConfig, useForm } from '../../shared_imports';
import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
import { formSchema } from './schema';
import { createFormSchema } from './schema';
const FORM_ID = 'editQueryFlyoutForm';
export interface UseScheduledQueryGroupQueryFormProps {
uniqueQueryIds: string[];
defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined;
handleSubmit: FormConfig<
OsqueryManagerPackagePolicyConfigRecord,
@ -32,12 +34,26 @@ export interface ScheduledQueryGroupFormData {
}
export const useScheduledQueryGroupQueryForm = ({
uniqueQueryIds,
defaultValue,
handleSubmit,
}: UseScheduledQueryGroupQueryFormProps) =>
useForm<OsqueryManagerPackagePolicyConfigRecord, ScheduledQueryGroupFormData>({
}: UseScheduledQueryGroupQueryFormProps) => {
const idSet = useMemo<Set<string>>(
() =>
new Set<string>(xor(uniqueQueryIds, defaultValue?.id.value ? [defaultValue.id.value] : [])),
[uniqueQueryIds, defaultValue]
);
const formSchema = useMemo<ReturnType<typeof createFormSchema>>(() => createFormSchema(idSet), [
idSet,
]);
return useForm<OsqueryManagerPackagePolicyConfigRecord, ScheduledQueryGroupFormData>({
id: FORM_ID + uuid.v4(),
onSubmit: handleSubmit,
onSubmit: async (formData, isValid) => {
if (isValid && handleSubmit) {
return handleSubmit(formData, isValid);
}
},
options: {
stripEmptyFields: false,
},
@ -75,3 +91,4 @@ export const useScheduledQueryGroupQueryForm = ({
},
schema: formSchema,
});
};

View file

@ -23,13 +23,28 @@ const idSchemaValidation: ValidationFunc<any, string, string> = ({ value }) => {
}
};
export const idFieldValidations = [
const createUniqueIdValidation = (ids: Set<string>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const uniqueIdCheck: ValidationFunc<any, string, string> = ({ value }) => {
if (ids.has(value)) {
return {
message: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.uniqueIdError', {
defaultMessage: 'ID must be unique',
}),
};
}
};
return uniqueIdCheck;
};
export const createIdFieldValidations = (ids: Set<string>) => [
fieldValidators.emptyField(
i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.emptyIdError', {
defaultMessage: 'ID is required',
})
),
idSchemaValidation,
createUniqueIdValidation(ids),
];
export const intervalFieldValidation: ValidationFunc<

View file

@ -31,7 +31,7 @@ export const useScheduledQueryGroup = ({
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
{
keepPreviousData: true,
enabled: !skip,
enabled: !skip || !scheduledQueryGroupId,
select: (response) => response.item,
}
);

View file

@ -11,7 +11,7 @@ import { usageMetricSavedObjectType } from '../../../common/types';
import {
CounterValue,
createMetricObjects,
getOrCreateMetricObject,
getRouteMetric,
incrementCount,
RouteString,
@ -45,31 +45,22 @@ describe('Usage metric recorder', () => {
get.mockClear();
create.mockClear();
});
it('should seed route metrics objects', async () => {
it('should create metrics that do not exist', async () => {
get.mockRejectedValueOnce('stub value');
create.mockReturnValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query');
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls);
expect(result).toBe(true);
expect(result).toBe('stub value');
});
it('should handle previously seeded objects properly', async () => {
it('should handle previously created objects properly', async () => {
get.mockReturnValueOnce('stub value');
create.mockRejectedValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query');
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls, []);
expect(result).toBe(true);
});
it('should report failure to create the metrics object', async () => {
get.mockRejectedValueOnce('stub value');
create.mockRejectedValueOnce('stub value');
const result = await createMetricObjects(savedObjectsClient);
checkGetCalls(get.mock.calls);
checkCreateCalls(create.mock.calls);
expect(result).toBe(false);
expect(result).toBe('stub value');
});
});

View file

@ -18,30 +18,28 @@ export type RouteString = 'live_query';
export const routeStrings: RouteString[] = ['live_query'];
export async function createMetricObjects(soClient: SavedObjectsClientContract) {
const res = await Promise.allSettled(
routeStrings.map(async (route) => {
try {
await soClient.get(usageMetricSavedObjectType, route);
} catch (e) {
await soClient.create(
usageMetricSavedObjectType,
{
errors: 0,
count: 0,
},
{
id: route,
}
);
export async function getOrCreateMetricObject<T>(
soClient: SavedObjectsClientContract,
route: string
) {
try {
return await soClient.get<T>(usageMetricSavedObjectType, route);
} catch (e) {
return await soClient.create(
usageMetricSavedObjectType,
{
errors: 0,
count: 0,
},
{
id: route,
}
})
);
return !res.some((e) => e.status === 'rejected');
);
}
}
export async function getCount(soClient: SavedObjectsClientContract, route: RouteString) {
return await soClient.get<LiveQuerySessionUsage>(usageMetricSavedObjectType, route);
return await getOrCreateMetricObject<LiveQuerySessionUsage>(soClient, route);
}
export interface CounterValue {
@ -55,7 +53,7 @@ export async function incrementCount(
key: keyof CounterValue = 'count',
increment = 1
) {
const metric = await soClient.get<CounterValue>(usageMetricSavedObjectType, route);
const metric = await getOrCreateMetricObject<CounterValue>(soClient, route);
metric.attributes[key] += increment;
await soClient.update(usageMetricSavedObjectType, route, metric.attributes);
}

View file

@ -7,7 +7,6 @@
import { CoreSetup, SavedObjectsClient } from '../../../../../src/core/server';
import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server';
import { createMetricObjects } from '../routes/usage';
import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers';
import { CollectorDependencies, usageSchema, UsageData } from './types';
@ -25,10 +24,7 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa
const collector = usageCollection.makeUsageCollector<UsageData>({
type: 'osquery',
schema: usageSchema,
isReady: async () => {
const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core));
return await createMetricObjects(savedObjectsClient);
},
isReady: () => true,
fetch: async ({ esClient }: CollectorFetchContext): Promise<UsageData> => {
const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core));
return {

View file

@ -45,6 +45,11 @@ export async function getPolicyLevelUsage(
const agentResponse = await esClient.search({
body: {
size: 0,
query: {
match: {
active: true,
},
},
aggs: {
policied: {
filter: {
@ -87,7 +92,8 @@ export function getScheduledQueryUsage(packagePolicies: ListResult<PackagePolicy
return packagePolicies.items.reduce(
(acc, item) => {
++acc.queryGroups.total;
if (item.inputs.length === 0) {
const policyAgents = item.inputs.reduce((sum, input) => sum + input.streams.length, 0);
if (policyAgents === 0) {
++acc.queryGroups.empty;
}
return acc;