[Osquery] Add Osquery tab to node details tabs (#104272)

This commit is contained in:
Patryk Kopyciński 2021-08-14 14:25:24 +03:00 committed by GitHub
parent a9844db461
commit 9d0a7b8394
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 377 additions and 192 deletions

View file

@ -15,7 +15,7 @@
"observability",
"ruleRegistry"
],
"optionalPlugins": ["ml", "home", "embeddable"],
"optionalPlugins": ["ml", "home", "embeddable", "osquery"],
"server": true,
"ui": true,
"configPath": ["xpack", "infra"],

View file

@ -20,6 +20,7 @@ import { LogsTab } from './tabs/logs';
import { ProcessesTab } from './tabs/processes';
import { PropertiesTab } from './tabs/properties/index';
import { AnomaliesTab } from './tabs/anomalies/anomalies';
import { OsqueryTab } from './tabs/osquery';
import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared';
import { useLinkProps } from '../../../../../hooks/use_link_props';
import { getNodeDetailUrl } from '../../../../link_to';
@ -45,7 +46,7 @@ export const NodeContextPopover = ({
openAlertFlyout,
}: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab];
const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab, OsqueryTab];
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const uiCapabilities = useKibana().services.application?.capabilities;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiLoadingContent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext, useMemo } from 'react';
import { useKibanaContextForPlugin } from '../../../../../../../hooks/use_kibana';
import { TabContent, TabProps } from '../shared';
import { Source } from '../../../../../../../containers/metrics_source';
import { findInventoryModel } from '../../../../../../../../common/inventory_models';
import { InventoryItemType } from '../../../../../../../../common/inventory_models/types';
import { useMetadata } from '../../../../../metric_detail/hooks/use_metadata';
import { useWaffleTimeContext } from '../../../../hooks/use_waffle_time';
const TabComponent = (props: TabProps) => {
const nodeId = props.node.id;
const nodeType = props.nodeType as InventoryItemType;
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useContext(Source.Context);
const { currentTimeRange } = useWaffleTimeContext();
const { loading, metadata } = useMetadata(
nodeId,
nodeType,
inventoryModel.requiredMetrics,
sourceId,
currentTimeRange
);
const {
services: { osquery },
} = useKibanaContextForPlugin();
// @ts-expect-error
const OsqueryAction = osquery?.OsqueryAction;
// avoids component rerender when resizing the popover
const content = useMemo(() => {
// TODO: Add info when Osquery plugin is not available
if (loading || !OsqueryAction) {
return (
<TabContent>
<EuiLoadingContent lines={10} />
</TabContent>
);
}
return (
<TabContent>
<OsqueryAction metadata={metadata} />
</TabContent>
);
}, [OsqueryAction, loading, metadata]);
return content;
};
export const OsqueryTab = {
id: 'osquery',
name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', {
defaultMessage: 'Osquery',
}),
content: TabComponent,
};

View file

@ -23,6 +23,7 @@ import type {
ObservabilityPublicSetup,
ObservabilityPublicStart,
} from '../../observability/public';
// import type { OsqueryPluginStart } from '../../osquery/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { MlPluginStart, MlPluginSetup } from '../../ml/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
@ -50,6 +51,7 @@ export interface InfraClientStartDeps {
usageCollection: UsageCollectionStart;
ml: MlPluginStart;
embeddable?: EmbeddableStart;
osquery?: unknown; // OsqueryPluginStart;
}
export type InfraClientCoreSetup = CoreSetup<InfraClientStartDeps, InfraClientStartExports>;

View file

@ -9,12 +9,14 @@
"id": "osquery",
"kibanaVersion": "kibana",
"optionalPlugins": [
"fleet",
"home",
"usageCollection",
"lens"
],
"requiredBundles": [
"esUiShared",
"fleet",
"kibanaUtils",
"kibanaReact"
],
@ -24,7 +26,6 @@
"dataEnhanced",
"discover",
"features",
"fleet",
"navigation",
"triggersActionsUi",
"security"

View file

@ -12,11 +12,12 @@ import { useKibana } from '../common/lib/kibana';
import { useErrorToast } from '../common/hooks/use_error_toast';
interface UseAgentPolicy {
policyId: string;
policyId?: string;
silent?: boolean;
skip?: boolean;
}
export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
export const useAgentPolicy = ({ policyId, skip, silent }: UseAgentPolicy) => {
const { http } = useKibana().services;
const setErrorToast = useErrorToast();
@ -24,11 +25,12 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
['agentPolicy', { policyId }],
() => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`),
{
enabled: !skip,
enabled: !!(policyId && !skip),
keepPreviousData: true,
select: (response) => response.item,
onSuccess: () => setErrorToast(),
onError: (error: Error) =>
!silent &&
setErrorToast(error, {
title: i18n.translate('xpack.osquery.agent_policy_details.fetchError', {
defaultMessage: 'Error while fetching agent policy details',

View file

@ -19,7 +19,7 @@ interface AgentIdToNameProps {
const AgentIdToNameComponent: React.FC<AgentIdToNameProps> = ({ agentId }) => {
const getUrlForApp = useKibana().services.application.getUrlForApp;
const { data } = useAgentDetails({ agentId });
const { data } = useAgentDetails({ agentId, skip: !agentId });
return (
<EuiLink
@ -29,7 +29,7 @@ const AgentIdToNameComponent: React.FC<AgentIdToNameProps> = ({ agentId }) => {
})}
target="_blank"
>
{data?.item.local_metadata.host.name ?? agentId}
{data?.local_metadata.host.name ?? agentId}
</EuiLink>
);
};

View file

@ -13,19 +13,24 @@ import { useErrorToast } from '../common/hooks/use_error_toast';
import { useKibana } from '../common/lib/kibana';
interface UseAgentDetails {
agentId: string;
agentId?: string;
silent?: boolean;
skip?: boolean;
}
export const useAgentDetails = ({ agentId }: UseAgentDetails) => {
export const useAgentDetails = ({ agentId, silent, skip }: UseAgentDetails) => {
const { http } = useKibana().services;
const setErrorToast = useErrorToast();
return useQuery<GetOneAgentResponse>(
return useQuery<GetOneAgentResponse, unknown, GetOneAgentResponse['item']>(
['agentDetails', agentId],
() => http.get(`/internal/osquery/fleet_wrapper/agents/${agentId}`),
{
enabled: agentId.length > 0,
enabled: !skip,
retry: false,
select: (response: GetOneAgentResponse) => response?.item,
onSuccess: () => setErrorToast(),
onError: (error) =>
!silent &&
setErrorToast(error as Error, {
title: i18n.translate('xpack.osquery.agentDetails.fetchError', {
defaultMessage: 'Error while fetching agent details',

View file

@ -11,7 +11,7 @@ import { useQuery } from 'react-query';
import { useKibana } from '../lib/kibana';
import { useErrorToast } from './use_error_toast';
export const useOsqueryIntegration = () => {
export const useOsqueryIntegrationStatus = () => {
const { http } = useKibana().services;
const setErrorToast = useErrorToast();

View file

@ -13,13 +13,13 @@ import { INTEGRATIONS_PLUGIN_ID } from '../../../fleet/common';
import { pagePathGetters } from '../../../fleet/public';
import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kibana';
import { useOsqueryIntegration } from '../common/hooks';
import { useOsqueryIntegrationStatus } from '../common/hooks';
const ManageIntegrationLinkComponent = () => {
const {
application: { getUrlForApp, navigateToApp },
} = useKibana().services;
const { data: osqueryIntegration } = useOsqueryIntegration();
const { data: osqueryIntegration } = useOsqueryIntegrationStatus();
const integrationHref = useMemo(() => {
if (osqueryIntegration) {

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiIcon, EuiIconProps } from '@elastic/eui';
import OsqueryLogo from './osquery.svg';
export type OsqueryIconProps = Omit<EuiIconProps, 'type'>;
const OsqueryIconComponent: React.FC<OsqueryIconProps> = (props) => (
<EuiIcon size="xl" type={OsqueryLogo} {...props} />
);
export const OsqueryIcon = React.memo(OsqueryIconComponent);

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M255.214617,0.257580247 L255.214617,63.993679 L191.609679,127.598617 L191.609679,63.7297778 L255.214617,0.257580247" fill="#A596FF"></path>
<path d="M128.006321,0.257580247 L128.006321,63.993679 L191.611259,127.598617 L191.611259,63.7297778 L128.006321,0.257580247" fill="#000000"></path>
<path d="M255.345778,254.803753 L191.609679,254.803753 L128.004741,191.198815 L191.872,191.198815 L255.345778,254.803753" fill="#A596FF"></path>
<path d="M255.345778,127.595457 L191.609679,127.595457 L128.004741,191.200395 L191.872,191.200395 L255.345778,127.595457" fill="#000000"></path>
<path d="M0.801185185,254.936494 L0.801185185,191.198815 L64.4061235,127.593877 L64.4061235,191.462716 L0.801185185,254.936494" fill="#A596FF"></path>
<path d="M128.009481,254.936494 L128.009481,191.198815 L64.4045432,127.593877 L64.4045432,191.462716 L128.009481,254.936494" fill="#000000"></path>
<path d="M0.671604938,0.385580247 L64.4077037,0.385580247 L128.012642,63.9905185 L64.1453827,63.9905185 L0.671604938,0.385580247" fill="#A596FF"></path>
<path d="M0.671604938,127.593877 L64.4077037,127.593877 L128.012642,63.9889383 L64.1453827,63.9889383 L0.671604938,127.593877" fill="#000000"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -28,13 +28,13 @@ interface OsqueryEditorProps {
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({
defaultValue,
disabled,
// disabled,
onChange,
}) => (
<EuiCodeEditor
value={defaultValue}
mode="osquery"
isReadOnly={disabled}
// isReadOnly={disabled}
theme="tomorrow"
onChange={onChange}
name="osquery_editor"

View file

@ -16,12 +16,10 @@ import {
agentPolicyRouteService,
AgentPolicy,
PLUGIN_ID,
INTEGRATIONS_PLUGIN_ID,
NewPackagePolicy,
} from '../../../fleet/common';
import {
pagePathGetters,
CreatePackagePolicyRouteState,
PackagePolicyCreateExtensionComponentProps,
PackagePolicyEditExtensionComponentProps,
} from '../../../fleet/public';
@ -49,7 +47,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
http,
} = useKibana().services;
const { state: locationState } = useLocation();
const { replace, go } = useHistory();
const { go } = useHistory();
const agentsLinkHref = useMemo(() => {
if (!policy?.policy_id) return '#';
@ -126,27 +124,28 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!editMode) {
replace({
state: {
onSaveNavigateTo: (newPackagePolicy) => [
INTEGRATIONS_PLUGIN_ID,
{
path:
'#' +
pagePathGetters.integration_policy_edit({
packagePolicyId: newPackagePolicy.id,
})[1],
state: {
forceRefresh: true,
},
},
],
} as CreatePackagePolicyRouteState,
});
}
}, [editMode, replace]);
// TODO: Find a better solution
// useEffect(() => {
// if (!editMode) {
// replace({
// state: {
// onSaveNavigateTo: (newPackagePolicy) => [
// INTEGRATIONS_PLUGIN_ID,
// {
// path:
// '#' +
// pagePathGetters.integration_policy_edit({
// packagePolicyId: newPackagePolicy.id,
// })[1],
// state: {
// forceRefresh: true,
// },
// },
// ],
// } as CreatePackagePolicyRouteState,
// });
// }
// }, [editMode, replace]);
const scheduledQueryGroupTableData = useMemo(() => {
const policyWithoutEmptyQueries = produce<

View file

@ -37,15 +37,15 @@ export const MAX_QUERY_LENGTH = 2000;
const GhostFormField = () => <></>;
interface LiveQueryFormProps {
agentId?: string | undefined;
defaultValue?: Partial<FormData> | undefined;
onSuccess?: () => void;
singleAgentMode?: boolean;
}
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
agentId,
defaultValue,
onSuccess,
singleAgentMode,
}) => {
const permissions = useKibana().services.application.capabilities.osquery;
const { http } = useKibana().services;
@ -55,14 +55,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
const handleShowSaveQueryFlout = useCallback(() => setShowSavedQueryFlyout(true), []);
const handleCloseSaveQueryFlout = useCallback(() => setShowSavedQueryFlyout(false), []);
const {
data,
isLoading,
mutateAsync,
isError,
isSuccess,
// error
} = useMutation(
const { data, isLoading, mutateAsync, isError, isSuccess } = useMutation(
(payload: Record<string, unknown>) =>
http.post('/internal/osquery/action', {
body: JSON.stringify(payload),
@ -173,7 +166,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
/>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd">
{!agentId && (
{!singleAgentMode && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
disabled={
@ -203,13 +196,13 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
</>
),
[
agentId,
agentSelected,
permissions.writeSavedQueries,
handleShowSaveQueryFlout,
queryComponentProps,
queryValueProvided,
resultsStatus,
singleAgentMode,
submit,
]
);
@ -262,7 +255,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
return (
<>
<Form form={form}>{agentId ? singleAgentForm : <EuiSteps steps={formSteps} />}</Form>
<Form form={form}>{singleAgentMode ? singleAgentForm : <EuiSteps steps={formSteps} />}</Form>
{showSavedQueryFlyout ? (
<SavedQueryFlyout
onClose={handleCloseSaveQueryFlout}

View file

@ -5,18 +5,69 @@
* 2.0.
*/
import React from 'react';
import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
import React, { useMemo } from 'react';
import { LiveQueryForm } from './form';
import { FormData } from '../shared_imports';
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
import { OsqueryIcon } from '../components/osquery_icon';
interface LiveQueryProps {
defaultValue?: Partial<FormData> | undefined;
agentId?: string;
agentPolicyId?: string;
onSuccess?: () => void;
query?: string;
}
const LiveQueryComponent: React.FC<LiveQueryProps> = ({ defaultValue, onSuccess }) => (
<LiveQueryForm defaultValue={defaultValue} onSuccess={onSuccess} />
);
const LiveQueryComponent: React.FC<LiveQueryProps> = ({
agentId,
agentPolicyId,
onSuccess,
query,
}) => {
const { data: hasActionResultsPrivileges, isFetched } = useActionResultsPrivileges();
const defaultValue = useMemo(() => {
if (agentId || agentPolicyId || query) {
return {
agentSelection: {
allAgentsSelected: false,
agents: agentId ? [agentId] : [],
platformsSelected: [],
policiesSelected: agentPolicyId ? [agentPolicyId] : [],
},
query,
};
}
return undefined;
}, [agentId, agentPolicyId, query]);
if (!isFetched) {
return <EuiLoadingContent lines={10} />;
}
if (!hasActionResultsPrivileges) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Permission denied</h2>}
titleSize="xs"
body={
<p>
To view query results, ask your administrator to update your user role to have index{' '}
<EuiCode>read</EuiCode> privileges on the{' '}
<EuiCode>logs-{OSQUERY_INTEGRATION_NAME}.result*</EuiCode> index.
</p>
}
/>
);
}
return (
<LiveQueryForm singleAgentMode={!!agentId} defaultValue={defaultValue} onSuccess={onSuccess} />
);
};
export const LiveQuery = React.memo(LiveQueryComponent);

View file

@ -5,6 +5,14 @@
* 2.0.
*/
import { QueryClient } from 'react-query';
/* eslint-disable @typescript-eslint/no-empty-function */
import { QueryClient, setLogger } from 'react-query';
setLogger({
log: () => {},
warn: () => {},
error: () => {},
});
export const queryClient = new QueryClient();

View file

@ -203,6 +203,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
const toolbarVisibility = useMemo(
() => ({
showStyleSelector: false,
additionalControls: (
<>
<ViewResultsInDiscoverAction
@ -247,8 +248,11 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
return (
<EuiCallOut title="Missing privileges" color="danger" iconType="alert">
<p>
You&apos;re missing <EuiCode>read</EuiCode> privileges to read from
<EuiCode>logs-{OSQUERY_INTEGRATION_NAME}.result*</EuiCode>.
{'Your user role doesnt have index read permissions on the '}
<EuiCode>logs-{OSQUERY_INTEGRATION_NAME}.result*</EuiCode>
{
'index. Access to this index is required to view osquery results. Administrators can update role permissions in Stack Management > Roles.'
}
</p>
</EuiCallOut>
);

View file

@ -7,7 +7,7 @@
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import qs from 'query-string';
@ -22,28 +22,20 @@ const NewLiveQueryPageComponent = () => {
const { replace } = useHistory();
const location = useLocation();
const liveQueryListProps = useRouterNavigate('live_queries');
const [initialQuery, setInitialQuery] = useState<string | undefined>(undefined);
const formDefaultValue = useMemo(() => {
const agentPolicyId = useMemo(() => {
const queryParams = qs.parse(location.search);
return queryParams?.agentPolicyId as string | undefined;
}, [location.search]);
useEffect(() => {
if (location.state?.form.query) {
replace({ state: null });
return { query: location.state?.form.query };
setInitialQuery(location.state?.form.query);
}
if (queryParams?.agentPolicyId) {
return {
agentSelection: {
allAgentsSelected: false,
agents: [],
platformsSelected: [],
policiesSelected: [queryParams?.agentPolicyId],
},
};
}
return undefined;
}, [location.search, location.state, replace]);
}, [location.state?.form.query, replace]);
const LeftColumn = useMemo(
() => (
@ -74,7 +66,7 @@ const NewLiveQueryPageComponent = () => {
return (
<WithHeaderLayout leftColumn={LeftColumn}>
<LiveQuery defaultValue={formDefaultValue} />
<LiveQuery agentPolicyId={agentPolicyId} query={initialQuery} />
</WithHeaderLayout>
);
};

View file

@ -12,14 +12,14 @@ import React, { useMemo } from 'react';
import { WithHeaderLayout } from '../../../components/layouts';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { ScheduledQueryGroupForm } from '../../../scheduled_query_groups/form';
import { useOsqueryIntegration } from '../../../common/hooks';
import { useOsqueryIntegrationStatus } from '../../../common/hooks';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
const AddScheduledQueryGroupPageComponent = () => {
useBreadcrumbs('scheduled_query_group_add');
const scheduledQueryListProps = useRouterNavigate('scheduled_query_groups');
const { data: osqueryIntegration } = useOsqueryIntegration();
const { data: osqueryIntegration } = useOsqueryIntegrationStatus();
const packageInfo = useMemo(() => {
if (!osqueryIntegration) return;

View file

@ -15,7 +15,6 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
TypedLensByValueInput,
@ -27,6 +26,20 @@ import { useKibana, isModifiedEvent, isLeftClickEvent } from '../common/lib/kiba
import { PlatformIcons } from './queries/platforms';
import { OsqueryManagerPackagePolicyInputStream } from '../../common/types';
const VIEW_IN_DISCOVER = i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View in Discover',
}
);
const VIEW_IN_LENS = i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View in Lens',
}
);
export enum ViewResultsActionButtonType {
icon = 'icon',
button = 'button',
@ -182,33 +195,18 @@ const ViewResultsInLensActionComponent: React.FC<ViewResultsInDiscoverActionProp
onClick={handleClick}
disabled={!lensService?.canUseEditor()}
>
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel"
defaultMessage="View results in Lens"
/>
{VIEW_IN_LENS}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip
content={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View results in Lens',
}
)}
>
<EuiToolTip content={VIEW_IN_LENS}>
<EuiButtonIcon
iconType="lensApp"
disabled={!lensService?.canUseEditor()}
onClick={handleClick}
aria-label={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel',
{
defaultMessage: 'View results in Lens',
}
)}
aria-label={VIEW_IN_LENS}
/>
</EuiToolTip>
);
@ -271,33 +269,14 @@ const ViewResultsInDiscoverActionComponent: React.FC<ViewResultsInDiscoverAction
if (buttonType === ViewResultsActionButtonType.button) {
return (
<EuiButtonEmpty size="xs" iconType="discoverApp" href={discoverUrl}>
<FormattedMessage
id="xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel"
defaultMessage="View results in Discover"
/>
{VIEW_IN_DISCOVER}
</EuiButtonEmpty>
);
}
return (
<EuiToolTip
content={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View results in Discover',
}
)}
>
<EuiButtonIcon
iconType="discoverApp"
href={discoverUrl}
aria-label={i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel',
{
defaultMessage: 'View results in Discover',
}
)}
/>
<EuiToolTip content={VIEW_IN_DISCOVER}>
<EuiButtonIcon iconType="discoverApp" href={discoverUrl} aria-label={VIEW_IN_DISCOVER} />
</EuiToolTip>
);
};

View file

@ -5,79 +5,129 @@
* 2.0.
*/
import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { find } from 'lodash';
import { EuiErrorBoundary, EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import React, { useMemo } from 'react';
import { QueryClientProvider } from 'react-query';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { useAgentDetails } from '../../agents/use_agent_details';
import { useAgentPolicy } from '../../agent_policies';
import { KibanaContextProvider, useKibana } from '../../common/lib/kibana';
import { LiveQueryForm } from '../../live_queries/form';
import { LiveQuery } from '../../live_queries';
import { queryClient } from '../../query_client';
import { OsqueryIcon } from '../../components/osquery_icon';
interface OsqueryActionProps {
hostId?: string | undefined;
metadata?: {
info: {
agent: { id: string };
};
};
}
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ hostId }) => {
const [agentId, setAgentId] = useState<string>();
const { indexPatterns, search } = useKibana().services.data;
useEffect(() => {
if (hostId) {
const findAgent = async () => {
const searchSource = await search.searchSource.create();
const indexPattern = await indexPatterns.find('.fleet-agents');
searchSource.setField('index', indexPattern[0]);
searchSource.setField('filter', [
{
meta: {
alias: null,
disabled: false,
negate: false,
key: 'local_metadata.host.id',
value: hostId,
},
query: {
match_phrase: {
'local_metadata.host.id': hostId,
},
},
},
{
meta: {
alias: null,
disabled: false,
negate: false,
key: 'active',
value: 'true',
},
query: {
match_phrase: {
active: 'true',
},
},
},
]);
const response = await searchSource.fetch$().toPromise();
if (response.rawResponse.hits.hits.length && response.rawResponse.hits.hits[0]._id) {
setAgentId(response.rawResponse.hits.hits[0]._id);
}
};
findAgent();
}
const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ metadata }) => {
const permissions = useKibana().services.application.capabilities.osquery;
const agentId = metadata?.info?.agent?.id ?? undefined;
const { data: agentData, isFetched: agentFetched } = useAgentDetails({
agentId,
silent: true,
skip: !agentId,
});
const {
data: agentPolicyData,
isFetched: policyFetched,
isError: policyError,
isLoading: policyLoading,
} = useAgentPolicy({
policyId: agentData?.policy_id,
skip: !agentData,
silent: true,
});
if (!agentId) {
const osqueryAvailable = useMemo(() => {
if (policyError) return false;
const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [
'package.name',
OSQUERY_INTEGRATION_NAME,
]);
return osqueryPackageInstalled?.enabled;
}, [agentPolicyData?.package_policies, policyError]);
if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Permissions denied</h2>}
titleSize="xs"
body={
<p>
To access this page, ask your administrator for <EuiCode>osquery</EuiCode> Kibana
privileges.
</p>
}
/>
);
}
if (!agentFetched) {
return <EuiLoadingContent lines={10} />;
}
return (
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
<LiveQueryForm defaultValue={{ agentSelection: { agents: [agentId] } }} agentId={agentId} />
);
if (!agentId || (agentFetched && !agentData)) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
titleSize="xs"
body={
<p>
An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on
the host, and then add the Osquery Manager integration to the agent policy in Fleet.
</p>
}
/>
);
}
if (!policyFetched && policyLoading) {
return <EuiLoadingContent lines={10} />;
}
if (!osqueryAvailable) {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
titleSize="xs"
body={
<p>
The Osquery Manager integration is not added to the agent policy. To run queries on the
host, add the Osquery Manager integration to the agent policy in Fleet.
</p>
}
/>
);
}
if (agentData?.status !== 'online') {
return (
<EuiEmptyPrompt
icon={<OsqueryIcon />}
title={<h2>Osquery is not available</h2>}
titleSize="xs"
body={
<p>
To run queries on this host, the Elastic Agent must be active. Check the status of this
agent in Fleet.
</p>
}
/>
);
}
return <LiveQuery agentId={agentId} />;
};
export const OsqueryAction = React.memo(OsqueryActionComponent);

View file

@ -22,10 +22,16 @@ export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryApp
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asInternalUser;
const agent = await osqueryContext.service
.getAgentService()
// @ts-expect-error update types
?.getAgent(esClient, request.params.id);
let agent;
try {
agent = await osqueryContext.service
.getAgentService()
// @ts-expect-error update types
?.getAgent(esClient, request.params.id);
} catch (err) {
return response.notFound();
}
return response.ok({ body: { item: agent } });
}

View file

@ -18724,8 +18724,6 @@
"xpack.osquery.scheduledQueryGroup.queriesTable.platformColumnTitle": "プラットフォーム",
"xpack.osquery.scheduledQueryGroup.queriesTable.queryColumnTitle": "クエリ",
"xpack.osquery.scheduledQueryGroup.queriesTable.versionColumnTitle": "最低Osqueryバージョン",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel": "Discoverで結果を表示",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel": "Lensで結果を表示",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewResultsColumnTitle": "結果を表示",
"xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel": "キャンセル",
"xpack.osquery.scheduledQueryGroup.queryFlyoutForm.descriptionFieldLabel": "説明",

View file

@ -19138,8 +19138,6 @@
"xpack.osquery.scheduledQueryGroup.queriesTable.platformColumnTitle": "平台",
"xpack.osquery.scheduledQueryGroup.queriesTable.queryColumnTitle": "查询",
"xpack.osquery.scheduledQueryGroup.queriesTable.versionColumnTitle": "最低 Osquery 版本",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewDiscoverResultsActionAriaLabel": "在 Discover 中查看结果",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewLensResultsActionAriaLabel": "在 Lens 中查看结果",
"xpack.osquery.scheduledQueryGroup.queriesTable.viewResultsColumnTitle": "查看结果",
"xpack.osquery.scheduledQueryGroup.queryFlyoutForm.cancelButtonLabel": "取消",
"xpack.osquery.scheduledQueryGroup.queryFlyoutForm.descriptionFieldLabel": "描述",