[Osquery] RBAC (#106669)

This commit is contained in:
Patryk Kopyciński 2021-08-10 17:36:27 +03:00 committed by GitHub
parent 5a92a7ef31
commit 9edcf9e71e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 1136 additions and 357 deletions

View file

@ -43,6 +43,8 @@ export const REMOVED_TYPES: string[] = [
'server',
// https://github.com/elastic/kibana/issues/95617
'tsvb-validation-telemetry',
// replaced by osquery-manager-usage-metric
'osquery-usage-metric',
].sort();
// When migrating from the outdated index we use a read query which excludes

View file

@ -72,6 +72,7 @@ const previouslyRegisteredTypes = [
'monitoring-telemetry',
'osquery-saved-query',
'osquery-usage-metric',
'osquery-manager-usage-metric',
'query',
'sample-data-telemetry',
'search',

View file

@ -44,7 +44,7 @@ export const getAgentUsage = async (
error,
offline,
updating,
} = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient);
} = await AgentService.getAgentStatusForAgentPolicy(esClient);
return {
total_enrolled: total,
healthy: online,

View file

@ -76,7 +76,6 @@ export const getFleetServerUsage = async (
}
const { total, inactive, online, error, updating, offline } = await getAgentStatusForAgentPolicy(
soClient,
esClient,
undefined,
Array.from(policyIds)

View file

@ -101,6 +101,7 @@ export const createMockAgentPolicyService = (): jest.Mocked<AgentPolicyServiceIn
export const createMockAgentService = (): jest.Mocked<AgentService> => {
return {
getAgentStatusById: jest.fn(),
getAgentStatusForAgentPolicy: jest.fn(),
authenticateAgentWithAccessToken: jest.fn(),
getAgent: jest.fn(),
listAgents: jest.fn(),

View file

@ -72,6 +72,7 @@ import {
} from './services';
import {
getAgentStatusById,
getAgentStatusForAgentPolicy,
authenticateAgentWithAccessToken,
getAgentsByKuery,
getAgentById,
@ -309,6 +310,7 @@ export class FleetPlugin
getAgent: getAgentById,
listAgents: getAgentsByKuery,
getAgentStatusById,
getAgentStatusForAgentPolicy,
authenticateAgentWithAccessToken,
},
agentPolicyService: {

View file

@ -202,13 +202,11 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler<
undefined,
TypeOf<typeof GetAgentStatusRequestSchema.query>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const esClient = context.core.elasticsearch.client.asCurrentUser;
try {
// TODO change path
const results = await AgentService.getAgentStatusForAgentPolicy(
soClient,
esClient,
request.query.policyId,
request.query.kuery

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
import type { ElasticsearchClient } from 'src/core/server';
import pMap from 'p-map';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
@ -49,7 +49,6 @@ function joinKuerys(...kuerys: Array<string | undefined>) {
}
export async function getAgentStatusForAgentPolicy(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentPolicyId?: string,
filterKuery?: string

View file

@ -10,6 +10,8 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/ser
import type { AgentStatus, Agent } from '../types';
import type { GetAgentStatusResponse } from '../../common';
import type { getAgentById, getAgentsByKuery } from './agents';
import type { agentPolicyService } from './agent_policy';
import * as settingsService from './settings';
@ -56,6 +58,14 @@ export interface AgentService {
* Return the status by the Agent's id
*/
getAgentStatusById(esClient: ElasticsearchClient, agentId: string): Promise<AgentStatus>;
/**
* Return the status by the Agent's Policy id
*/
getAgentStatusForAgentPolicy(
esClient: ElasticsearchClient,
agentPolicyId?: string,
filterKuery?: string
): Promise<GetAgentStatusResponse['results']>;
/**
* List agents
*/

View file

@ -9,8 +9,11 @@ import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../
export const savedQuerySavedObjectType = 'osquery-saved-query';
export const packSavedObjectType = 'osquery-pack';
export const usageMetricSavedObjectType = 'osquery-usage-metric';
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack' | 'osquery-usage-metric';
export const usageMetricSavedObjectType = 'osquery-manager-usage-metric';
export type SavedObjectType =
| 'osquery-saved-query'
| 'osquery-pack'
| 'osquery-manager-usage-metric';
/**
* This makes any optional property the same as Required<T> would but also has the

View file

@ -15,6 +15,7 @@ import { AgentIdToName } from '../agents/agent_id_to_name';
import { useActionResults } from './use_action_results';
import { useAllResults } from '../results/use_all_results';
import { Direction } from '../../common/search_strategy';
import { useActionResultsPrivileges } from './use_action_privileges';
interface ActionResultsSummaryProps {
actionId: string;
@ -41,6 +42,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
expirationDate,
]);
const [isLive, setIsLive] = useState(true);
const { data: hasActionResultsPrivileges } = useActionResultsPrivileges();
const {
// @ts-expect-error update types
data: { aggregations, edges },
@ -52,6 +54,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
direction: Direction.asc,
sortField: '@timestamp',
isLive,
skip: !hasActionResultsPrivileges,
});
if (expired) {
// @ts-expect-error update types
@ -77,6 +80,7 @@ const ActionResultsSummaryComponent: React.FC<ActionResultsSummaryProps> = ({
},
],
isLive,
skip: !hasActionResultsPrivileges,
});
const renderAgentIdColumn = useCallback((agentId) => <AgentIdToName agentId={agentId} />, []);

View file

@ -0,0 +1,33 @@
/*
* 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 { useQuery } from 'react-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../common/lib/kibana';
import { useErrorToast } from '../common/hooks/use_error_toast';
export const useActionResultsPrivileges = () => {
const { http } = useKibana().services;
const setErrorToast = useErrorToast();
return useQuery(
['actionResultsPrivileges'],
() => http.get('/internal/osquery/privileges_check'),
{
keepPreviousData: true,
select: (response) => response?.has_all_requested ?? false,
onSuccess: () => setErrorToast(),
onError: (error: Error) =>
setErrorToast(error, {
title: i18n.translate('xpack.osquery.action_results_privileges.fetchError', {
defaultMessage: 'Error while fetching action results privileges',
}),
}),
}
);
};

View file

@ -9,11 +9,7 @@ import { useQuery } from 'react-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../common/lib/kibana';
import {
agentPolicyRouteService,
GetAgentPoliciesResponse,
GetAgentPoliciesResponseItem,
} from '../../../fleet/common';
import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
export const useAgentPolicies = () => {
@ -22,12 +18,7 @@ export const useAgentPolicies = () => {
return useQuery<GetAgentPoliciesResponse, unknown, GetAgentPoliciesResponseItem[]>(
['agentPolicies'],
() =>
http.get(agentPolicyRouteService.getListPath(), {
query: {
perPage: 100,
},
}),
() => http.get('/internal/osquery/fleet_wrapper/agent_policies/'),
{
initialData: { items: [], total: 0, page: 1, perPage: 100 },
keepPreviousData: true,

View file

@ -9,7 +9,6 @@ import { useQuery } from 'react-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../common/lib/kibana';
import { agentPolicyRouteService } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
interface UseAgentPolicy {
@ -23,7 +22,7 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => {
return useQuery(
['agentPolicy', { policyId }],
() => http.get(agentPolicyRouteService.getInfoPath(policyId)),
() => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`),
{
enabled: !skip,
keepPreviousData: true,

View file

@ -65,9 +65,13 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
osqueryPolicyData
);
const grouper = useMemo(() => new AgentGrouper(), []);
const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, {
perPage,
});
const { isLoading: agentsLoading, data: agents } = useAllAgents(
osqueryPolicyData,
debouncedSearchValue,
{
perPage,
}
);
// option related
const [options, setOptions] = useState<GroupOption[]>([]);
@ -108,8 +112,8 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ agentSelection, onCh
grouper.setTotalAgents(totalNumAgents);
grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms);
grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents!);
// @ts-expect-error update types
grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents);
const newOptions = grouper.generateOptions();
setOptions(newOptions);
}, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]);

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from 'react-query';
import { GetOneAgentResponse, agentRouteService } from '../../../fleet/common';
import { GetOneAgentResponse } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
import { useKibana } from '../common/lib/kibana';
@ -21,7 +21,7 @@ export const useAgentDetails = ({ agentId }: UseAgentDetails) => {
const setErrorToast = useErrorToast();
return useQuery<GetOneAgentResponse>(
['agentDetails', agentId],
() => http.get(agentRouteService.getInfoPath(agentId)),
() => http.get(`/internal/osquery/fleet_wrapper/agents/${agentId}`),
{
enabled: agentId.length > 0,
onSuccess: () => setErrorToast(),

View file

@ -9,7 +9,7 @@ import { mapKeys } from 'lodash';
import { useQueries, UseQueryResult } from 'react-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../common/lib/kibana';
import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common';
import { GetOneAgentPolicyResponse } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
export const useAgentPolicies = (policyIds: string[] = []) => {
@ -19,7 +19,7 @@ export const useAgentPolicies = (policyIds: string[] = []) => {
const agentResponse = useQueries(
policyIds.map((policyId) => ({
queryKey: ['agentPolicy', policyId],
queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)),
queryFn: () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`),
enabled: policyIds.length > 0,
onSuccess: () => setErrorToast(),
onError: (error) =>

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from 'react-query';
import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common';
import { GetAgentStatusResponse } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
import { useKibana } from '../common/lib/kibana';
@ -25,7 +25,7 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => {
['agentStatus', policyId],
() =>
http.get(
agentRouteService.getStatusPath(),
`/internal/osquery/fleet_wrapper/agent-status`,
policyId
? {
query: {

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from 'react-query';
import { GetAgentsResponse, agentRouteService } from '../../../fleet/common';
import { GetAgentsResponse } from '../../../fleet/common';
import { useErrorToast } from '../common/hooks/use_error_toast';
import { useKibana } from '../common/lib/kibana';
@ -31,7 +31,8 @@ export const useAllAgents = (
const { perPage } = opts;
const { http } = useKibana().services;
const setErrorToast = useErrorToast();
const { isLoading: agentsLoading, data: agentData } = useQuery<GetAgentsResponse>(
return useQuery<GetAgentsResponse>(
['agents', osqueryPolicies, searchValue, perPage],
() => {
let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`;
@ -40,7 +41,7 @@ export const useAllAgents = (
kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`;
}
return http.get(agentRouteService.getListPath(), {
return http.get(`/internal/osquery/fleet_wrapper/agents`, {
query: {
kuery,
perPage,
@ -48,6 +49,8 @@ export const useAllAgents = (
});
},
{
// @ts-expect-error update types
select: (data) => data?.agents || [],
enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0,
onSuccess: () => setErrorToast(),
onError: (error) =>
@ -58,6 +61,4 @@ export const useAllAgents = (
}),
}
);
return { agentsLoading, agents: agentData?.list };
};

View file

@ -10,8 +10,6 @@ import { useQuery } from 'react-query';
import { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../common/lib/kibana';
import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
import { useErrorToast } from '../common/hooks/use_error_toast';
export const useOsqueryPolicies = () => {
@ -20,12 +18,7 @@ export const useOsqueryPolicies = () => {
const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery(
['osqueryPolicies'],
() =>
http.get(packagePolicyRouteService.getListPath(), {
query: {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
},
}),
() => http.get('/internal/osquery/fleet_wrapper/package_policies'),
{
select: (response) =>
uniq<string>(response.items.map((p: { policy_id: string }) => p.policy_id)),

View file

@ -6,11 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
import { find } from 'lodash/fp';
import { useQuery } from 'react-query';
import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { useKibana } from '../lib/kibana';
import { useErrorToast } from './use_error_toast';
@ -18,23 +15,12 @@ export const useOsqueryIntegration = () => {
const { http } = useKibana().services;
const setErrorToast = useErrorToast();
return useQuery(
'integrations',
() =>
http.get(epmRouteService.getListPath(), {
query: {
experimental: true,
},
}),
{
select: ({ response }: GetPackagesResponse) =>
find(['name', OSQUERY_INTEGRATION_NAME], response),
onError: (error: Error) =>
setErrorToast(error, {
title: i18n.translate('xpack.osquery.osquery_integration.fetchError', {
defaultMessage: 'Error while fetching osquery integration',
}),
return useQuery('integration', () => http.get('/internal/osquery/status'), {
onError: (error: Error) =>
setErrorToast(error, {
title: i18n.translate('xpack.osquery.osquery_integration.fetchError', {
defaultMessage: 'Error while fetching osquery integration',
}),
}
);
}),
});
};

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

@ -0,0 +1,28 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const DisabledCalloutComponent = () => (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
title={i18n.translate('xpack.osquery.fleetIntegration.saveIntegrationCalloutTitle', {
defaultMessage: 'Save the integration to access the options below',
})}
iconType="save"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</>
);
export const DisabledCallout = React.memo(DisabledCalloutComponent);

View file

@ -5,16 +5,42 @@
* 2.0.
*/
import React from 'react';
import { EuiLoadingContent } from '@elastic/eui';
import React, { useEffect } from 'react';
import { PackageCustomExtensionComponentProps } from '../../../fleet/public';
import { NavigationButtons } from './navigation_buttons';
import { DisabledCallout } from './disabled_callout';
import { useKibana } from '../common/lib/kibana';
/**
* Exports Osquery-specific package policy instructions
* for use in the Fleet app custom tab
*/
export const OsqueryManagedCustomButtonExtension = React.memo<PackageCustomExtensionComponentProps>(
() => <NavigationButtons />
() => {
const [disabled, setDisabled] = React.useState<boolean | null>(null);
const { http } = useKibana().services;
useEffect(() => {
const fetchStatus = () => {
http.get('/internal/osquery/status').then((response) => {
setDisabled(response.install_status !== 'installed');
});
};
fetchStatus();
}, [http]);
if (disabled === null) {
return <EuiLoadingContent lines={5} />;
}
return (
<>
{disabled ? <DisabledCallout /> : null}
<NavigationButtons isDisabled={disabled} />
</>
);
}
);
OsqueryManagedCustomButtonExtension.displayName = 'OsqueryManagedCustomButtonExtension';

View file

@ -11,7 +11,6 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { produce } from 'immer';
import { i18n } from '@kbn/i18n';
import {
agentRouteService,
agentPolicyRouteService,
@ -29,6 +28,7 @@ import {
import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table';
import { useKibana } from '../common/lib/kibana';
import { NavigationButtons } from './navigation_buttons';
import { DisabledCallout } from './disabled_callout';
import { OsqueryManagerPackagePolicy } from '../../common/types';
/**
@ -163,22 +163,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo<
return (
<>
{!editMode ? (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
title={i18n.translate(
'xpack.osquery.fleetIntegration.saveIntegrationCalloutTitle',
{ defaultMessage: 'Save the integration to access the options below' }
)}
iconType="save"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</>
) : null}
{!editMode ? <DisabledCallout /> : null}
{policyAgentsCount === 0 ? (
<>
<EuiFlexGroup>

View file

@ -47,6 +47,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
defaultValue,
onSuccess,
}) => {
const permissions = useKibana().services.application.capabilities.osquery;
const { http } = useKibana().services;
const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false);
const setErrorToast = useErrorToast();
@ -175,7 +176,12 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
{!agentId && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
disabled={!agentSelected || !queryValueProvided || resultsStatus === 'disabled'}
disabled={
!permissions.writeSavedQueries ||
!agentSelected ||
!queryValueProvided ||
resultsStatus === 'disabled'
}
onClick={handleShowSaveQueryFlout}
>
<FormattedMessage
@ -199,6 +205,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
[
agentId,
agentSelected,
permissions.writeSavedQueries,
handleShowSaveQueryFlout,
queryComponentProps,
queryValueProvided,

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
import { EuiCodeBlock, EuiFormRow, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useRef } from 'react';
import styled from 'styled-components';
import { OsquerySchemaLink } from '../../components/osquery_schema_link';
import { FieldHook } from '../../shared_imports';
@ -15,6 +16,11 @@ import {
SavedQueriesDropdown,
SavedQueriesDropdownRef,
} from '../../saved_queries/saved_queries_dropdown';
import { useKibana } from '../../common/lib/kibana';
const StyledEuiCodeBlock = styled(EuiCodeBlock)`
min-height: 150px;
`;
interface LiveQueryQueryFieldProps {
disabled?: boolean;
@ -22,6 +28,7 @@ interface LiveQueryQueryFieldProps {
}
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disabled, field }) => {
const permissions = useKibana().services.application.capabilities.osquery;
const { value, setValue, errors } = field;
const error = errors[0]?.message;
const savedQueriesDropdownRef = useRef<SavedQueriesDropdownRef>(null);
@ -46,12 +53,23 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disa
<>
<SavedQueriesDropdown
ref={savedQueriesDropdownRef}
disabled={disabled}
disabled={disabled || !permissions.runSavedQueries}
onChange={handleSavedQueryChange}
/>
<EuiSpacer />
<EuiFormRow fullWidth labelAppend={<OsquerySchemaLink />}>
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
{!permissions.writeLiveQueries ? (
<StyledEuiCodeBlock
language="sql"
fontSize="m"
paddingSize="m"
transparentBackground={!value.length}
>
{value}
</StyledEuiCodeBlock>
) : (
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
)}
</EuiFormRow>
</>
</EuiFormRow>

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { BehaviorSubject, Subject } from 'rxjs';
import {
AppMountParameters,
CoreSetup,
@ -13,9 +12,6 @@ import {
PluginInitializerContext,
CoreStart,
DEFAULT_APP_CATEGORIES,
AppStatus,
AppNavLinkStatus,
AppUpdater,
} from '../../../../src/core/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import {
@ -25,7 +21,6 @@ import {
AppPluginStartDependencies,
} from './types';
import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common';
import { Installation } from '../../fleet/common';
import {
LazyOsqueryManagedPolicyCreateImportExtension,
LazyOsqueryManagedPolicyEditExtension,
@ -33,48 +28,7 @@ import {
} from './fleet_integration';
import { getLazyOsqueryAction } from './shared_components';
export function toggleOsqueryPlugin(
updater$: Subject<AppUpdater>,
http: CoreStart['http'],
registerExtension?: StartPlugins['fleet']['registerExtension']
) {
if (http.anonymousPaths.isAnonymous(window.location.pathname)) {
updater$.next(() => ({
status: AppStatus.inaccessible,
navLinkStatus: AppNavLinkStatus.hidden,
}));
return;
}
http
.fetch<Installation | undefined>(`/internal/osquery/status`)
.then((response) => {
const installed = response?.install_status === 'installed';
if (installed && registerExtension) {
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-detail-custom',
Component: LazyOsqueryManagedCustomButtonExtension,
});
}
updater$.next(() => ({
navLinkStatus: installed ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
}));
})
.catch(() => {
updater$.next(() => ({
status: AppStatus.inaccessible,
navLinkStatus: AppNavLinkStatus.hidden,
}));
});
}
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({
navLinkStatus: AppNavLinkStatus.hidden,
}));
private kibanaVersion: string;
private storage = new Storage(localStorage);
@ -102,8 +56,6 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
id: 'osquery',
title: PLUGIN_NAME,
order: 9030,
updater$: this.appUpdater$,
navLinkStatus: AppNavLinkStatus.hidden,
category: DEFAULT_APP_CATEGORIES.management,
async mount(params: AppMountParameters) {
// Get start services as specified in kibana.json
@ -141,8 +93,6 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
if (plugins.fleet) {
const { registerExtension } = plugins.fleet;
toggleOsqueryPlugin(this.appUpdater$, core.http, registerExtension);
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-policy-create',
@ -154,11 +104,12 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
view: 'package-policy-edit',
Component: LazyOsqueryManagedPolicyEditExtension,
});
} else {
this.appUpdater$.next(() => ({
status: AppStatus.inaccessible,
navLinkStatus: AppNavLinkStatus.hidden,
}));
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-detail-custom',
Component: LazyOsqueryManagedCustomButtonExtension,
});
}
return {

View file

@ -8,6 +8,7 @@
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
import {
EuiCallOut,
EuiCode,
EuiDataGrid,
EuiDataGridSorting,
EuiDataGridProps,
@ -31,6 +32,8 @@ import {
ViewResultsInLensAction,
ViewResultsActionButtonType,
} from '../scheduled_query_groups/scheduled_query_group_queries_table';
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
const DataContext = createContext<ResultEdges>([]);
@ -49,6 +52,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
endDate,
}) => {
const [isLive, setIsLive] = useState(true);
const { data: hasActionResultsPrivileges } = useActionResultsPrivileges();
const {
// @ts-expect-error update types
data: { aggregations },
@ -60,6 +64,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
direction: Direction.asc,
sortField: '@timestamp',
isLive,
skip: !hasActionResultsPrivileges,
});
const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]);
const { getUrlForApp } = useKibana().services.application;
@ -104,6 +109,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
field: sortedColumn.id,
direction: sortedColumn.direction as Direction,
})),
skip: !hasActionResultsPrivileges,
});
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
@ -237,6 +243,17 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
]
);
if (!hasActionResultsPrivileges) {
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>.
</p>
</EuiCallOut>
);
}
if (!isFetched) {
return <EuiLoadingContent lines={5} />;
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './missing_privileges';

View file

@ -0,0 +1,47 @@
/*
* 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 { EuiEmptyPrompt, EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import styled from 'styled-components';
const Panel = styled(EuiPanel)`
max-width: 500px;
margin-right: auto;
margin-left: auto;
`;
const MissingPrivilegesComponent = () => (
<div>
<EuiSpacer />
<Panel>
<EuiEmptyPrompt
iconType="securityApp"
title={
<h2>
<FormattedMessage
id="xpack.osquery.permissionDeniedErrorTitle"
defaultMessage="Permission denied"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.osquery.permissionDeniedErrorMessage"
defaultMessage="You are not authorized to access this page."
/>
</p>
}
/>
</Panel>
<EuiSpacer />
</div>
);
export const MissingPrivileges = React.memo(MissingPrivilegesComponent);

View file

@ -12,15 +12,26 @@ import { LiveQueriesPage } from './list';
import { NewLiveQueryPage } from './new';
import { LiveQueryDetailsPage } from './details';
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
import { useKibana } from '../../common/lib/kibana';
import { MissingPrivileges } from '../components';
const LiveQueriesComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
useBreadcrumbs('live_queries');
const match = useRouteMatch();
if (!permissions.readLiveQueries) {
return <MissingPrivileges />;
}
return (
<Switch>
<Route path={`${match.url}/new`}>
<NewLiveQueryPage />
{permissions.runSavedQueries || permissions.writeLiveQueries ? (
<NewLiveQueryPage />
) : (
<MissingPrivileges />
)}
</Route>
<Route path={`${match.url}/:actionId`}>
<LiveQueryDetailsPage />

View file

@ -9,13 +9,14 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
import { ActionsTable } from '../../../actions/actions_table';
import { WithHeaderLayout } from '../../../components/layouts';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
const LiveQueriesPageComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
useBreadcrumbs('live_queries');
const newQueryLinkProps = useRouterNavigate('live_queries/new');
@ -40,14 +41,19 @@ const LiveQueriesPageComponent = () => {
const RightColumn = useMemo(
() => (
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
<EuiButton
fill
{...newQueryLinkProps}
iconType="plusInCircle"
isDisabled={!(permissions.writeLiveQueries || permissions.runSavedQueries)}
>
<FormattedMessage
id="xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel"
defaultMessage="New live query"
/>
</EuiButton>
),
[newQueryLinkProps]
[permissions.writeLiveQueries, permissions.runSavedQueries, newQueryLinkProps]
);
return (

View file

@ -24,11 +24,13 @@ import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_f
interface EditSavedQueryFormProps {
defaultValue?: unknown;
handleSubmit: () => Promise<void>;
viewMode?: boolean;
}
const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
defaultValue,
handleSubmit,
viewMode,
}) => {
const savedQueryListProps = useRouterNavigate('saved_queries');
@ -39,41 +41,45 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
return (
<Form form={form}>
<SavedQueryForm />
<EuiBottomBar>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<SavedQueryForm viewMode={viewMode} />
{!viewMode && (
<>
<EuiBottomBar>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" {...savedQueryListProps}>
<FormattedMessage
id="xpack.osquery.editSavedQuery.form.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
// isLoading={isLoading}
color="primary"
fill
size="m"
iconType="save"
onClick={form.submit}
>
<FormattedMessage
id="xpack.osquery.editSavedQuery.form.updateQueryButtonLabel"
defaultMessage="Update query"
/>
</EuiButton>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" {...savedQueryListProps}>
<FormattedMessage
id="xpack.osquery.editSavedQuery.form.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
// isLoading={isLoading}
color="primary"
fill
size="m"
iconType="save"
onClick={form.submit}
>
<FormattedMessage
id="xpack.osquery.editSavedQuery.form.updateQueryButtonLabel"
defaultMessage="Update query"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
</EuiBottomBar>
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
</>
)}
</Form>
);
};

View file

@ -17,7 +17,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { useParams } from 'react-router-dom';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
import { WithHeaderLayout } from '../../../components/layouts';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
@ -25,6 +25,8 @@ import { EditSavedQueryForm } from './form';
import { useDeleteSavedQuery, useUpdateSavedQuery, useSavedQuery } from '../../../saved_queries';
const EditSavedQueryPageComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const { savedQueryId } = useParams<{ savedQueryId: string }>();
const savedQueryListProps = useRouterNavigate('saved_queries');
@ -35,6 +37,8 @@ const EditSavedQueryPageComponent = () => {
useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' });
const viewMode = useMemo(() => !permissions.writeSavedQueries, [permissions.writeSavedQueries]);
const handleCloseDeleteConfirmationModal = useCallback(() => {
setIsDeleteModalVisible(false);
}, []);
@ -63,21 +67,32 @@ const EditSavedQueryPageComponent = () => {
<EuiFlexItem>
<BetaBadgeRowWrapper>
<h1>
<FormattedMessage
id="xpack.osquery.editSavedQuery.pageTitle"
defaultMessage='Edit "{savedQueryId}"'
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
values={{
savedQueryId: savedQueryDetails?.attributes?.id ?? '',
}}
/>
{viewMode ? (
<FormattedMessage
id="xpack.osquery.viewSavedQuery.pageTitle"
defaultMessage='"{savedQueryId}" details'
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
values={{
savedQueryId: savedQueryDetails?.attributes?.id ?? '',
}}
/>
) : (
<FormattedMessage
id="xpack.osquery.editSavedQuery.pageTitle"
defaultMessage='Edit "{savedQueryId}"'
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
values={{
savedQueryId: savedQueryDetails?.attributes?.id ?? '',
}}
/>
)}
</h1>
<BetaBadge />
</BetaBadgeRowWrapper>
</EuiFlexItem>
</EuiFlexGroup>
),
[savedQueryDetails?.attributes?.id, savedQueryListProps]
[savedQueryDetails?.attributes?.id, savedQueryListProps, viewMode]
);
const RightColumn = useMemo(
@ -95,12 +110,17 @@ const EditSavedQueryPageComponent = () => {
if (isLoading) return null;
return (
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
<WithHeaderLayout
leftColumn={LeftColumn}
rightColumn={!viewMode ? RightColumn : undefined}
rightColumnGrow={false}
>
{!isLoading && !isEmpty(savedQueryDetails) && (
<EditSavedQueryForm
defaultValue={savedQueryDetails?.attributes}
// @ts-expect-error update types
handleSubmit={updateSavedQueryMutation.mutateAsync}
viewMode={viewMode}
/>
)}
{isDeleteModalVisible ? (

View file

@ -12,15 +12,22 @@ import { QueriesPage } from './list';
import { NewSavedQueryPage } from './new';
import { EditSavedQueryPage } from './edit';
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
import { MissingPrivileges } from '../components';
import { useKibana } from '../../common/lib/kibana';
const SavedQueriesComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
useBreadcrumbs('saved_queries');
const match = useRouteMatch();
if (!permissions.readSavedQueries) {
return <MissingPrivileges />;
}
return (
<Switch>
<Route path={`${match.url}/new`}>
<NewSavedQueryPage />
{permissions.writeSavedQueries ? <NewSavedQueryPage /> : <MissingPrivileges />}
</Route>
<Route path={`${match.url}/:savedQueryId`}>
<EditSavedQueryPage />

View file

@ -21,16 +21,63 @@ import { useHistory } from 'react-router-dom';
import { SavedObject } from 'kibana/public';
import { WithHeaderLayout } from '../../../components/layouts';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
import { useSavedQueries } from '../../../saved_queries/use_saved_queries';
interface EditButtonProps {
interface PlayButtonProps {
disabled: boolean;
savedQueryId: string;
savedQueryName: string;
}
const EditButtonComponent: React.FC<EditButtonProps> = ({ savedQueryId, savedQueryName }) => {
const PlayButtonComponent: React.FC<PlayButtonProps> = ({
disabled = false,
savedQueryId,
savedQueryName,
}) => {
const { push } = useHistory();
// TODO: Fix href
const handlePlayClick = useCallback(
() =>
push('/live_queries/new', {
form: {
savedQueryId,
},
}),
[push, savedQueryId]
);
return (
<EuiButtonIcon
color="primary"
iconType="play"
isDisabled={disabled}
onClick={handlePlayClick}
aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.runActionAriaLabel', {
defaultMessage: 'Run {savedQueryName}',
values: {
savedQueryName,
},
})}
/>
);
};
const PlayButton = React.memo(PlayButtonComponent);
interface EditButtonProps {
disabled?: boolean;
savedQueryId: string;
savedQueryName: string;
}
const EditButtonComponent: React.FC<EditButtonProps> = ({
disabled = false,
savedQueryId,
savedQueryName,
}) => {
const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`);
return (
@ -38,6 +85,7 @@ const EditButtonComponent: React.FC<EditButtonProps> = ({ savedQueryId, savedQue
color="primary"
{...buttonProps}
iconType="pencil"
isDisabled={disabled}
aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', {
defaultMessage: 'Edit {savedQueryName}',
values: {
@ -51,8 +99,9 @@ const EditButtonComponent: React.FC<EditButtonProps> = ({ savedQueryId, savedQue
const EditButton = React.memo(EditButtonComponent);
const SavedQueriesPageComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
useBreadcrumbs('saved_queries');
const { push } = useHistory();
const newQueryLinkProps = useRouterNavigate('saved_queries/new');
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(20);
@ -61,16 +110,6 @@ const SavedQueriesPageComponent = () => {
const { data } = useSavedQueries({ isLive: true });
const handlePlayClick = useCallback(
(item) =>
push('/live_queries/new', {
form: {
savedQueryId: item.id,
},
}),
[push]
);
const renderEditAction = useCallback(
(item: SavedObject<{ name: string }>) => (
<EditButton savedQueryId={item.id} savedQueryName={item.attributes.name} />
@ -78,6 +117,17 @@ const SavedQueriesPageComponent = () => {
[]
);
const renderPlayAction = useCallback(
(item: SavedObject<{ name: string }>) => (
<PlayButton
savedQueryId={item.id}
savedQueryName={item.attributes.name}
disabled={!(permissions.runSavedQueries || permissions.writeLiveQueries)}
/>
),
[permissions.runSavedQueries, permissions.writeLiveQueries]
);
const renderUpdatedAt = useCallback((updatedAt, item) => {
if (!updatedAt) return '-';
@ -128,17 +178,10 @@ const SavedQueriesPageComponent = () => {
name: i18n.translate('xpack.osquery.savedQueries.table.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
actions: [
{
type: 'icon',
icon: 'play',
onClick: handlePlayClick,
},
{ render: renderEditAction },
],
actions: [{ render: renderPlayAction }, { render: renderEditAction }],
},
],
[handlePlayClick, renderEditAction, renderUpdatedAt]
[renderEditAction, renderPlayAction, renderUpdatedAt]
);
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
@ -189,14 +232,19 @@ const SavedQueriesPageComponent = () => {
const RightColumn = useMemo(
() => (
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
<EuiButton
fill
{...newQueryLinkProps}
iconType="plusInCircle"
isDisabled={!permissions.writeSavedQueries}
>
<FormattedMessage
id="xpack.osquery.savedQueryList.addSavedQueryButtonLabel"
defaultMessage="Add saved query"
/>
</EuiButton>
),
[newQueryLinkProps]
[permissions.writeSavedQueries, newQueryLinkProps]
);
return (

View file

@ -21,7 +21,7 @@ import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
import { WithHeaderLayout } from '../../../components/layouts';
import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group';
import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table';
@ -36,6 +36,7 @@ const Divider = styled.div`
`;
const ScheduledQueryGroupDetailsPageComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>();
const scheduledQueryGroupsListProps = useRouterNavigate('scheduled_query_groups');
const editQueryLinkProps = useRouterNavigate(
@ -111,7 +112,12 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
<Divider />
</EuiFlexItem>
<EuiFlexItem grow={false} key="edit_button">
<EuiButton fill {...editQueryLinkProps} iconType="pencil">
<EuiButton
fill
{...editQueryLinkProps}
iconType="pencil"
isDisabled={!permissions.writePacks}
>
<FormattedMessage
id="xpack.osquery.scheduledQueryDetailsPage.editQueryButtonLabel"
defaultMessage="Edit"
@ -120,7 +126,7 @@ const ScheduledQueryGroupDetailsPageComponent = () => {
</EuiFlexItem>
</EuiFlexGroup>
),
[data?.policy_id, editQueryLinkProps]
[data?.policy_id, editQueryLinkProps, permissions]
);
return (

View file

@ -13,18 +13,25 @@ import { AddScheduledQueryGroupPage } from './add';
import { EditScheduledQueryGroupPage } from './edit';
import { ScheduledQueryGroupDetailsPage } from './details';
import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs';
import { useKibana } from '../../common/lib/kibana';
import { MissingPrivileges } from '../components';
const ScheduledQueryGroupsComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
useBreadcrumbs('scheduled_query_groups');
const match = useRouteMatch();
if (!permissions.readPacks) {
return <MissingPrivileges />;
}
return (
<Switch>
<Route path={`${match.url}/add`}>
<AddScheduledQueryGroupPage />
{permissions.writePacks ? <AddScheduledQueryGroupPage /> : <MissingPrivileges />}
</Route>
<Route path={`${match.url}/:scheduledQueryGroupId/edit`}>
<EditScheduledQueryGroupPage />
{permissions.writePacks ? <EditScheduledQueryGroupPage /> : <MissingPrivileges />}
</Route>
<Route path={`${match.url}/:scheduledQueryGroupId`}>
<ScheduledQueryGroupDetailsPage />

View file

@ -9,12 +9,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import { useRouterNavigate } from '../../../common/lib/kibana';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';
import { WithHeaderLayout } from '../../../components/layouts';
import { ScheduledQueryGroupsTable } from '../../../scheduled_query_groups/scheduled_query_groups_table';
import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge';
const ScheduledQueryGroupsPageComponent = () => {
const permissions = useKibana().services.application.capabilities.osquery;
const newQueryLinkProps = useRouterNavigate('scheduled_query_groups/add');
const LeftColumn = useMemo(
@ -38,14 +39,19 @@ const ScheduledQueryGroupsPageComponent = () => {
const RightColumn = useMemo(
() => (
<EuiButton fill {...newQueryLinkProps} iconType="plusInCircle">
<EuiButton
fill
{...newQueryLinkProps}
iconType="plusInCircle"
isDisabled={!permissions.writePacks}
>
<FormattedMessage
id="xpack.osquery.scheduledQueryList.addScheduledQueryButtonLabel"
defaultMessage="Add scheduled query group"
/>
</EuiButton>
),
[newQueryLinkProps]
[newQueryLinkProps, permissions.writePacks]
);
return (

View file

@ -6,7 +6,7 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
import React from 'react';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -17,64 +17,78 @@ import { CodeEditorField } from './code_editor_field';
export const CommonUseField = getUseField({ component: Field });
const SavedQueryFormComponent = () => (
<>
<CommonUseField path="id" />
<EuiSpacer />
<CommonUseField path="description" />
<EuiSpacer />
<UseField path="query" component={CodeEditorField} />
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h5>
interface SavedQueryFormProps {
viewMode?: boolean;
}
const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({ viewMode }) => {
const euiFieldProps = useMemo(
() => ({
isDisabled: !!viewMode,
}),
[viewMode]
);
return (
<>
<CommonUseField path="id" euiFieldProps={euiFieldProps} />
<EuiSpacer />
<CommonUseField path="description" euiFieldProps={euiFieldProps} />
<EuiSpacer />
<UseField path="query" component={CodeEditorField} euiFieldProps={euiFieldProps} />
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.title"
defaultMessage="Scheduled query group configuration"
/>
</h5>
</EuiTitle>
<EuiText color="subdued">
<FormattedMessage
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.title"
defaultMessage="Scheduled query group configuration"
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.description"
defaultMessage="The options listed below are optional and are only applied when the query is assigned to a scheduled query group."
/>
</h5>
</EuiTitle>
<EuiText color="subdued">
<FormattedMessage
id="xpack.osquery.savedQueries.form.scheduledQueryGroupConfigSection.description"
defaultMessage="The options listed below are optional and are only applied when the query is assigned to a scheduled query group."
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<CommonUseField
path="interval"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
euiFieldProps={{ append: 's', ...euiFieldProps }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<CommonUseField
path="interval"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
euiFieldProps={{ append: 's' }}
/>
<EuiSpacer />
<CommonUseField
path="version"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
euiFieldProps={{
noSuggestions: false,
singleSelection: { asPlainText: true },
placeholder: i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
{
defaultMessage: 'ALL',
}
),
options: ALL_OSQUERY_VERSIONS_OPTIONS,
onCreateOption: undefined,
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</>
);
<EuiSpacer />
<CommonUseField
path="version"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
euiFieldProps={{
noSuggestions: false,
singleSelection: { asPlainText: true },
placeholder: i18n.translate(
'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel',
{
defaultMessage: 'ALL',
}
),
options: ALL_OSQUERY_VERSIONS_OPTIONS,
onCreateOption: undefined,
...euiFieldProps,
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<CommonUseField path="platform" component={PlatformCheckBoxGroupField} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</>
);
};
export const SavedQueryForm = React.memo(SavedQueryFormComponent);

View file

@ -28,12 +28,16 @@ const StyledEuiLoadingSpinner = styled(EuiLoadingSpinner)`
`;
interface ActiveStateSwitchProps {
disabled?: boolean;
item: PackagePolicy;
}
const ActiveStateSwitchComponent: React.FC<ActiveStateSwitchProps> = ({ item }) => {
const queryClient = useQueryClient();
const {
application: {
capabilities: { osquery: permissions },
},
http,
notifications: { toasts },
} = useKibana().services;
@ -126,7 +130,7 @@ const ActiveStateSwitchComponent: React.FC<ActiveStateSwitchProps> = ({ item })
{isLoading && <StyledEuiLoadingSpinner />}
<EuiSwitch
checked={item.enabled}
disabled={isLoading}
disabled={!permissions.writePacks || isLoading}
showLabel={false}
label=""
onChange={handleToggleActiveClick}

View file

@ -8,7 +8,7 @@
import { useQuery } from 'react-query';
import { useKibana } from '../common/lib/kibana';
import { GetOnePackagePolicyResponse, packagePolicyRouteService } from '../../../fleet/common';
import { GetOnePackagePolicyResponse } from '../../../fleet/common';
import { OsqueryManagerPackagePolicy } from '../../common/types';
interface UseScheduledQueryGroup {
@ -28,7 +28,7 @@ export const useScheduledQueryGroup = ({
OsqueryManagerPackagePolicy
>(
['scheduledQueryGroup', { scheduledQueryGroupId }],
() => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)),
() => http.get(`/internal/osquery/scheduled_query_group/${scheduledQueryGroupId}`),
{
keepPreviousData: true,
enabled: !skip || !scheduledQueryGroupId,

View file

@ -9,12 +9,7 @@ import { produce } from 'immer';
import { useQuery } from 'react-query';
import { useKibana } from '../common/lib/kibana';
import {
ListResult,
PackagePolicy,
packagePolicyRouteService,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
} from '../../../fleet/common';
import { ListResult, PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
export const useScheduledQueryGroups = () => {
@ -23,7 +18,7 @@ export const useScheduledQueryGroups = () => {
return useQuery<ListResult<PackagePolicy>>(
['scheduledQueries'],
() =>
http.get(packagePolicyRouteService.getListPath(), {
http.get('/internal/osquery/scheduled_query_group', {
query: {
page: 1,
perPage: 10000,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Logger, LoggerFactory } from 'src/core/server';
import { CoreSetup, Logger, LoggerFactory } from '../../../../../src/core/server';
import { SecurityPluginStart } from '../../../security/server';
import {
AgentService,
@ -71,6 +71,7 @@ export interface OsqueryAppContext {
logFactory: LoggerFactory;
config(): ConfigType;
security: SecurityPluginStart;
getStartServices: CoreSetup['getStartServices'];
/**
* Object readiness is tied to plugin start method
*/

View file

@ -5,14 +5,20 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
PACKAGES_SAVED_OBJECT_TYPE,
} from '../../fleet/common';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
Logger,
DEFAULT_APP_CATEGORIES,
} from '../../../../src/core/server';
import { createConfig } from './create_config';
import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types';
import { defineRoutes } from './routes';
@ -21,6 +27,169 @@ import { initSavedObjects } from './saved_objects';
import { initUsageCollectors } from './usage';
import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_context_services';
import { ConfigType } from './config';
import { packSavedObjectType, savedQuerySavedObjectType } from '../common/types';
import { PLUGIN_ID } from '../common';
const registerFeatures = (features: SetupPlugins['features']) => {
features.registerKibanaFeature({
id: PLUGIN_ID,
name: i18n.translate('xpack.osquery.features.osqueryFeatureName', {
defaultMessage: 'Osquery',
}),
category: DEFAULT_APP_CATEGORIES.management,
app: [PLUGIN_ID, 'kibana'],
catalogue: [PLUGIN_ID],
order: 2300,
excludeFromBasePrivileges: true,
privileges: {
all: {
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-write`],
app: [PLUGIN_ID, 'kibana'],
catalogue: [PLUGIN_ID],
savedObject: {
all: [PACKAGE_POLICY_SAVED_OBJECT_TYPE],
read: [PACKAGES_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE],
},
ui: ['write'],
},
read: {
api: [`${PLUGIN_ID}-read`],
app: [PLUGIN_ID, 'kibana'],
catalogue: [PLUGIN_ID],
savedObject: {
all: [],
read: [
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
PACKAGES_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
],
},
ui: ['read'],
},
},
subFeatures: [
{
name: i18n.translate('xpack.osquery.features.liveQueriesSubFeatureName', {
defaultMessage: 'Live queries',
}),
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${PLUGIN_ID}-writeLiveQueries`, `${PLUGIN_ID}-readLiveQueries`],
id: 'live_queries_all',
includeIn: 'all',
name: 'All',
savedObject: {
all: [],
read: [],
},
ui: ['writeLiveQueries', 'readLiveQueries'],
},
{
api: [`${PLUGIN_ID}-readLiveQueries`],
id: 'live_queries_read',
includeIn: 'read',
name: 'Read',
savedObject: {
all: [],
read: [],
},
ui: ['readLiveQueries'],
},
],
},
{
groupType: 'independent',
privileges: [
{
api: [`${PLUGIN_ID}-runSavedQueries`],
id: 'run_saved_queries',
name: i18n.translate('xpack.osquery.features.runSavedQueriesPrivilegeName', {
defaultMessage: 'Run Saved queries',
}),
includeIn: 'all',
savedObject: {
all: [],
read: [],
},
ui: ['runSavedQueries'],
},
],
},
],
},
{
name: i18n.translate('xpack.osquery.features.savedQueriesSubFeatureName', {
defaultMessage: 'Saved queries',
}),
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
id: 'saved_queries_all',
includeIn: 'all',
name: 'All',
savedObject: {
all: [savedQuerySavedObjectType],
read: [],
},
ui: ['writeSavedQueries', 'readSavedQueries'],
},
{
id: 'saved_queries_read',
includeIn: 'read',
name: 'Read',
savedObject: {
all: [],
read: [savedQuerySavedObjectType],
},
ui: ['readSavedQueries'],
},
],
},
],
},
{
// TODO: Rename it to "Packs" as part of https://github.com/elastic/kibana/pull/107345
name: i18n.translate('xpack.osquery.features.scheduledQueryGroupsSubFeatureName', {
defaultMessage: 'Scheduled query groups',
}),
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${PLUGIN_ID}-writePacks`],
id: 'packs_all',
includeIn: 'all',
name: 'All',
savedObject: {
all: [packSavedObjectType],
read: [],
},
ui: ['writePacks', 'readPacks'],
},
{
api: [`${PLUGIN_ID}-readPacks`],
id: 'packs_read',
includeIn: 'read',
name: 'Read',
savedObject: {
all: [],
read: [packSavedObjectType],
},
ui: ['readPacks'],
},
],
},
],
},
],
});
};
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
private readonly logger: Logger;
@ -40,10 +209,13 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
return {};
}
registerFeatures(plugins.features);
const router = core.http.createRouter();
const osqueryContext: OsqueryAppContext = {
logFactory: this.context.logger,
getStartServices: core.getStartServices,
service: this.osqueryAppContextService,
config: (): ConfigType => config,
security: plugins.security,

View file

@ -8,6 +8,7 @@
import uuid from 'uuid';
import moment from 'moment';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
@ -19,6 +20,7 @@ import {
} from '../../../common/schemas/routes/action/create_action_request_body_schema';
import { incrementCount } from '../usage';
import { getInternalSavedObjectsClient } from '../../usage/collector';
export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.post(
@ -30,10 +32,17 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
CreateActionRequestBodySchema
>(createActionRequestBodySchema),
},
options: {
tags: [`access:${PLUGIN_ID}-readLiveQueries`, `access:${PLUGIN_ID}-runSavedQueries`],
},
},
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asCurrentUser;
const esClient = context.core.elasticsearch.client.asInternalUser;
const soClient = context.core.savedObjects.client;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
);
const { agentSelection } = request.body as { agentSelection: AgentSelection };
const selectedAgents = await parseAgentSelection(
esClient,
@ -41,12 +50,14 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
osqueryContext,
agentSelection
);
incrementCount(soClient, 'live_query');
incrementCount(internalSavedObjectsClient, 'live_query');
if (!selectedAgents.length) {
incrementCount(soClient, 'live_query', 'errors');
incrementCount(internalSavedObjectsClient, 'live_query', 'errors');
return response.badRequest({ body: new Error('No agents found for selection') });
}
// TODO: Add check for `runSavedQueries` only
try {
const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username;
const action = {
@ -74,7 +85,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon
},
});
} catch (error) {
incrementCount(soClient, 'live_query', 'errors');
incrementCount(internalSavedObjectsClient, 'live_query', 'errors');
return response.customError({
statusCode: 500,
body: new Error(`Error occurred while processing ${error}`),

View file

@ -0,0 +1,33 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/fleet_wrapper/agents/{id}',
validate: {
params: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
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);
return response.ok({ body: { item: agent } });
}
);
};

View file

@ -0,0 +1,34 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/fleet_wrapper/agent_policies',
validate: {
params: schema.object({}, { unknowns: 'allow' }),
query: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const agentPolicies = await osqueryContext.service.getAgentPolicyService()?.list(soClient, {
...(request.query || {}),
perPage: 100,
});
return response.ok({ body: agentPolicies });
}
);
};

View file

@ -0,0 +1,34 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/fleet_wrapper/agent_policies/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const packageInfo = await osqueryContext.service
.getAgentPolicyService()
?.get(soClient, request.params.id);
return response.ok({ body: { item: packageInfo } });
}
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { GetAgentStatusResponse } from '../../../../fleet/common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getAgentStatusForAgentPolicyRoute = (
router: IRouter,
osqueryContext: OsqueryAppContext
) => {
router.get(
{
path: '/internal/osquery/fleet_wrapper/agent-status',
validate: {
query: schema.object({
policyId: schema.string(),
kuery: schema.maybe(schema.string()),
}),
params: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asInternalUser;
const results = await osqueryContext.service
.getAgentService()
?.getAgentStatusForAgentPolicy(esClient, request.query.policyId, request.query.kuery);
if (!results) {
return response.ok({ body: {} });
}
const body: GetAgentStatusResponse = { results };
return response.ok({ body });
}
);
};

View file

@ -0,0 +1,33 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/fleet_wrapper/agents',
validate: {
query: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asInternalUser;
const agents = await osqueryContext.service
.getAgentService()
// @ts-expect-error update types
?.listAgents(esClient, request.query);
return response.ok({ body: agents });
}
);
};

View file

@ -6,19 +6,19 @@
*/
import { schema } from '@kbn/config-schema';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const findScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/scheduled_query',
path: '/internal/osquery/fleet_wrapper/package_policies',
validate: {
query: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`;

View file

@ -0,0 +1,24 @@
/*
* 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 { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { getAgentPoliciesRoute } from './get_agent_policies';
import { getAgentPolicyRoute } from './get_agent_policy';
import { getAgentStatusForAgentPolicyRoute } from './get_agent_status_for_agent_policy';
import { getPackagePoliciesRoute } from './get_package_policies';
import { getAgentsRoute } from './get_agents';
import { getAgentDetailsRoute } from './get_agent_details';
export const initFleetWrapperRoutes = (router: IRouter, context: OsqueryAppContext) => {
getAgentDetailsRoute(router, context);
getAgentPoliciesRoute(router, context);
getAgentPolicyRoute(router, context);
getAgentStatusForAgentPolicyRoute(router, context);
getPackagePoliciesRoute(router, context);
getAgentsRoute(router, context);
};

View file

@ -10,13 +10,19 @@ import { initActionRoutes } from './action';
import { OsqueryAppContext } from '../lib/osquery_app_context_services';
import { initSavedQueryRoutes } from './saved_query';
import { initStatusRoutes } from './status';
import { initFleetWrapperRoutes } from './fleet_wrapper';
import { initPackRoutes } from './pack';
import { initScheduledQueryGroupRoutes } from './scheduled_query_group';
import { initPrivilegesCheckRoutes } from './privileges_check';
export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => {
const config = context.config();
initActionRoutes(router, context);
initStatusRoutes(router, context);
initScheduledQueryGroupRoutes(router, context);
initFleetWrapperRoutes(router, context);
initPrivilegesCheckRoutes(router, context);
if (config.packs) {
initPackRoutes(router);

View file

@ -0,0 +1,14 @@
/*
* 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 { IRouter } from '../../../../../../src/core/server';
import { privilegesCheckRoute } from './privileges_check_route';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const initPrivilegesCheckRoutes = (router: IRouter, context: OsqueryAppContext) => {
privilegesCheckRoute(router, context);
};

View file

@ -0,0 +1,43 @@
/*
* 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 { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.get(
{
path: '/internal/osquery/privileges_check',
validate: {},
options: {
tags: [`access:${PLUGIN_ID}-readLiveQueries`],
},
},
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asCurrentUser;
const privileges = (
await esClient.security.hasPrivileges({
body: {
index: [
{
names: [`logs-${OSQUERY_INTEGRATION_NAME}.result*`],
privileges: ['read'],
},
],
},
})
).body;
return response.ok({
body: privileges,
});
}
);
};

View file

@ -6,7 +6,7 @@
*/
import { IRouter } from '../../../../../../src/core/server';
import { PLUGIN_ID } from '../../../common';
import {
createSavedQueryRequestSchema,
CreateSavedQueryRequestSchemaDecoded,
@ -24,6 +24,7 @@ export const createSavedQueryRoute = (router: IRouter) => {
CreateSavedQueryRequestSchemaDecoded
>(createSavedQueryRequestSchema),
},
options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
@ -17,6 +17,7 @@ export const deleteSavedQueryRoute = (router: IRouter) => {
validate: {
body: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
@ -17,6 +17,7 @@ export const findSavedQueryRoute = (router: IRouter) => {
validate: {
query: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
@ -17,6 +17,7 @@ export const readSavedQueryRoute = (router: IRouter) => {
validate: {
params: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => {
params: schema.object({}, { unknowns: 'allow' }),
body: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -6,16 +6,18 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const createScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.post(
{
path: '/internal/osquery/scheduled',
path: '/internal/osquery/scheduled_query_group',
validate: {
body: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-writePacks`] },
},
async (context, request, response) => {
const esClient = context.core.elasticsearch.client.asCurrentUser;

View file

@ -6,17 +6,18 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
export const deleteSavedQueryRoute = (router: IRouter) => {
router.delete(
{
path: '/internal/osquery/saved_query',
path: '/internal/osquery/scheduled_query_group',
validate: {
body: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-writePacks`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -0,0 +1,38 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const findScheduledQueryGroupRoute = (
router: IRouter,
osqueryContext: OsqueryAppContext
) => {
router.get(
{
path: '/internal/osquery/scheduled_query_group',
validate: {
query: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-readPacks`] },
},
async (context, request, response) => {
const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`;
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const policies = await packagePolicyService?.list(context.core.savedObjects.client, {
kuery,
});
return response.ok({
body: policies,
});
}
);
};

View file

@ -10,14 +10,14 @@ import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
// import { createScheduledQueryRoute } from './create_scheduled_query_route';
// import { deleteScheduledQueryRoute } from './delete_scheduled_query_route';
import { findScheduledQueryRoute } from './find_scheduled_query_route';
import { readScheduledQueryRoute } from './read_scheduled_query_route';
import { findScheduledQueryGroupRoute } from './find_scheduled_query_group_route';
import { readScheduledQueryGroupRoute } from './read_scheduled_query_group_route';
// import { updateScheduledQueryRoute } from './update_scheduled_query_route';
export const initScheduledQueryRoutes = (router: IRouter, context: OsqueryAppContext) => {
export const initScheduledQueryGroupRoutes = (router: IRouter, context: OsqueryAppContext) => {
// createScheduledQueryRoute(router);
// deleteScheduledQueryRoute(router);
findScheduledQueryRoute(router, context);
readScheduledQueryRoute(router, context);
findScheduledQueryGroupRoute(router, context);
readScheduledQueryGroupRoute(router, context);
// updateScheduledQueryRoute(router);
};

View file

@ -6,28 +6,34 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const readScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
export const readScheduledQueryGroupRoute = (
router: IRouter,
osqueryContext: OsqueryAppContext
) => {
router.get(
{
path: '/internal/osquery/scheduled_query/{id}',
path: '/internal/osquery/scheduled_query_group/{id}',
validate: {
params: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-readPacks`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
// @ts-expect-error update types
const scheduledQuery = await packagePolicyService?.get(savedObjectsClient, request.params.id);
const scheduledQueryGroup = await packagePolicyService?.get(
savedObjectsClient,
// @ts-expect-error update types
request.params.id
);
return response.ok({
// @ts-expect-error update types
body: scheduledQuery,
body: { item: scheduledQueryGroup },
});
}
);

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { PLUGIN_ID } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { savedQuerySavedObjectType } from '../../../common/types';
@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => {
params: schema.object({}, { unknowns: 'allow' }),
body: schema.object({}, { unknowns: 'allow' }),
},
options: { tags: [`access:${PLUGIN_ID}-writePacks`] },
},
async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common';
import { IRouter } from '../../../../../../src/core/server';
import { OsqueryAppContext } from '../../lib/osquery_app_context_services';
@ -14,16 +14,10 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
{
path: '/internal/osquery/status',
validate: false,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const isSuperUser = osqueryContext.security.authc
.getCurrentUser(request)
?.roles.includes('superuser');
if (!isSuperUser) {
return response.ok({ body: undefined });
}
const packageInfo = await osqueryContext.service
.getPackageService()

View file

@ -23,6 +23,6 @@ export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = {
export const usageMetricType: SavedObjectsType = {
name: usageMetricSavedObjectType,
hidden: false,
namespaceType: 'single',
namespaceType: 'agnostic',
mappings: usageMetricSavedObjectMappings,
};

View file

@ -23,7 +23,7 @@ import { OsqueryFactory } from './factory/types';
export const osquerySearchStrategyProvider = <T extends FactoryQueryTypes>(
data: PluginStart
): ISearchStrategy<StrategyRequestType<T>, StrategyResponseType<T>> => {
const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY);
let es: typeof data.search.searchAsInternalUser;
return {
search: (request, options, deps) => {
@ -32,20 +32,35 @@ export const osquerySearchStrategyProvider = <T extends FactoryQueryTypes>(
}
const queryFactory: OsqueryFactory<T> = osqueryFactory[request.factoryQueryType];
const dsl = queryFactory.buildDsl(request);
return es.search({ ...request, params: dsl }, options, deps).pipe(
map((response) => {
return {
...response,
...{
rawResponse: shimHitsTotal(response.rawResponse),
},
};
}),
mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))
);
// use internal user for searching .fleet* indicies
es = dsl.index?.includes('fleet')
? data.search.searchAsInternalUser
: data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY);
return es
.search(
{
...request,
params: dsl,
},
options,
deps
)
.pipe(
map((response) => {
return {
...response,
...{
rawResponse: shimHitsTotal(response.rawResponse),
},
};
}),
mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))
);
},
cancel: async (id, options, deps) => {
if (es.cancel) {
if (es?.cancel) {
return es.cancel(id, options, deps);
}
},

View file

@ -11,11 +11,12 @@ import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers
import { CollectorDependencies, usageSchema, UsageData } from './types';
export type RegisterCollector = (deps: CollectorDependencies) => void;
export async function getInternalSavedObjectsClient(core: CoreSetup) {
return core.getStartServices().then(async ([coreStart]) => {
return coreStart.savedObjects.createInternalRepository();
});
}
export const getInternalSavedObjectsClient = async (
getStartServices: CoreSetup['getStartServices']
) => {
const [coreStart] = await getStartServices();
return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
};
export const registerCollector: RegisterCollector = ({ core, osqueryContext, usageCollection }) => {
if (!usageCollection) {
@ -26,7 +27,8 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa
schema: usageSchema,
isReady: () => true,
fetch: async ({ esClient }: CollectorFetchContext): Promise<UsageData> => {
const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core));
const savedObjectsClient = await getInternalSavedObjectsClient(core.getStartServices);
return {
beat_metrics: {
usage: await getBeatUsage(esClient),

View file

@ -115,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
'logs',
'maps',
'observabilityCases',
'osquery',
'uptime',
'siem',
'fleet',

View file

@ -70,6 +70,19 @@ export default function ({ getService }: FtrProviderContext) {
indexPatterns: ['all', 'read'],
savedObjectsManagement: ['all', 'read'],
timelion: ['all', 'read'],
osquery: [
'all',
'read',
'minimal_all',
'minimal_read',
'live_queries_all',
'live_queries_read',
'run_saved_queries',
'saved_queries_all',
'saved_queries_read',
'packs_all',
'packs_read',
],
},
reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
};

View file

@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) {
logs: ['all', 'read'],
uptime: ['all', 'read'],
apm: ['all', 'read'],
osquery: ['all', 'read'],
ml: ['all', 'read'],
siem: ['all', 'read'],
fleet: ['all', 'read'],

View file

@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
catalogueId !== 'ml' &&
catalogueId !== 'ml_file_data_visualizer' &&
catalogueId !== 'monitoring' &&
catalogueId !== 'osquery' &&
!esFeatureExceptions.includes(catalogueId)
);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
@ -74,6 +75,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
'appSearch',
'workplaceSearch',
'spaces',
'osquery',
...esFeatureExceptions,
];
const expected = mapValues(

View file

@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.except('ml', 'monitoring')
navLinksBuilder.except('ml', 'monitoring', 'osquery')
);
break;
case 'everything_space_all at everything_space':
@ -57,7 +57,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
'monitoring',
'enterpriseSearch',
'appSearch',
'workplaceSearch'
'workplaceSearch',
'osquery'
)
);
break;

View file

@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
catalogueId !== 'ml' &&
catalogueId !== 'monitoring' &&
catalogueId !== 'ml_file_data_visualizer' &&
catalogueId !== 'osquery' &&
!esFeatureExceptions.includes(catalogueId)
);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
@ -70,6 +71,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
'enterpriseSearch',
'appSearch',
'workplaceSearch',
'osquery',
...esFeatureExceptions,
];
const expected = mapValues(

View file

@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.except('ml', 'monitoring')
navLinksBuilder.except('ml', 'monitoring', 'osquery')
);
break;
case 'read':
@ -55,7 +55,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
'monitoring',
'enterpriseSearch',
'appSearch',
'workplaceSearch'
'workplaceSearch',
'osquery'
)
);
break;