[EDR Workflows][Osquery] Osquery space awareness (#221606)

## Summary

This PR introduces Fleet’s agent space awareness to the Osquery plugin.
The primary goal is to ensure that agents, policies, packs, and queries
remain isolated to their respective Kibana spaces. Specifically:

1. Agents enrolled in policy A (belonging to Space A) are not accessible
from Space B—live queries cannot be performed on them from other spaces.
2. Packs created in Space A are not visible in Space B.
3. Saved queries created in Space A are not visible in Space B.
4. Agent policies, agents, and space-exclusive operating systems from
Space A are not visible from Space B.
5. Live query history is only visible in a space if it includes agents
from that space.

## Implementation Details

- All usages of the Saved Object Client have been updated to use the
space-aware SO client, ensuring queries only return results from the
current space.
- When creating live queries, we now add a `space_id` field to
`.logs-osquery_manager.actions-default` documents. This field stores the
integration policy (osquery_manager) IDs of the agents involved in each
query.
- When reading live queries, searches are filtered to include only
documents with `policy_ids` that are present in the current space.

This ensures strict separation of Osquery data and actions across
different spaces.

## Feature Flag Considerations

At this stage, we believe no feature flag or gating is necessary. In all
cases, we default to the `default` space ID. We have tested these
changes with the feature flag turned off and observed the following:

| Scenario                                             | Result      |
|------------------------------------------------------|:-----------:|
| Agents and policies are visible in the Live query UI |
 |
| Users can query different agents/policies |  |
| Live queries are saved in the live query history | 
|
| Users can create both packs and saved queries |  |
| Users can run packs and saved queries |  |
| Users can enable packs for interval execution |  |

Based on these results, we conclude that this PR can be safely merged
without enabling the feature flag, as it does not impact current
operations.

**The index mappings for `.logs-osquery_manager.actions-default` will be
updated to introduce the `policy_ids` field. This field will also be
populated when actions are created. This change is fully backward
compatible and does not introduce any breaking changes.**

### Enable feature flags

```yaml
xpack.securitySolution.enableExperimental:
  - endpointManagementSpaceAwarenessEnabled

xpack.fleet.enableExperimental:
  - useSpaceAwareness
```

### Switch Fleet to Space aware

```http request
POST /internal/fleet/enable_space_awareness
Elastic-Api-Version: 1,
```

## Manual Test Cases for Space Awareness (Feature Enabled)

### Setup
- **Space A:** Policies `PolicyA1`, `PolicyA2`, `PolicyAB` (each with an
enrolled agent)
- **Space B:** Policies `PolicyB1`, `PolicyB2`, `PolicyAB` (each with an
enrolled agent)
- `PolicyAB` is present in both spaces

---

### 1. Agent Visibility

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, view the list of agents and policies | Only agents for
`PolicyA1`, `PolicyA2`, `PolicyAB` are visible |  |
| In Space B, view the list of agents and policies | Only agents for
`PolicyB1`, `PolicyB2`, `PolicyAB` are visible |  |
| `PolicyAB`'s agents are visible in both spaces | Agents enrolled in
`PolicyAB` are visible in both spaces |  |

---

### 2. Live Query Permissions

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, attempt a live query on an agent from `PolicyB1` or
`PolicyB2`| Agent is not available for selection/query |
 |
| In Space B, attempt a live query on an agent from `PolicyA1` or
`PolicyA2`| Agent is not available for selection/query |
 |
| In either space, perform a live query on an agent from `PolicyAB` |
Query is allowed and succeeds |  |
| In Space A, attempt a live query on an agent from `PolicyA1` or
`PolicyA2`| Query is allowed and succeeds |  |
| In Space B, attempt a live query on an agent from `PolicyB1` or
`PolicyB2`| Query is allowed and succeeds |  |

---

### 3. Packs and Saved Queries

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, create a pack and a saved query | Only visible in Space A
|  |
| In Space B, create a pack and a saved query | Only visible in Space B
|  |
| In Space A, check for packs/queries created in Space B | Not visible |
 |
| In Space B, check for packs/queries created in Space A | Not visible |
 |
| In Space A, live query a pack that has Agent A and Agent AB. | In
space B user doesnt see results from Agent A |  |
---

### 4. Live Query History

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, view live query history | Only queries involving agents
from Space A (including `PolicyAB`) are visible |  |
| In Space B, view live query history | Only queries involving agents
from Space B (including `PolicyAB`) are visible |  |

---

### 5. Pack Execution

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, run a pack on agents from `PolicyA1`, `PolicyA2`,
`PolicyAB` | Execution succeeds |  |
| In Space B, run a pack on agents from `PolicyB1`, `PolicyB2`,
`PolicyAB` | Execution succeeds |  |
| Attempt to run a pack on an agent from another space | Not possible;
agent not available for selection |  |

---

### 6. Interval Execution of Packs

| Test Step | Expected Result | Pass |

|---------------------------------------------------------------------------|------------------------------------------------------|----------|
| In Space A, enable interval execution for a pack | Only
agents/policies in Space A are affected |  |
| In Space B, enable interval execution for a pack | Only
agents/policies in Space B are affected |  |

closes https://github.com/elastic/kibana/pull/222935

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Konrad Szwarc 2025-06-23 16:37:56 +02:00 committed by GitHub
parent 3b6a0dcd1e
commit b33c211b5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1790 additions and 255 deletions

View file

@ -539,6 +539,8 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
logger.debug(`updating policy [${request.params.agentPolicyId}] in space [${spaceId}]`);
try {
const requestSpaceId = spaceId;
if (spaceIds?.length) {
const authorizedSpaces = await checkAgentPoliciesAllPrivilegesForSpaces(
request,
@ -564,7 +566,7 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
esClient,
request.params.agentPolicyId,
data,
{ force, bumpRevision, user, spaceId }
{ force, bumpRevision, user, spaceId, requestSpaceId }
);
let item: any = agentPolicy;

View file

@ -358,7 +358,8 @@ class AgentPolicyService {
public async runExternalCallbacks(
externalCallbackType: ExternalCallback[0],
agentPolicy: NewAgentPolicy | Partial<AgentPolicy> | AgentPolicy
agentPolicy: NewAgentPolicy | Partial<AgentPolicy> | AgentPolicy,
requestSpaceId?: string
): Promise<NewAgentPolicy | Partial<AgentPolicy> | AgentPolicy> {
const logger = this.getLogger('runExternalCallbacks');
try {
@ -387,7 +388,8 @@ class AgentPolicyService {
}
if (externalCallbackType === 'agentPolicyPostUpdate') {
result = await (callback as PostAgentPolicyPostUpdateCallback)(
newAgentPolicy as AgentPolicy
newAgentPolicy as AgentPolicy,
requestSpaceId
);
updatedNewAgentPolicy = result;
}
@ -828,6 +830,7 @@ class AgentPolicyService {
authorizationHeader?: HTTPAuthorizationHeader | null;
skipValidation?: boolean;
bumpRevision?: boolean;
requestSpaceId?: string;
}
): Promise<AgentPolicy> {
const logger = this.getLogger('update');
@ -906,7 +909,8 @@ class AgentPolicyService {
.then((updatedAgentPolicy) => {
return this.runExternalCallbacks(
'agentPolicyPostUpdate',
updatedAgentPolicy
updatedAgentPolicy,
options?.requestSpaceId ?? DEFAULT_SPACE_ID
) as unknown as AgentPolicy;
})
.then((response) => {

View file

@ -76,7 +76,10 @@ export interface PackageService {
}
export interface PackageClient {
getInstallation(pkgName: string): Promise<Installation | undefined>;
getInstallation(
pkgName: string,
savedObjectsClient?: SavedObjectsClientContract
): Promise<Installation | undefined>;
ensureInstalledPackage(options: {
pkgName: string;
@ -208,11 +211,14 @@ class PackageClientImpl implements PackageClient {
}
}
public async getInstallation(pkgName: string) {
public async getInstallation(
pkgName: string,
savedObjectsClient: SavedObjectsClientContract = this.internalSoClient
) {
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return getInstallation({
pkgName,
savedObjectsClient: this.internalSoClient,
savedObjectsClient,
});
}

View file

@ -76,7 +76,10 @@ export type PostAgentPolicyUpdateCallback = (
agentPolicy: Partial<AgentPolicy>
) => Promise<Partial<AgentPolicy>>;
export type PostAgentPolicyPostUpdateCallback = (agentPolicy: AgentPolicy) => Promise<AgentPolicy>;
export type PostAgentPolicyPostUpdateCallback = (
agentPolicy: AgentPolicy,
requestSpaceId?: string
) => Promise<AgentPolicy>;
export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback];
export type ExternalCallbackPostCreate = [

View file

@ -31,6 +31,7 @@ export interface ActionDetails {
user_id?: string;
pack_id?: string;
pack_name?: string;
space_id?: string;
pack_prebuilt?: boolean;
status?: string;
queries?: Array<{
@ -46,7 +47,9 @@ export interface ActionDetails {
}>;
}
export type ActionsRequestOptions = RequestOptionsPaginated;
export interface ActionsRequestOptions extends RequestOptionsPaginated {
spaceId: string;
}
export interface ActionDetailsStrategyResponse extends IEsSearchResponse {
actionDetails: estypes.SearchHit<ActionDetails>;
@ -55,6 +58,7 @@ export interface ActionDetailsStrategyResponse extends IEsSearchResponse {
export interface ActionDetailsRequestOptions extends RequestOptions {
actionId: string;
spaceId: string;
}
export interface ActionResultsStrategyResponse

View file

@ -30,7 +30,8 @@
"home",
"lens",
"telemetry",
"cases"
"cases",
"spaces"
],
"requiredBundles": [
"esUiShared",

View file

@ -23,6 +23,9 @@ export const actionsMapping: MappingTypeMapping = {
type: 'keyword',
ignore_above: 1024,
},
space_id: {
type: 'keyword',
},
data: {
properties: {
query: {

View file

@ -0,0 +1,437 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { actionsMapping } from './actions_mapping';
import { actionResponsesMapping } from './action_responses_mapping';
import {
isSubsetMapping,
createIndexIfNotExists,
initializeTransformsIndices,
ACTIONS_INDEX_NAME,
ACTIONS_INDEX_DEFAULT_NS,
ACTION_RESPONSES_INDEX_NAME,
ACTION_RESPONSES_INDEX_DEFAULT_NS,
} from './create_transforms_indices';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
interface IndexTemplateResponse {
index_templates: Array<{
name: string;
index_template: {
template: {
mappings: Record<string, unknown>;
};
};
}>;
}
interface MockEsClient {
indices: {
exists: jest.Mock<Promise<boolean>>;
putIndexTemplate: jest.Mock<Promise<{ acknowledged: boolean }>>;
create: jest.Mock<Promise<{ acknowledged: boolean }>>;
getIndexTemplate: jest.Mock<Promise<IndexTemplateResponse>>;
putMapping: jest.Mock<Promise<{ acknowledged: boolean }>>;
};
}
interface EsIndexMappings {
properties: Record<string, unknown>;
}
const createMockEsClient = (): MockEsClient => ({
indices: {
exists: jest.fn().mockResolvedValue(false),
putIndexTemplate: jest.fn().mockResolvedValue({ acknowledged: true }),
create: jest.fn().mockResolvedValue({ acknowledged: true }),
getIndexTemplate: jest.fn().mockResolvedValue({
index_templates: [
{
name: 'test',
index_template: {
template: {
mappings: {},
},
},
},
],
}),
putMapping: jest.fn().mockResolvedValue({ acknowledged: true }),
},
});
const logger: Logger = {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
get: jest.fn(() => logger),
} as Partial<Logger> as Logger;
let mockEsClient: ReturnType<typeof createMockEsClient>;
function makeEsIndexMappings(
overrides: Partial<EsIndexMappings['properties']> = {}
): EsIndexMappings {
return {
properties: {
pack_name: { ignore_above: 1024, type: 'keyword' },
metadata: { type: 'object', enabled: false },
data: {
properties: {
query: { ignore_above: 1024, type: 'keyword' },
},
},
pack_id: { ignore_above: 1024, type: 'keyword' },
space_id: { type: 'keyword' },
input_type: { ignore_above: 1024, type: 'keyword' },
pack_prebuilt: { type: 'boolean' },
type: { ignore_above: 1024, type: 'keyword' },
queries: {
properties: {
action_id: { ignore_above: 1024, type: 'keyword' },
saved_query_id: { ignore_above: 1024, type: 'keyword' },
saved_query_prebuilt: { type: 'boolean' },
query: { type: 'text' },
id: { ignore_above: 1024, type: 'keyword' },
version: { ignore_above: 1024, type: 'keyword' },
ecs_mapping: { type: 'object', enabled: false },
platform: { ignore_above: 1024, type: 'keyword' },
agents: { ignore_above: 1024, type: 'keyword' },
...((overrides.queries as any)?.properties || {}),
},
...((overrides.queries as any) || {}),
},
agents: { ignore_above: 1024, type: 'keyword' },
'@timestamp': { type: 'date' },
action_id: { ignore_above: 1024, type: 'keyword' },
user_id: { ignore_above: 1024, type: 'keyword' },
expiration: { type: 'date' },
event: {
properties: {
agent_id_status: { ignore_above: 1024, type: 'keyword' },
ingested: {
format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis',
type: 'date',
},
...((overrides.event as any)?.properties || {}),
},
...((overrides.event as any) || {}),
},
agent_ids: { ignore_above: 1024, type: 'keyword' },
...overrides,
},
};
}
describe('createTransformIndices', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('isSubsetMapping', () => {
it('returns true for exact match', () => {
const mapping = { foo: { type: 'keyword' } };
expect(isSubsetMapping(mapping, mapping, logger)).toBe(true);
});
it('returns true if current has extra fields', () => {
const desired = { foo: { type: 'keyword' } };
const current = { foo: { type: 'keyword' }, bar: { type: 'text' } };
expect(isSubsetMapping(desired, current, logger)).toBe(true);
});
it('returns false if current is missing a field', () => {
const desired = { foo: { type: 'keyword' }, bar: { type: 'text' } };
const current = { foo: { type: 'keyword' } };
expect(isSubsetMapping(desired, current, logger)).toBe(false);
});
it('returns false if a field has a different value', () => {
const desired = { foo: { type: 'keyword' } };
const current = { foo: { type: 'text' } };
expect(isSubsetMapping(desired, current, logger)).toBe(false);
});
it('handles nested properties correctly', () => {
const desired = {
properties: {
foo: { type: 'keyword' },
},
};
const current = {
properties: {
foo: { type: 'keyword' },
bar: { type: 'text' },
},
extra: 'ignored',
};
expect(isSubsetMapping(desired, current, logger)).toBe(true);
});
it('returns false for missing nested property', () => {
const desired = {
properties: {
foo: { type: 'keyword' },
bar: { type: 'text' },
},
};
const current = {
properties: {
foo: { type: 'keyword' },
},
};
expect(isSubsetMapping(desired, current, logger)).toBe(false);
});
it('returns true for leaf value match', () => {
expect(isSubsetMapping({ foo: 'a' }, { foo: 'a' }, logger)).toBe(true);
});
it('returns false for leaf value mismatch', () => {
expect(isSubsetMapping({ foo: 'a' }, { foo: 'b' }, logger)).toBe(false);
});
describe('Using mocked data', () => {
it('returns true for real-world mapping vs index mappings', () => {
const esIndexMappings = makeEsIndexMappings();
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(true);
});
it('returns false if a required field is missing in ES mapping', () => {
const esIndexMappings = makeEsIndexMappings();
delete esIndexMappings.properties.space_id;
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(false);
});
it('returns false if a field type mismatches', () => {
const esIndexMappings = makeEsIndexMappings({
action_id: { type: 'text' }, // should be 'keyword'
});
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(false);
});
it('returns true if ES mapping has extra fields', () => {
const esIndexMappings = makeEsIndexMappings({ extra_field: { type: 'keyword' } });
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(true);
});
it('returns false if a nested property is missing', () => {
const esIndexMappings = makeEsIndexMappings();
delete (esIndexMappings.properties.queries as { properties: Record<string, unknown> })
.properties.action_id;
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(false);
});
it('returns false if a nested property type mismatches', () => {
const esIndexMappings = makeEsIndexMappings({
queries: {
properties: {
...(makeEsIndexMappings().properties.queries as any).properties,
action_id: { type: 'text' }, // should be 'keyword'
},
},
});
expect(
isSubsetMapping(
actionsMapping as Record<string, unknown>,
esIndexMappings as unknown as Record<string, unknown>,
logger
)
).toBe(false);
});
});
});
describe('createIndexIfNotExists', () => {
beforeEach(() => {
mockEsClient = createMockEsClient();
jest.clearAllMocks();
});
it('should create index and template if they do not exist', async () => {
mockEsClient.indices.exists.mockResolvedValueOnce(false);
mockEsClient.indices.putIndexTemplate.mockResolvedValueOnce({ acknowledged: true } as any);
mockEsClient.indices.create.mockResolvedValueOnce({ acknowledged: true } as any);
await createIndexIfNotExists(
mockEsClient as unknown as ElasticsearchClient,
'test-index',
'.logs-test-index-default',
actionsMapping,
logger
);
expect(mockEsClient.indices.exists).toHaveBeenCalledWith({
index: '.logs-test-index-default',
});
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenCalledWith({
name: 'test-index',
index_patterns: '.logs-test-index-default',
template: { mappings: actionsMapping },
priority: 500,
});
expect(mockEsClient.indices.create).toHaveBeenCalledWith({
index: '.logs-test-index-default',
mappings: actionsMapping,
});
expect(logger.debug).toHaveBeenCalledWith('Index test-index does not exist, creating...');
});
it('should update index template if mappings are outdated', async () => {
const currentMappings = {
properties: {
...actionsMapping.properties,
// Remove a field to simulate outdated mappings
pack_name: undefined,
},
};
mockEsClient.indices.exists.mockResolvedValueOnce(true);
mockEsClient.indices.getIndexTemplate.mockResolvedValueOnce({
index_templates: [
{
name: 'test-index',
index_template: {
template: {
mappings: currentMappings,
},
},
},
],
} as any);
mockEsClient.indices.putIndexTemplate.mockResolvedValueOnce({ acknowledged: true } as any);
mockEsClient.indices.putMapping.mockResolvedValueOnce({ acknowledged: true } as any);
await createIndexIfNotExists(
mockEsClient as unknown as ElasticsearchClient,
'test-index',
'.logs-test-index-default',
actionsMapping,
logger
);
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenCalledWith({
name: 'test-index',
index_patterns: '.logs-test-index-default',
template: { mappings: actionsMapping },
priority: 500,
});
expect(mockEsClient.indices.putMapping).toHaveBeenCalledWith({
index: '.logs-test-index-default',
body: { properties: actionsMapping.properties },
});
expect(logger.debug).toHaveBeenCalledWith(
'Index test-index already exists, checking template...'
);
expect(logger.debug).toHaveBeenCalledWith(
'Mappings for "test-index" are outdated. Updating mappings...'
);
});
it('should log error if index creation fails', async () => {
const error = new Error('Failed to create index');
mockEsClient.indices.exists.mockRejectedValueOnce(error);
await createIndexIfNotExists(
mockEsClient as unknown as ElasticsearchClient,
'test-index',
'.logs-test-index-default',
actionsMapping,
logger
);
expect(logger.error).toHaveBeenCalledWith('Failed to create the index template: test-index');
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed to create index'));
});
});
describe('initializeTransformsIndices', () => {
beforeEach(() => {
mockEsClient = createMockEsClient();
jest.clearAllMocks();
});
it('should initialize both actions and action responses indices', async () => {
mockEsClient.indices.exists.mockResolvedValue(false);
mockEsClient.indices.putIndexTemplate.mockResolvedValue({ acknowledged: true } as any);
mockEsClient.indices.create.mockResolvedValue({ acknowledged: true } as any);
await initializeTransformsIndices(mockEsClient as unknown as ElasticsearchClient, logger);
// Should call createIndexIfNotExists for both indices
expect(mockEsClient.indices.exists).toHaveBeenCalledTimes(2);
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenCalledTimes(2);
expect(mockEsClient.indices.create).toHaveBeenCalledTimes(2);
// Verify actions index creation
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(1, {
name: ACTIONS_INDEX_NAME,
index_patterns: ACTIONS_INDEX_DEFAULT_NS,
template: { mappings: actionsMapping },
priority: 500,
});
// Verify action responses index creation
expect(mockEsClient.indices.putIndexTemplate).toHaveBeenNthCalledWith(2, {
name: ACTION_RESPONSES_INDEX_NAME,
index_patterns: ACTION_RESPONSES_INDEX_DEFAULT_NS,
template: { mappings: actionResponsesMapping },
priority: 500,
});
});
it('should handle errors during initialization', async () => {
const error = new Error('Failed to initialize');
mockEsClient.indices.exists.mockRejectedValueOnce(error);
// The second call to exists will resolve to false
mockEsClient.indices.exists.mockResolvedValueOnce(false);
mockEsClient.indices.putIndexTemplate.mockResolvedValue({ acknowledged: true } as any);
mockEsClient.indices.create.mockResolvedValue({ acknowledged: true } as any);
await initializeTransformsIndices(mockEsClient as unknown as ElasticsearchClient, logger);
// Should still attempt to create both indices even if one fails
expect(mockEsClient.indices.exists).toHaveBeenCalledTimes(2);
expect(logger.error).toHaveBeenCalledWith(
'Failed to create the index template: osquery_manager.actions'
);
});
});
});

View file

@ -5,11 +5,21 @@
* 2.0.
*/
import { transformError } from '@kbn/securitysolution-es-utils';
import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
import type {
MappingTypeMapping,
IndicesPutMappingRequest,
} from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { actionsMapping } from './actions_mapping';
import { actionResponsesMapping } from './action_responses_mapping';
interface ESMappingObject {
[key: string]: ESMappingValue;
}
type ESMappingValue = string | number | boolean | null | ESMappingObject | undefined;
export const ACTIONS_INDEX_NAME = 'osquery_manager.actions';
export const ACTIONS_INDEX_DEFAULT_NS = '.logs-' + ACTIONS_INDEX_NAME + '-default';
@ -42,26 +52,149 @@ export const createIndexIfNotExists = async (
mappings: MappingTypeMapping,
logger: Logger
) => {
const subLogger = logger.get('createIndexIfNotExists');
try {
const isLatestIndexExists = await esClient.indices.exists({
index: indexPattern,
});
if (!isLatestIndexExists) {
subLogger.debug(`Index ${indexTemplateName} does not exist, creating...`);
await esClient.indices.putIndexTemplate({
name: indexTemplateName,
index_patterns: indexPattern,
template: { mappings },
priority: 500,
});
await esClient.indices.create({
index: indexPattern,
mappings,
});
} else {
subLogger.debug(`Index ${indexTemplateName} already exists, checking template...`);
// pull the index template to check if mappings need to be updated
const indexTemplate = await esClient.indices.getIndexTemplate({
name: indexTemplateName,
});
// Find the template with the exact name match
const matchedTemplateObj = indexTemplate.index_templates.find(
(tpl: { name: string }) => tpl.name === indexTemplateName
);
const currentMappings = matchedTemplateObj?.index_template?.template?.mappings || {};
subLogger.debug(`Fetched current mappings for template "${indexTemplateName}"`);
if (isSubsetMapping(mappings, currentMappings as ESMappingObject, subLogger)) {
subLogger.debug(`Mappings for "${indexTemplateName}" are up to date. No update needed.`);
return;
}
subLogger.debug(`Mappings for "${indexTemplateName}" are outdated. Updating mappings...`);
if (mappings.properties) {
await esClient.indices.putIndexTemplate({
name: indexTemplateName,
index_patterns: indexPattern,
template: { mappings },
priority: 500,
});
await esClient.indices.putMapping({
index: indexPattern,
body: (mappings.properties
? { properties: mappings.properties }
: {}) as IndicesPutMappingRequest['body'],
});
subLogger.debug(`Mappings for "${indexTemplateName}" have been updated.`);
} else {
subLogger.error(`No properties found in mappings for "${indexTemplateName}"`);
}
}
} catch (err) {
const error = transformError(err);
logger.error(`Failed to create the index template: ${indexTemplateName}`);
logger.error(error.message);
subLogger.error(`Failed to create the index template: ${indexTemplateName}`);
subLogger.error(error.message);
}
};
/**
* Recursively checks if all fields and configs from `desired` mapping exist in `current` mapping.
* Ignores extra fields in `current`. Logs each major step for traceability.
*
* @param desired - The mapping you want to enforce
* @param current - The mapping fetched from ESccc
* @param logger - Kibana logger instance
* @returns true if all fields/configs in desired are present in current
*/
export function isSubsetMapping(
desired: MappingTypeMapping | ESMappingObject,
current: MappingTypeMapping | ESMappingObject,
logger: Logger
): boolean {
const subLogger = logger.get('isSubsetMapping');
// Handle primitive types or null values
if (typeof desired !== 'object' || desired === null) {
const result = desired === current;
subLogger.debug(`Comparing leaf values: ${desired} === ${current} -> ${result}`);
return result;
}
// Convert both to ESMappingObject to safely access with string keys
const desiredObj = desired as ESMappingObject;
const currentObj = current as ESMappingObject;
for (const key of Object.keys(desiredObj)) {
if (!(key in currentObj)) {
subLogger.debug(`Key "${key}" missing in current mapping`);
return false;
}
const desiredValue = desiredObj[key];
const currentValue = currentObj[key];
const bothAreNonNullObjects =
typeof desiredValue === 'object' &&
desiredValue !== null &&
typeof currentValue === 'object' &&
currentValue !== null;
if (key === 'properties') {
if (bothAreNonNullObjects) {
if (
!isSubsetMapping(desiredValue as ESMappingObject, currentValue as ESMappingObject, logger)
) {
subLogger.debug(`Nested properties mismatch for key "${key}"`);
return false;
}
} else {
subLogger.debug(`Expected both desired and current to be objects for key "${key}"`);
return false;
}
} else if (bothAreNonNullObjects) {
if (
!isSubsetMapping(desiredValue as ESMappingObject, currentValue as ESMappingObject, logger)
) {
subLogger.debug(`Value mismatch for key "${key}"`);
return false;
}
} else if (desiredValue !== currentValue) {
subLogger.debug(`Value mismatch for key "${key}": ${desiredValue} !== ${currentValue}`);
return false;
}
}
return true;
}

View file

@ -8,11 +8,10 @@
import { v4 as uuidv4 } from 'uuid';
import moment from 'moment';
import { filter, isEmpty, isNumber, map, omit, pick, pickBy, some } from 'lodash';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import type { CreateLiveQueryRequestBodySchema } from '../../../common/api';
import { createDynamicQueries, replacedQueries } from './create_queries';
import { getInternalSavedObjectsClient } from '../../routes/utils';
import { parseAgentSelection } from '../../lib/parse_agent_groups';
import { packSavedObjectType } from '../../../common/types';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
@ -21,13 +20,14 @@ import { ACTIONS_INDEX, QUERY_TIMEOUT } from '../../../common/constants';
import { TELEMETRY_EBT_LIVE_QUERY_EVENT } from '../../lib/telemetry/constants';
import type { PackSavedObject } from '../../common/types';
import { CustomHttpRequestError } from '../../common/error';
import { getInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
interface Metadata {
currentUser: string | undefined;
}
interface CreateActionHandlerOptions {
soClient?: SavedObjectsClientContract;
space?: { id: string };
metadata?: Metadata;
alertData?: ParsedTechnicalFields & { _index: string };
error?: string;
@ -40,24 +40,30 @@ export const createActionHandler = async (
) => {
const [coreStartServices] = await osqueryContext.getStartServices();
const esClientInternal = coreStartServices.elasticsearch.client.asInternalUser;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedInternalSavedObjectsClient = getInternalSavedObjectsClientForSpaceId(
coreStartServices,
options.space?.id ?? DEFAULT_SPACE_ID
);
const { soClient, metadata, alertData, error } = options;
const savedObjectsClient = soClient ?? coreStartServices.savedObjects.createInternalRepository();
const { metadata, alertData, error } = options;
const elasticsearchClient = coreStartServices.elasticsearch.client.asInternalUser;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { agent_all, agent_ids, agent_platforms, agent_policy_ids } = params;
const {
agent_all: agentAll,
agent_ids: agentIds,
agent_platforms: agentPlatforms,
agent_policy_ids: agentPolicyIds,
} = params;
const selectedAgents = await parseAgentSelection(
internalSavedObjectsClient,
spaceScopedInternalSavedObjectsClient,
elasticsearchClient,
osqueryContext,
{
agents: agent_ids,
allAgentsSelected: !!agent_all,
platformsSelected: agent_platforms,
policiesSelected: agent_policy_ids,
agents: agentIds,
allAgentsSelected: !!agentAll,
platformsSelected: agentPlatforms,
policiesSelected: agentPolicyIds,
spaceId: options.space?.id ?? DEFAULT_SPACE_ID,
}
);
@ -68,7 +74,10 @@ export const createActionHandler = async (
let packSO;
if (params.pack_id) {
packSO = await savedObjectsClient.get<PackSavedObject>(packSavedObjectType, params.pack_id);
packSO = await spaceScopedInternalSavedObjectsClient.get<PackSavedObject>(
packSavedObjectType,
params.pack_id
);
}
const osqueryAction = {
@ -92,6 +101,7 @@ export const createActionHandler = async (
pack_prebuilt: params.pack_id
? some(packSO?.references, ['type', 'osquery-pack-asset'])
: undefined,
space_id: options.space?.id ?? DEFAULT_SPACE_ID,
queries: packSO
? map(convertSOQueriesToPack(packSO.attributes.queries), (packQuery, packQueryId) => {
const replacedQuery = replacedQueries(packQuery.query, alertData);
@ -117,6 +127,8 @@ export const createActionHandler = async (
agents: selectedAgents,
osqueryContext,
error,
spaceId: options.space?.id ?? DEFAULT_SPACE_ID,
spaceScopedClient: spaceScopedInternalSavedObjectsClient,
}),
};

View file

@ -13,6 +13,11 @@ import type { OsqueryActiveLicenses } from './validate_license';
import { validateLicense } from './validate_license';
import { createActionHandler } from './create_action_handler';
export interface CreateActionOptions {
alertData?: ParsedTechnicalFields & { _index: string };
space?: { id: string };
}
export const createActionService = (osqueryContext: OsqueryAppContext) => {
let licenseSubscription: Subscription | null = null;
const licenses: OsqueryActiveLicenses = { isActivePlatinumLicense: false };
@ -21,13 +26,19 @@ export const createActionService = (osqueryContext: OsqueryAppContext) => {
licenses.isActivePlatinumLicense = license.isActive && license.hasAtLeast('platinum');
});
const logger = osqueryContext.logFactory.get('createActionService');
const create = async (
params: CreateLiveQueryRequestBodySchema,
alertData?: ParsedTechnicalFields & { _index: string }
options?: CreateActionOptions
) => {
const error = validateLicense(licenses);
return createActionHandler(osqueryContext, params, { alertData, error });
return createActionHandler(osqueryContext, params, {
alertData: options?.alertData,
space: options?.space,
error,
});
};
const stop = () => {
@ -39,5 +50,6 @@ export const createActionService = (osqueryContext: OsqueryAppContext) => {
return {
create,
stop,
logger,
};
};

View file

@ -9,8 +9,11 @@ import { createDynamicQueries } from './create_queries';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { PARAMETER_NOT_FOUND } from '../../../common/translations/errors';
import type { SavedObjectsClient } from '@kbn/core/server';
describe('create queries', () => {
const spaceId = 'default';
const mockSavedObjectsClient = {} as unknown as SavedObjectsClient;
const defualtQueryParams = {
interval: 3600,
platform: 'linux',
@ -62,6 +65,8 @@ describe('create queries', () => {
},
} as unknown as ParsedTechnicalFields & { _index: string },
osqueryContext: {} as OsqueryAppContext,
spaceId,
spaceScopedClient: mockSavedObjectsClient,
});
expect(queries[0].query).toBe(`SELECT * FROM processes where pid=${pid};`);
expect(queries[0].error).toBe(undefined);
@ -79,6 +84,8 @@ describe('create queries', () => {
process: { pid },
} as unknown as ParsedTechnicalFields & { _index: string },
osqueryContext: {} as OsqueryAppContext,
spaceId,
spaceScopedClient: mockSavedObjectsClient,
});
expect(queries[0].query).toBe(`SELECT * FROM processes where pid=${pid};`);
expect(queries[0].error).toBe(undefined);
@ -91,6 +98,8 @@ describe('create queries', () => {
process: {},
} as unknown as ParsedTechnicalFields & { _index: string },
osqueryContext: {} as OsqueryAppContext,
spaceId,
spaceScopedClient: mockSavedObjectsClient,
});
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
expect(queries[0].error).toBe(PARAMETER_NOT_FOUND);
@ -100,6 +109,8 @@ describe('create queries', () => {
params: mockedQueriesParams,
agents: [TEST_AGENT],
osqueryContext: {} as OsqueryAppContext,
spaceId,
spaceScopedClient: mockSavedObjectsClient,
});
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
expect(queries[0].agents).toContain(TEST_AGENT);
@ -114,6 +125,8 @@ describe('create queries', () => {
params: mockedSingleQueryParams,
agents: [TEST_AGENT],
osqueryContext: {} as OsqueryAppContext,
spaceId,
spaceScopedClient: mockSavedObjectsClient,
});
expect(queries[0].query).toBe('SELECT * FROM processes where pid={{process.pid}};');
expect(queries[0].agents).toContain(TEST_AGENT);

View file

@ -9,6 +9,7 @@ import { isEmpty, isNumber, map, pickBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal';
import type { CreateLiveQueryRequestBodySchema } from '../../../common/api';
import { PARAMETER_NOT_FOUND } from '../../../common/translations/errors';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
@ -21,6 +22,8 @@ interface CreateDynamicQueriesParams {
agents: string[];
osqueryContext: OsqueryAppContext;
error?: string;
spaceId: string;
spaceScopedClient: SavedObjectsClient;
}
export const createDynamicQueries = async ({
@ -29,6 +32,8 @@ export const createDynamicQueries = async ({
agents,
osqueryContext,
error,
spaceId,
spaceScopedClient,
}: CreateDynamicQueriesParams) =>
params.queries?.length
? map(params.queries, ({ query, ...restQuery }) => {
@ -56,7 +61,9 @@ export const createDynamicQueries = async ({
saved_query_prebuilt: params.saved_query_id
? await isSavedQueryPrebuilt(
osqueryContext.service.getPackageService()?.asInternalUser,
params.saved_query_id
params.saved_query_id,
spaceScopedClient,
spaceId
)
: undefined,
ecs_mapping: params.ecs_mapping,

View file

@ -5,12 +5,75 @@
* 2.0.
*/
import type { SavedObjectReference, SavedObjectsClient } from '@kbn/core/server';
import type { CoreStart, SavedObjectReference, SavedObjectsClient } from '@kbn/core/server';
import { filter, map } from 'lodash';
import type { PostPackagePolicyPostDeleteCallback } from '@kbn/fleet-plugin/server';
import type {
PackagePolicy,
PostAgentPolicyPostUpdateCallback,
} from '@kbn/fleet-plugin/server/types';
import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { packSavedObjectType } from '../../common/types';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
import { getInternalSavedObjectsClientForSpaceId } from '../utils/get_internal_saved_object_client';
export const getAgentPolicyPostUpdateCallback =
(core: CoreStart): PostAgentPolicyPostUpdateCallback =>
async (updatedAgentPolicy, requestSpaceId) => {
const hasOsqueryIntegration =
Array.isArray(updatedAgentPolicy.package_policies) &&
updatedAgentPolicy.package_policies.some(
(pkg: PackagePolicy) => pkg?.package?.name === OSQUERY_INTEGRATION_NAME
);
if (!hasOsqueryIntegration) {
return updatedAgentPolicy;
}
if (requestSpaceId) {
if (!(updatedAgentPolicy.space_ids ?? []).includes(requestSpaceId)) {
const spaceScopedSavedObjectClient = getInternalSavedObjectsClientForSpaceId(
core,
requestSpaceId
);
const foundPacks = await spaceScopedSavedObjectClient.find({
type: packSavedObjectType,
hasReference: {
type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE,
id: updatedAgentPolicy.id,
},
perPage: 10000,
});
if (foundPacks.saved_objects.length) {
const policyId = updatedAgentPolicy.id;
await Promise.all(
map(
foundPacks.saved_objects,
(pack: {
id: string;
references: SavedObjectReference[];
attributes: { shards: Array<{ key: string; value: string }> };
}) =>
spaceScopedSavedObjectClient.update(
packSavedObjectType,
pack.id,
{
shards: filter(pack.attributes.shards, (shard) => shard.key !== policyId),
},
{
references: filter(pack.references, (reference) => reference.id !== policyId),
}
)
)
);
}
}
}
return updatedAgentPolicy;
};
export const getPackagePolicyDeleteCallback =
(packsClient: SavedObjectsClient): PostPackagePolicyPostDeleteCallback =>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { CoreSetup, Logger, LoggerFactory } from '@kbn/core/server';
import type { CoreSetup, KibanaRequest, Logger, LoggerFactory } from '@kbn/core/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type {
AgentService,
@ -17,6 +17,7 @@ import type {
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions';
import type { Space, SpacesServiceStart } from '@kbn/spaces-plugin/server';
import type { ConfigType } from '../../common/config';
import type { TelemetryEventsSender } from './telemetry/sender';
@ -34,6 +35,7 @@ export type OsqueryAppContextServiceStartContract = Partial<
config: ConfigType;
registerIngestCallback?: FleetStartContract['registerExternalCallback'];
ruleRegistryService?: RuleRegistryPluginStartContract;
spacesService: SpacesServiceStart | undefined;
};
/**
@ -47,6 +49,7 @@ export class OsqueryAppContextService {
private agentPolicyService: AgentPolicyServiceInterface | undefined;
private ruleRegistryService: RuleRegistryPluginStartContract | undefined;
private fleetActionsClient: FleetActionsClientInterface | undefined;
private spacesService: SpacesServiceStart | undefined;
public start(dependencies: OsqueryAppContextServiceStartContract) {
this.agentService = dependencies.agentService;
@ -55,6 +58,7 @@ export class OsqueryAppContextService {
this.agentPolicyService = dependencies.agentPolicyService;
this.ruleRegistryService = dependencies.ruleRegistryService;
this.fleetActionsClient = dependencies.createFleetActionsClient?.('osquery');
this.spacesService = dependencies.spacesService;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -83,6 +87,9 @@ export class OsqueryAppContextService {
public getFleetActionsClient(): FleetActionsClientInterface | undefined {
return this.fleetActionsClient;
}
public getActiveSpace(httpRequest: KibanaRequest): Promise<Space> | undefined {
return this.spacesService?.getActiveSpace(httpRequest);
}
}
/**

View file

@ -17,6 +17,7 @@ export interface AgentSelection {
allAgentsSelected?: boolean;
platformsSelected?: string[];
policiesSelected?: string[];
spaceId: string;
}
const PER_PAGE = 9000;
@ -90,7 +91,9 @@ export const parseAgentSelection = async (
policiesSelected = [],
agents = [],
} = agentSelection;
const agentService = context.service.getAgentService()?.asInternalUser;
const agentService = context.service
.getAgentService()
?.asInternalScopedUser(agentSelection.spaceId);
const packagePolicyService = context.service.getPackagePolicyService();
const kueryFragments = ['status:online'];
@ -172,5 +175,16 @@ export const parseAgentSelection = async (
agents.forEach(addAgent);
return Array.from(selectedAgents);
const selectedAgentsArray = Array.from(selectedAgents);
// validate if all selected agents are in current space. If not, getByIds will throw an error, caught by caller
try {
await agentService?.getByIds(selectedAgentsArray, { ignoreMissing: false });
return selectedAgentsArray;
} catch (error) {
context.logFactory.get().error(error);
return [];
}
};

View file

@ -101,6 +101,7 @@ export class TelemetryEventsSender {
pack_id: { type: 'keyword', _meta: { description: '', optional: true } },
pack_name: { type: 'keyword', _meta: { description: '', optional: true } },
pack_prebuilt: { type: 'boolean', _meta: { description: '', optional: true } },
space_id: { type: 'keyword', _meta: { description: '', optional: true } },
},
});

View file

@ -35,7 +35,10 @@ import type { OsqueryAppContext } from './lib/osquery_app_context_services';
import { OsqueryAppContextService } from './lib/osquery_app_context_services';
import type { ConfigType } from '../common/config';
import { OSQUERY_INTEGRATION_NAME } from '../common';
import { getPackagePolicyDeleteCallback } from './lib/fleet_integration';
import {
getPackagePolicyDeleteCallback,
getAgentPolicyPostUpdateCallback,
} from './lib/fleet_integration';
import { TelemetryEventsSender } from './lib/telemetry/sender';
import { TelemetryReceiver } from './lib/telemetry/receiver';
import { initializeTransformsIndices } from './create_indices/create_transforms_indices';
@ -82,6 +85,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
initSavedObjects(core.savedObjects);
// TODO: We do not pass so client here.
this.createActionService = createActionService(osqueryContext);
core
@ -119,6 +123,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
config: this.config!,
logger: this.logger,
registerIngestCallback,
spacesService: plugins.spaces?.spacesService,
});
this.telemetryReceiver.start(core, this.osqueryAppContextService);
@ -192,6 +197,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
);
registerIngestCallback('packagePolicyPostDelete', getPackagePolicyDeleteCallback(client));
registerIngestCallback('agentPolicyPostUpdate', getAgentPolicyPostUpdateCallback(core));
}
})
.catch(() => {

View file

@ -7,6 +7,7 @@
import type { IRouter } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { API_VERSIONS } from '../../../common/constants';
import { PLUGIN_ID } from '../../../common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
@ -33,12 +34,15 @@ export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryApp
},
},
async (context, request, response) => {
const space = await osqueryContext.service.getActiveSpace(request);
let agent;
try {
agent = await osqueryContext.service
.getAgentService()
?.asInternalUser?.getAgent(request.params.id);
?.asInternalScopedUser(space?.id ?? DEFAULT_SPACE_ID)
?.getAgent(request.params.id);
} catch (err) {
return response.notFound();
}

View file

@ -11,10 +11,11 @@ import { satisfies } from 'semver';
import type { GetAgentPoliciesResponseItem, PackagePolicy } from '@kbn/fleet-plugin/common';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { API_VERSIONS } from '../../../common/constants';
import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { getInternalSavedObjectsClient } from '../utils';
export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
@ -33,35 +34,33 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp
validate: {},
},
async (context, request, response) => {
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const space = await osqueryContext.service.getActiveSpace(request);
const agentService = osqueryContext.service.getAgentService();
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const { items: packagePolicies } = (await packagePolicyService?.list(
internalSavedObjectsClient,
{
const { items: packagePolicies } = (await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 1000,
page: 1,
}
)) ?? { items: [] as PackagePolicy[] };
})) ?? { items: [] as PackagePolicy[] };
const supportedPackagePolicyIds = filter(packagePolicies, (packagePolicy) =>
satisfies(packagePolicy.package?.version ?? '', '>=0.6.0')
);
const agentPolicyIds = uniq(flatMap(supportedPackagePolicyIds, 'policy_ids'));
const agentPolicies = await agentPolicyService?.getByIds(
internalSavedObjectsClient,
agentPolicyIds
);
const agentPolicies = await agentPolicyService?.getByIds(spaceScopedClient, agentPolicyIds);
if (agentPolicies?.length) {
await pMap(
agentPolicies,
(agentPolicy: GetAgentPoliciesResponseItem) =>
agentService?.asInternalUser
agentService
?.asInternalScopedUser(space?.id ?? DEFAULT_SPACE_ID)
.getAgentStatusForAgentPolicy(agentPolicy.id)
.then(({ active: agentTotal }) => (agentPolicy.agents = agentTotal)),
{ concurrency: 10 }

View file

@ -7,10 +7,10 @@
import type { IRouter } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { API_VERSIONS } from '../../../common/constants';
import { PLUGIN_ID } from '../../../common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { getInternalSavedObjectsClient } from '../utils';
import { GetAgentPolicyRequestParams } from '../../../common/api';
export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
@ -34,12 +34,13 @@ export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppC
},
},
async (context, request, response) => {
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const packageInfo = await osqueryContext.service
.getAgentPolicyService()
?.get(internalSavedObjectsClient, request.params.id);
?.get(spaceScopedClient, request.params.id);
return response.ok({ body: { item: packageInfo } });
}

View file

@ -7,6 +7,7 @@
import type { GetAgentStatusResponse } from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import type {
GetAgentStatusForAgentPolicyRequestParamsSchema,
GetAgentStatusForAgentPolicyRequestQuerySchema,
@ -51,9 +52,10 @@ export const getAgentStatusForAgentPolicyRoute = (
},
},
async (context, request, response) => {
const space = await osqueryContext.service.getActiveSpace(request);
const results = await osqueryContext.service
.getAgentService()
?.asScoped(request)
?.asInternalScopedUser(space?.id ?? DEFAULT_SPACE_ID)
.getAgentStatusForAgentPolicy(request.query.policyId, request.query.kuery);
if (!results) {

View file

@ -12,8 +12,9 @@ import { filter, flatMap, mapKeys, uniq } from 'lodash';
import type { PackagePolicy } from '@kbn/fleet-plugin/server/types';
import { satisfies } from 'semver';
import type { SortResults } from '@elastic/elasticsearch/lib/api/types';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { processAggregations } from '../../../common/utils/aggregations';
import { getInternalSavedObjectsClient } from '../utils';
import { getAgentsRequestQuerySchema } from '../../../common/api';
import type { GetAgentsRequestQuerySchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
@ -54,29 +55,26 @@ export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContex
getStatusSummary?: boolean;
};
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const space = await osqueryContext.service.getActiveSpace(request);
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
const { items: packagePolicies } = (await packagePolicyService?.list(
internalSavedObjectsClient,
{
const { items: packagePolicies } = (await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 1000,
page: 1,
}
)) ?? { items: [] as PackagePolicy[] };
})) ?? { items: [] as PackagePolicy[] };
const supportedPackagePolicyIds = filter(packagePolicies, (packagePolicy) =>
satisfies(packagePolicy.package?.version ?? '', '>=0.6.0')
);
const agentPolicyIds = uniq(flatMap(supportedPackagePolicyIds, 'policy_ids'));
const agentPolicies = await agentPolicyService?.getByIds(
internalSavedObjectsClient,
agentPolicyIds
);
const agentPolicies = await agentPolicyService?.getByIds(spaceScopedClient, agentPolicyIds);
// FIND agents by policy_name
const policyNamePattern = /policy_name:([^ ]+)/;
@ -101,7 +99,10 @@ export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContex
const agentPolicyById = mapKeys(agentPolicies, 'id');
try {
esAgents = await osqueryContext.service.getAgentService()?.asInternalUser.listAgents({
esAgents = await osqueryContext.service
.getAgentService()
?.asInternalScopedUser(space?.id ?? DEFAULT_SPACE_ID)
.listAgents({
page: query.page,
perPage: query.perPage,
sortField: query.sortField,

View file

@ -7,10 +7,10 @@
import type { IRouter } from '@kbn/core/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { API_VERSIONS } from '../../../common/constants';
import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { getInternalSavedObjectsClient } from '../utils';
export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
@ -29,12 +29,14 @@ export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: Osquery
validate: {},
},
async (context, request, response) => {
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`;
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const policies = await packagePolicyService?.list(internalSavedObjectsClient, {
const policies = await packagePolicyService?.list(spaceScopedClient, {
kuery,
perPage: 1000,
});

View file

@ -50,7 +50,6 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
async (context, request, response) => {
const [coreStartServices] = await osqueryContext.getStartServices();
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
const {
osquery: { writeLiveQueries, runSavedQueries },
@ -115,13 +114,14 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
try {
const currentUser = coreContext.security.authc.getCurrentUser()?.username;
const space = await osqueryContext.service.getActiveSpace(request);
const { response: osqueryAction, fleetActionsCount } = await createActionHandler(
osqueryContext,
request.body,
{
soClient,
metadata: { currentUser },
alertData,
space,
}
);
if (!fleetActionsCount) {

View file

@ -10,6 +10,8 @@ import { omit } from 'lodash';
import type { Observable } from 'rxjs';
import { lastValueFrom } from 'rxjs';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import type { FindLiveQueryRequestQuerySchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
@ -24,7 +26,10 @@ import { OsqueryQueries } from '../../../common/search_strategy';
import { findLiveQueryRequestQuerySchema } from '../../../common/api';
import { generateTablePaginationOptions } from '../../../common/utils/build_query';
export const findLiveQueryRoute = (router: IRouter<DataRequestHandlerContext>) => {
export const findLiveQueryRoute = (
router: IRouter<DataRequestHandlerContext>,
osqueryContext: OsqueryAppContext
) => {
router.versioned
.get({
access: 'public',
@ -52,6 +57,10 @@ export const findLiveQueryRoute = (router: IRouter<DataRequestHandlerContext>) =
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
try {
const spaceId = osqueryContext?.service?.getActiveSpace
? (await osqueryContext.service.getActiveSpace(request))?.id || DEFAULT_SPACE_ID
: DEFAULT_SPACE_ID;
const search = await context.search;
const res = await lastValueFrom(
search.search<ActionsRequestOptions, ActionsStrategyResponse>(
@ -66,6 +75,7 @@ export const findLiveQueryRoute = (router: IRouter<DataRequestHandlerContext>) =
direction: (request.query.sortOrder ?? 'desc') as Direction,
field: request.query.sort ?? 'created_at',
},
spaceId,
},
{ abortSignal, strategy: 'osquerySearchStrategy' }
)

View file

@ -10,6 +10,7 @@ import { every, map, mapKeys, pick, reduce } from 'lodash';
import type { Observable } from 'rxjs';
import { lastValueFrom, zip } from 'rxjs';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import type {
GetLiveQueryDetailsRequestParamsSchema,
GetLiveQueryDetailsRequestQuerySchema,
@ -28,8 +29,12 @@ import {
getLiveQueryDetailsRequestParamsSchema,
getLiveQueryDetailsRequestQuerySchema,
} from '../../../common/api';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getLiveQueryDetailsRoute = (router: IRouter<DataRequestHandlerContext>) => {
export const getLiveQueryDetailsRoute = (
router: IRouter<DataRequestHandlerContext>,
osqueryContext: OsqueryAppContext
) => {
router.versioned
.get({
access: 'public',
@ -60,12 +65,17 @@ export const getLiveQueryDetailsRoute = (router: IRouter<DataRequestHandlerConte
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
try {
const spaceId = osqueryContext?.service?.getActiveSpace
? (await osqueryContext.service.getActiveSpace(request))?.id || DEFAULT_SPACE_ID
: DEFAULT_SPACE_ID;
const search = await context.search;
const { actionDetails } = await lastValueFrom(
search.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(
{
actionId: request.params.id,
factoryQueryType: OsqueryQueries.actionDetails,
spaceId,
},
{ abortSignal, strategy: 'osquerySearchStrategy' }
)

View file

@ -10,6 +10,7 @@ import { map } from 'lodash';
import type { Observable } from 'rxjs';
import { lastValueFrom, zip } from 'rxjs';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import type {
GetLiveQueryResultsRequestQuerySchema,
GetLiveQueryResultsRequestParamsSchema,
@ -30,8 +31,12 @@ import {
getLiveQueryResultsRequestParamsSchema,
getLiveQueryResultsRequestQuerySchema,
} from '../../../common/api';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const getLiveQueryResultsRoute = (router: IRouter<DataRequestHandlerContext>) => {
export const getLiveQueryResultsRoute = (
router: IRouter<DataRequestHandlerContext>,
osqueryContext: OsqueryAppContext
) => {
router.versioned
.get({
access: 'public',
@ -62,6 +67,10 @@ export const getLiveQueryResultsRoute = (router: IRouter<DataRequestHandlerConte
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
try {
const spaceId = osqueryContext?.service?.getActiveSpace
? (await osqueryContext.service.getActiveSpace(request))?.id || DEFAULT_SPACE_ID
: DEFAULT_SPACE_ID;
const search = await context.search;
const { actionDetails } = await lastValueFrom(
search.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(
@ -69,11 +78,16 @@ export const getLiveQueryResultsRoute = (router: IRouter<DataRequestHandlerConte
actionId: request.params.id,
kuery: request.query.kuery,
factoryQueryType: OsqueryQueries.actionDetails,
spaceId,
},
{ abortSignal, strategy: 'osquerySearchStrategy' }
)
);
if (!actionDetails) {
return response.notFound({ body: { message: 'Action not found' } });
}
const queries = actionDetails?._source?.queries;
await lastValueFrom(

View file

@ -17,8 +17,8 @@ export const initLiveQueryRoutes = (
router: IRouter<DataRequestHandlerContext>,
context: OsqueryAppContext
) => {
findLiveQueryRoute(router);
findLiveQueryRoute(router, context);
createLiveQueryRoute(router, context);
getLiveQueryDetailsRoute(router);
getLiveQueryResultsRoute(router);
getLiveQueryDetailsRoute(router, context);
getLiveQueryResultsRoute(router, context);
};

View file

@ -15,6 +15,8 @@ import {
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
} from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import type { CreatePackRequestBodySchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
@ -28,7 +30,7 @@ import {
findMatchingShards,
getInitialPolicies,
} from './utils';
import { convertShardsToArray, getInternalSavedObjectsClient } from '../utils';
import { convertShardsToArray } from '../utils';
import type { PackSavedObject } from '../../common/types';
import type { PackResponseData } from './types';
import { createPackRequestBodySchema } from '../../../common/api';
@ -61,10 +63,12 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const savedObjectsClient = coreContext.savedObjects.client;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
@ -72,7 +76,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
// eslint-disable-next-line @typescript-eslint/naming-convention
const { name, description, queries, enabled, policy_ids, shards = {} } = request.body;
const conflictingEntries = await savedObjectsClient.find({
const conflictingEntries = await spaceScopedClient.find({
type: packSavedObjectType,
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
});
@ -84,14 +88,11 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
return response.conflict({ body: `Pack with name "${name}" already exists.` });
}
const { items: packagePolicies } = (await packagePolicyService?.list(
internalSavedObjectsClient,
{
const { items: packagePolicies } = (await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 1000,
page: 1,
}
)) ?? { items: [] };
})) ?? { items: [] };
const { policiesList, invalidPolicies } = getInitialPolicies(
packagePolicies,
@ -104,10 +105,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
});
}
const agentPolicies = await agentPolicyService?.getByIds(
internalSavedObjectsClient,
policiesList
);
const agentPolicies = await agentPolicyService?.getByIds(spaceScopedClient, policiesList);
const policyShards = findMatchingShards(agentPolicies, shards);
@ -119,7 +117,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE,
}));
const packSO = await savedObjectsClient.create<PackSavedObjectLimited>(
const packSO = await spaceScopedClient.create<PackSavedObjectLimited>(
packSavedObjectType,
{
name,
@ -146,7 +144,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
);
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {

View file

@ -9,7 +9,7 @@ import { has, filter, unset } from 'lodash';
import { produce } from 'immer';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { getInternalSavedObjectsClient } from '../utils';
import type { DeletePacksRequestParamsSchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
@ -19,6 +19,7 @@ import { PLUGIN_ID } from '../../../common';
import { packSavedObjectType } from '../../../common/types';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { deletePacksRequestParamsSchema } from '../../../common/api';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
export const deletePackRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
@ -46,22 +47,23 @@ export const deletePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const savedObjectsClient = coreContext.savedObjects.client;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const currentPackSO = await savedObjectsClient.get<{ name: string }>(
const currentPackSO = await spaceScopedClient.get<{ name: string }>(
packSavedObjectType,
request.params.id
);
await savedObjectsClient.delete(packSavedObjectType, request.params.id, {
await spaceScopedClient.delete(packSavedObjectType, request.params.id, {
refresh: 'wait_for',
});
const { items: packagePolicies } = (await packagePolicyService?.list(savedObjectsClient, {
const { items: packagePolicies } = (await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 1000,
page: 1,
@ -76,7 +78,7 @@ export const deletePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
await Promise.all(
currentPackagePolicies.map((packagePolicy) =>
packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce(packagePolicy, (draft) => {

View file

@ -9,6 +9,8 @@ import { filter, map, omit } from 'lodash';
import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import type { FindPacksRequestQuerySchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
@ -17,8 +19,9 @@ import { PLUGIN_ID } from '../../../common';
import type { PackSavedObject } from '../../common/types';
import type { PackResponseData } from './types';
import { findPacksRequestQuerySchema } from '../../../common/api';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const findPackRoute = (router: IRouter) => {
export const findPackRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
.get({
access: 'public',
@ -42,10 +45,12 @@ export const findPackRoute = (router: IRouter) => {
},
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const soClientResponse = await savedObjectsClient.find<PackSavedObject>({
const soClientResponse = await spaceScopedClient.find<PackSavedObject>({
type: packSavedObjectType,
page: request.query.page ?? 1,
perPage: request.query.pageSize ?? 20,

View file

@ -17,7 +17,7 @@ import { updatePackRoute } from './update_pack_route';
export const initPackRoutes = (router: IRouter, context: OsqueryAppContext) => {
createPackRoute(router, context);
deletePackRoute(router, context);
findPackRoute(router);
readPackRoute(router);
findPackRoute(router, context);
readPackRoute(router, context);
updatePackRoute(router, context);
};

View file

@ -8,6 +8,8 @@
import { filter, map } from 'lodash';
import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import type { ReadPacksRequestParamsSchema } from '../../../common/api';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
@ -19,8 +21,9 @@ import { convertSOQueriesToPack } from './utils';
import { convertShardsToObject } from '../utils';
import type { ReadPackResponseData } from './types';
import { readPacksRequestParamsSchema } from '../../../common/api';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
export const readPackRoute = (router: IRouter) => {
export const readPackRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
.get({
access: 'public',
@ -44,11 +47,13 @@ export const readPackRoute = (router: IRouter) => {
},
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const { attributes, references, id, ...rest } =
await savedObjectsClient.get<PackSavedObject>(packSavedObjectType, request.params.id);
await spaceScopedClient.get<PackSavedObject>(packSavedObjectType, request.params.id);
const policyIds = map(
filter(references, ['type', LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE]),

View file

@ -16,6 +16,7 @@ import {
} from '@kbn/fleet-plugin/common';
import type { IRouter } from '@kbn/core/server';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import type {
UpdatePacksRequestParamsSchema,
UpdatePacksRequestBodySchema,
@ -34,7 +35,7 @@ import {
findMatchingShards,
} from './utils';
import { convertShardsToArray, getInternalSavedObjectsClient } from '../utils';
import { convertShardsToArray } from '../utils';
import type { PackSavedObject } from '../../common/types';
import type { PackResponseData } from './types';
import { updatePacksRequestBodySchema, updatePacksRequestParamsSchema } from '../../../common/api';
@ -69,10 +70,12 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const savedObjectsClient = coreContext.savedObjects.client;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const currentUser = coreContext.security.authc.getCurrentUser()?.username;
@ -80,13 +83,13 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
// eslint-disable-next-line @typescript-eslint/naming-convention
const { name, description, queries, enabled, policy_ids, shards = {} } = request.body;
const currentPackSO = await savedObjectsClient.get<{ name: string; enabled: boolean }>(
const currentPackSO = await spaceScopedClient.get<{ name: string; enabled: boolean }>(
packSavedObjectType,
request.params.id
);
if (name) {
const conflictingEntries = await savedObjectsClient.find<PackSavedObject>({
const conflictingEntries = await spaceScopedClient.find<PackSavedObject>({
type: packSavedObjectType,
filter: `${packSavedObjectType}.attributes.name: "${name}"`,
});
@ -101,14 +104,11 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
}
}
const { items: packagePolicies } = (await packagePolicyService?.list(
internalSavedObjectsClient,
{
const { items: packagePolicies } = (await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 1000,
page: 1,
}
)) ?? { items: [] };
})) ?? { items: [] };
const currentPackagePolicies = filter(packagePolicies, (packagePolicy) =>
has(
packagePolicy,
@ -128,10 +128,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
});
}
const agentPolicies = await agentPolicyService?.getByIds(
internalSavedObjectsClient,
policiesList
);
const agentPolicies = await agentPolicyService?.getByIds(spaceScopedClient, policiesList);
const policyShards = findMatchingShards(agentPolicies, shards);
@ -158,7 +155,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
const references = getUpdatedReferences();
await savedObjectsClient.update<PackSavedObject>(
await spaceScopedClient.update<PackSavedObject>(
packSavedObjectType,
request.params.id,
{
@ -180,7 +177,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
filter(currentPackSO.references, ['type', LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE]),
'id'
);
const updatedPackSO = await savedObjectsClient.get<PackSavedObject>(
const updatedPackSO = await spaceScopedClient.get<PackSavedObject>(
packSavedObjectType,
request.params.id
);
@ -203,7 +200,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
);
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {
@ -235,7 +232,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
if (!packagePolicy) return;
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {
@ -266,7 +263,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
);
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {
@ -290,7 +287,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
);
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {
@ -326,7 +323,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce<PackagePolicy>(packagePolicy, (draft) => {

View file

@ -7,6 +7,7 @@
import { isEmpty, pickBy, some, isBoolean, isNumber } from 'lodash';
import type { IRouter } from '@kbn/core/server';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import type { CreateSavedQueryRequestSchemaDecoded } from '../../../common/api';
import { API_VERSIONS } from '../../../common/constants';
import type { SavedQueryResponse } from './types';
@ -43,7 +44,10 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const {
id,
@ -61,7 +65,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const currentUser = coreContext.security.authc.getCurrentUser()?.username;
const conflictingEntries = await savedObjectsClient.find<SavedQuerySavedObject>({
const conflictingEntries = await spaceScopedClient.find<SavedQuerySavedObject>({
type: savedQuerySavedObjectType,
filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`,
});
@ -73,7 +77,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
return response.conflict({ body: `Saved query with id "${id}" already exists.` });
}
const savedQuerySO = await savedObjectsClient.create(
const savedQuerySO = await spaceScopedClient.create(
savedQuerySavedObjectType,
pickBy(
{

View file

@ -6,6 +6,8 @@
*/
import type { IRouter } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
import { PLUGIN_ID } from '../../../common';
@ -39,18 +41,25 @@ export const deleteSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
},
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const space = await osqueryContext.service.getActiveSpace(request);
const spaceId = space?.id ?? DEFAULT_SPACE_ID;
const isPrebuilt = await isSavedQueryPrebuilt(
osqueryContext.service.getPackageService()?.asInternalUser,
request.params.id
request.params.id,
spaceScopedClient,
spaceId
);
if (isPrebuilt) {
return response.conflict({ body: `Elastic prebuilt Saved query cannot be deleted.` });
}
await savedObjectsClient.delete(savedQuerySavedObjectType, request.params.id, {
await spaceScopedClient.delete(savedQuerySavedObjectType, request.params.id, {
refresh: 'wait_for',
});

View file

@ -8,6 +8,8 @@
import type { IRouter } from '@kbn/core/server';
import { omit } from 'lodash';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
import type { SavedQueryResponse } from './types';
@ -44,11 +46,16 @@ export const findSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
},
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const space = await osqueryContext.service.getActiveSpace(request);
const spaceId = space?.id ?? DEFAULT_SPACE_ID;
try {
const savedQueries = await savedObjectsClient.find<SavedQuerySavedObject>({
const savedQueries = await spaceScopedClient.find<SavedQuerySavedObject>({
type: savedQuerySavedObjectType,
page: request.query.page || 1,
perPage: request.query.pageSize,
@ -57,14 +64,22 @@ export const findSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
});
const prebuiltSavedQueriesMap = await getInstalledSavedQueriesMap(
osqueryContext.service.getPackageService()?.asInternalUser
osqueryContext.service.getPackageService()?.asInternalUser,
spaceScopedClient,
spaceId
);
const savedObjects: SavedQueryResponse[] = savedQueries.saved_objects.map(
(savedObject) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const ecs_mapping = savedObject.attributes.ecs_mapping;
savedObject.attributes.prebuilt = !!prebuiltSavedQueriesMap[savedObject.id];
const prebuiltById = savedObject.id && prebuiltSavedQueriesMap[savedObject.id];
const prebuiltByOriginId =
!prebuiltById && savedObject.originId
? prebuiltSavedQueriesMap[savedObject.originId]
: false;
savedObject.attributes.prebuilt = !!(prebuiltById || prebuiltByOriginId);
if (ecs_mapping) {
// @ts-expect-error update types

View file

@ -6,6 +6,8 @@
*/
import type { IRouter } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
import type { SavedQueryResponse } from './types';
@ -42,10 +44,15 @@ export const readSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
},
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const savedQuery = await savedObjectsClient.get<SavedQuerySavedObject>(
const space = await osqueryContext.service.getActiveSpace(request);
const spaceId = space?.id ?? DEFAULT_SPACE_ID;
const savedQuery = await spaceScopedClient.get<SavedQuerySavedObject>(
savedQuerySavedObjectType,
request.params.id
);
@ -57,11 +64,25 @@ export const readSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAppC
);
}
savedQuery.attributes.prebuilt = await isSavedQueryPrebuilt(
const prebuiltById = await isSavedQueryPrebuilt(
osqueryContext.service.getPackageService()?.asInternalUser,
savedQuery.id
savedQuery.id,
spaceScopedClient,
spaceId
);
const prebuiltByOriginId =
!prebuiltById && savedQuery.originId
? await isSavedQueryPrebuilt(
osqueryContext.service.getPackageService()?.asInternalUser,
savedQuery.originId,
spaceScopedClient,
spaceId
)
: false;
savedQuery.attributes.prebuilt = prebuiltById || prebuiltByOriginId;
const {
created_at: createdAt,
created_by: createdBy,

View file

@ -8,6 +8,8 @@
import { filter, some } from 'lodash';
import type { IRouter } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
import { API_VERSIONS } from '../../../common/constants';
import { isSavedQueryPrebuilt } from './utils';
@ -54,7 +56,14 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
},
async (context, request, response) => {
const coreContext = await context.core;
const savedObjectsClient = coreContext.savedObjects.client;
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const space = await osqueryContext.service.getActiveSpace(request);
const spaceId = space?.id ?? DEFAULT_SPACE_ID;
const currentUser = coreContext.security.authc.getCurrentUser()?.username;
const {
@ -73,14 +82,16 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
const isPrebuilt = await isSavedQueryPrebuilt(
osqueryContext.service.getPackageService()?.asInternalUser,
request.params.id
request.params.id,
spaceScopedClient,
spaceId
);
if (isPrebuilt) {
return response.conflict({ body: `Elastic prebuilt Saved query cannot be updated.` });
}
const conflictingEntries = await savedObjectsClient.find<{ id: string }>({
const conflictingEntries = await spaceScopedClient.find<{ id: string }>({
type: savedQuerySavedObjectType,
filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`,
});
@ -97,7 +108,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp
return response.conflict({ body: `Saved query with id "${id}" already exists.` });
}
const updatedSavedQuerySO = await savedObjectsClient.update(
const updatedSavedQuerySO = await spaceScopedClient.update(
savedQuerySavedObjectType,
request.params.id,
{

View file

@ -0,0 +1,374 @@
/*
* 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 type { SavedObjectsClientContract } from '@kbn/core/server';
import type { PackageClient } from '@kbn/fleet-plugin/server';
import type { KibanaAssetReference, KibanaSavedObjectType } from '@kbn/fleet-plugin/common';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import {
getInstalledSavedQueriesMap,
getPrebuiltSavedQueryIds,
isSavedQueryPrebuilt,
} from './utils';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { savedQuerySavedObjectType } from '../../../common/types';
describe('saved query utils', () => {
const mockPackageService = {
getInstallation: jest.fn(),
} as unknown as PackageClient;
const mockSavedObjectsClient = {} as SavedObjectsClientContract;
const mockSavedQueryAsset: KibanaAssetReference = {
id: 'saved-query-1',
type: savedQuerySavedObjectType as KibanaSavedObjectType,
};
const mockOtherAsset: KibanaAssetReference = {
id: 'other-asset-1',
type: 'other-type' as KibanaSavedObjectType,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('getInstalledSavedQueriesMap', () => {
it('should return empty object when no installation found', async () => {
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(null);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
'default'
);
expect(result).toEqual({});
expect(mockPackageService.getInstallation).toHaveBeenCalledWith(
OSQUERY_INTEGRATION_NAME,
mockSavedObjectsClient
);
});
it('should return empty object when packageService is undefined', async () => {
const result = await getInstalledSavedQueriesMap(
undefined,
mockSavedObjectsClient,
'default'
);
expect(result).toEqual({});
});
it('should return saved queries map for default space', async () => {
const mockInstallation = {
installed_kibana_space_id: DEFAULT_SPACE_ID,
installed_kibana: [mockSavedQueryAsset, mockOtherAsset],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toEqual({
'saved-query-1': mockSavedQueryAsset,
});
});
it('should return saved queries map for default space when installed_kibana_space_id is undefined', async () => {
const mockInstallation = {
installed_kibana: [mockSavedQueryAsset, mockOtherAsset],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toEqual({
'saved-query-1': mockSavedQueryAsset,
});
});
it('should return saved queries map for additional space', async () => {
const customSpaceId = 'custom-space';
const mockInstallation = {
installed_kibana_space_id: 'other-space',
installed_kibana: [mockOtherAsset],
additional_spaces_installed_kibana: {
[customSpaceId]: [mockSavedQueryAsset],
},
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
customSpaceId
);
expect(result).toEqual({
'saved-query-1': mockSavedQueryAsset,
});
});
it('should return empty object when space is not found in additional spaces', async () => {
const customSpaceId = 'custom-space';
const mockInstallation = {
installed_kibana_space_id: 'yet-another-space',
installed_kibana: [mockSavedQueryAsset],
additional_spaces_installed_kibana: {
'different-space': [mockSavedQueryAsset],
},
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
customSpaceId
);
expect(result).toEqual({});
});
it('should filter out non-saved-query assets', async () => {
const mockInstallation = {
installed_kibana_space_id: DEFAULT_SPACE_ID,
installed_kibana: [
mockSavedQueryAsset,
mockOtherAsset,
{ id: 'saved-query-2', type: savedQuerySavedObjectType },
],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getInstalledSavedQueriesMap(
mockPackageService,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toEqual({
'saved-query-1': mockSavedQueryAsset,
'saved-query-2': { id: 'saved-query-2', type: savedQuerySavedObjectType },
});
});
});
describe('getPrebuiltSavedQueryIds', () => {
it('should return empty array when no installation found', async () => {
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(null);
const result = await getPrebuiltSavedQueryIds(mockPackageService);
expect(result).toEqual([]);
expect(mockPackageService.getInstallation).toHaveBeenCalledWith(OSQUERY_INTEGRATION_NAME);
});
it('should return empty array when packageService is undefined', async () => {
const result = await getPrebuiltSavedQueryIds(undefined);
expect(result).toEqual([]);
});
it('should return saved query IDs from installation', async () => {
const mockInstallation = {
installed_kibana: [
mockSavedQueryAsset,
mockOtherAsset,
{ id: 'saved-query-2', type: savedQuerySavedObjectType },
{ id: 'saved-query-3', type: savedQuerySavedObjectType },
],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getPrebuiltSavedQueryIds(mockPackageService);
expect(result).toEqual(['saved-query-1', 'saved-query-2', 'saved-query-3']);
});
it('should filter out non-saved-query assets', async () => {
const mockInstallation = {
installed_kibana: [
mockOtherAsset,
{ id: 'another-asset', type: 'another-type' },
mockSavedQueryAsset,
],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await getPrebuiltSavedQueryIds(mockPackageService);
expect(result).toEqual(['saved-query-1']);
});
});
describe('isSavedQueryPrebuilt', () => {
const savedQueryId = 'test-saved-query';
it('should return false when no installation found', async () => {
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(null);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
'default'
);
expect(result).toBe(false);
expect(mockPackageService.getInstallation).toHaveBeenCalledWith(
OSQUERY_INTEGRATION_NAME,
mockSavedObjectsClient
);
});
it('should return false when packageService is undefined', async () => {
const result = await isSavedQueryPrebuilt(
undefined,
savedQueryId,
mockSavedObjectsClient,
'default'
);
expect(result).toBe(false);
});
it('should return true when saved query is found in default space', async () => {
const mockInstallation = {
installed_kibana_space_id: DEFAULT_SPACE_ID,
installed_kibana: [{ id: savedQueryId, type: savedQuerySavedObjectType }, mockOtherAsset],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toBe(true);
});
it('should return true when saved query is found in default space and installed_kibana_space_id is undefined', async () => {
const mockInstallation = {
installed_kibana: [{ id: savedQueryId, type: savedQuerySavedObjectType }, mockOtherAsset],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toBe(true);
});
it('should return true when saved query is found in additional space', async () => {
const customSpaceId = 'custom-space';
const mockInstallation = {
installed_kibana_space_id: 'other-space',
installed_kibana: [mockOtherAsset],
additional_spaces_installed_kibana: {
[customSpaceId]: [{ id: savedQueryId, type: savedQuerySavedObjectType }],
},
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
customSpaceId
);
expect(result).toBe(true);
});
it('should return false when saved query is not found in space', async () => {
const customSpaceId = 'custom-space';
const mockInstallation = {
installed_kibana_space_id: 'yet-another-space',
installed_kibana: [{ id: 'different-query', type: savedQuerySavedObjectType }],
additional_spaces_installed_kibana: {
'different-space': [{ id: 'another-query', type: savedQuerySavedObjectType }],
},
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
customSpaceId
);
expect(result).toBe(false);
});
it('should return false when saved query has wrong type', async () => {
const mockInstallation = {
installed_kibana_space_id: DEFAULT_SPACE_ID,
installed_kibana: [{ id: savedQueryId, type: 'wrong-type' }, mockOtherAsset],
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
DEFAULT_SPACE_ID
);
expect(result).toBe(false);
});
it('should return false when space is not found in additional spaces', async () => {
const customSpaceId = 'custom-space';
const mockInstallation = {
installed_kibana_space_id: 'other-space',
installed_kibana: [mockOtherAsset],
additional_spaces_installed_kibana: {
'different-space': [{ id: savedQueryId, type: savedQuerySavedObjectType }],
},
};
(mockPackageService.getInstallation as jest.Mock).mockResolvedValue(mockInstallation);
const result = await isSavedQueryPrebuilt(
mockPackageService,
savedQueryId,
mockSavedObjectsClient,
customSpaceId
);
expect(result).toBe(false);
});
});
});

View file

@ -9,15 +9,32 @@ import { find, filter, map, reduce } from 'lodash';
import type { KibanaAssetReference } from '@kbn/fleet-plugin/common';
import type { PackageClient } from '@kbn/fleet-plugin/server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { savedQuerySavedObjectType } from '../../../common/types';
export const getInstalledSavedQueriesMap = async (packageService: PackageClient | undefined) => {
const installation = await packageService?.getInstallation(OSQUERY_INTEGRATION_NAME);
export const getInstalledSavedQueriesMap = async (
packageService: PackageClient | undefined,
savedObjectsClient: SavedObjectsClientContract,
spaceId: string
) => {
const installation = await packageService?.getInstallation(
OSQUERY_INTEGRATION_NAME,
savedObjectsClient
);
if (installation) {
let installedSavedQueries: KibanaAssetReference[] = [];
if ((installation.installed_kibana_space_id ?? DEFAULT_SPACE_ID) === spaceId) {
installedSavedQueries = installation.installed_kibana;
} else if (installation.additional_spaces_installed_kibana?.[spaceId]) {
installedSavedQueries = installation.additional_spaces_installed_kibana[spaceId];
}
return reduce<KibanaAssetReference, Record<string, KibanaAssetReference>>(
installation.installed_kibana,
installedSavedQueries,
(acc, item) => {
if (item.type === savedQuerySavedObjectType) {
return { ...acc, [item.id]: item };
@ -49,13 +66,26 @@ export const getPrebuiltSavedQueryIds = async (packageService: PackageClient | u
export const isSavedQueryPrebuilt = async (
packageService: PackageClient | undefined,
savedQueryId: string
savedQueryId: string,
savedObjectsClient: SavedObjectsClientContract,
spaceId: string
) => {
const installation = await packageService?.getInstallation(OSQUERY_INTEGRATION_NAME);
const installation = await packageService?.getInstallation(
OSQUERY_INTEGRATION_NAME,
savedObjectsClient
);
if (installation) {
let installedSavedQueries: KibanaAssetReference[] = [];
if ((installation.installed_kibana_space_id ?? DEFAULT_SPACE_ID) === spaceId) {
installedSavedQueries = installation.installed_kibana;
} else if (installation.additional_spaces_installed_kibana?.[spaceId]) {
installedSavedQueries = installation.additional_spaces_installed_kibana[spaceId];
}
const installationSavedQueries = find(
installation.installed_kibana,
installedSavedQueries,
(item) => item.type === savedQuerySavedObjectType && item.id === savedQueryId
);

View file

@ -20,7 +20,8 @@ import { packSavedObjectType } from '../../../common/types';
import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common';
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
import { convertPackQueriesToSO } from '../pack/utils';
import { getInternalSavedObjectsClient } from '../utils';
import { createInternalSavedObjectsClientForSpaceId } from '../../utils/get_internal_saved_object_client';
import { fetchOsqueryPackagePolicyIds } from '../utils';
export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
router.versioned
@ -41,18 +42,34 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
const spaceScopedClient = await createInternalSavedObjectsClientForSpaceId(
osqueryContext,
request
);
const packageService = osqueryContext.service.getPackageService()?.asInternalUser;
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
const packageInfo = await packageService?.getInstallation(OSQUERY_INTEGRATION_NAME);
const packageInfo = await packageService?.getInstallation(
OSQUERY_INTEGRATION_NAME,
spaceScopedClient
);
const osqueryPackagePolicyIdsWithinCurrentSpace = await fetchOsqueryPackagePolicyIds(
spaceScopedClient,
osqueryContext
);
if (!osqueryPackagePolicyIdsWithinCurrentSpace.length) {
return response.ok({
body: undefined,
});
}
if (packageInfo?.install_version && satisfies(packageInfo?.install_version, '<0.6.0')) {
try {
const policyPackages = await packagePolicyService?.list(internalSavedObjectsClient, {
const policyPackages = await packagePolicyService?.list(spaceScopedClient, {
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`,
perPage: 10000,
page: 1,
@ -128,13 +145,13 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
const agentPolicyIds = uniq(flatMap(policyPackages?.items, 'policy_ids'));
const agentPolicies = mapKeys(
await agentPolicyService?.getByIds(internalSavedObjectsClient, agentPolicyIds),
await agentPolicyService?.getByIds(spaceScopedClient, agentPolicyIds),
'id'
);
await Promise.all(
map(migrationObject.packs, async (packObject) => {
await internalSavedObjectsClient.create(
await spaceScopedClient.create(
packSavedObjectType,
{
name: packObject.name,
@ -160,7 +177,7 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
// delete unnecessary package policies
await packagePolicyService?.delete(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
migrationObject.packagePoliciesToDelete
);
@ -171,15 +188,12 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
const agentPacks = filter(migrationObject.packs, (pack) =>
pack.policy_ids.includes(key)
);
await packagePolicyService?.upgrade(internalSavedObjectsClient, esClient, value);
const packagePolicy = await packagePolicyService?.get(
internalSavedObjectsClient,
value
);
await packagePolicyService?.upgrade(spaceScopedClient, esClient, value);
const packagePolicy = await packagePolicyService?.get(spaceScopedClient, value);
if (packagePolicy) {
return packagePolicyService?.update(
internalSavedObjectsClient,
spaceScopedClient,
esClient,
packagePolicy.id,
produce(packagePolicy, (draft) => {

View file

@ -8,8 +8,12 @@
import type { CoreSetup } from '@kbn/core/server';
import { SavedObjectsClient } from '@kbn/core/server';
import { reduce } from 'lodash';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { Shard } from '../../common/utils/converters';
import type { SOShard } from '../common/types';
import { OSQUERY_INTEGRATION_NAME } from '../../common';
import type { OsqueryAppContext } from '../lib/osquery_app_context_services';
export const convertECSMappingToArray = (ecsMapping: Record<string, object> | undefined) =>
ecsMapping
@ -56,3 +60,34 @@ export const getInternalSavedObjectsClient = async (
return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
};
/**
* Fetches all package policy IDs for osquery_manager integration in the current space.
* @param packagePolicyService - Fleet's package policy service
* @param soClient - Saved objects client
* @returns Array of package policy IDs
*/
export const fetchOsqueryPackagePolicyIds = async (
soClient: SavedObjectsClientContract,
osqueryContext: OsqueryAppContext
): Promise<string[]> => {
const logger = osqueryContext.logFactory.get('fetchOsqueryPackagePolicyIds');
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
if (!packagePolicyService) {
throw new Error('Package policy service is not available');
}
logger.debug('Fetching osquery package policy IDs');
const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`;
const idIterable = await packagePolicyService.fetchAllItemIds(soClient, { kuery });
const ids: string[] = [];
for await (const batch of idIterable) {
ids.push(...batch);
}
logger.debug(`Fetched ${ids.length} osquery package policy IDs`);
return ids;
};

View file

@ -9,27 +9,50 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ISearchRequestParams } from '@kbn/search-types';
import { AGENT_ACTIONS_INDEX } from '@kbn/fleet-plugin/common';
import type { AgentsRequestOptions } from '../../../../../../common/search_strategy/osquery/agents';
import { getQueryFilter } from '../../../../../utils/build_query';
import { ACTIONS_INDEX } from '../../../../../../common/constants';
import type { ActionsRequestOptions } from '../../../../../../common/search_strategy/osquery/actions';
export const buildActionsQuery = ({
kuery = '',
sort,
pagination: { cursorStart, querySize },
componentTemplateExists,
}: AgentsRequestOptions): ISearchRequestParams => {
spaceId,
}: ActionsRequestOptions): ISearchRequestParams => {
const {
bool: { filter },
} = getQueryFilter({ filter: kuery });
let extendedFilter = filter;
if (spaceId === 'default') {
// For default space, include docs where space_id matches 'default' OR where space_id field does not exist
extendedFilter = [
{
bool: {
should: [
{ term: { space_id: 'default' } },
{ bool: { must_not: { exists: { field: 'space_id' } } } },
],
},
},
...filter,
];
} else {
// For other spaces, only include docs where space_id matches the current spaceId
extendedFilter = [...filter, { term: { space_id: spaceId } }];
}
return {
allow_no_indices: true,
index: componentTemplateExists ? `${ACTIONS_INDEX}*` : AGENT_ACTIONS_INDEX,
ignore_unavailable: true,
query: {
bool: {
filter,
filter: extendedFilter,
must: [
{
term: {

View file

@ -16,6 +16,7 @@ export const buildActionDetailsQuery = ({
actionId,
kuery,
componentTemplateExists,
spaceId,
}: ActionDetailsRequestOptions): ISearchRequestParams => {
const actionIdQuery = `action_id: ${actionId}`;
let filter = actionIdQuery;
@ -23,13 +24,35 @@ export const buildActionDetailsQuery = ({
filter = filter + ` AND ${kuery}`;
}
const filterQuery = getQueryFilter({ filter });
const {
bool: { filter: baseFilter },
} = getQueryFilter({ filter });
let extendedFilter = baseFilter;
if (spaceId === 'default') {
// For default space, include docs where space_id matches 'default' OR where space_id field does not exist
extendedFilter = [
{
bool: {
should: [
{ term: { space_id: 'default' } },
{ bool: { must_not: { exists: { field: 'space_id' } } } },
],
},
},
...baseFilter,
];
} else {
// For other spaces, only include docs where space_id matches the current spaceId
extendedFilter = [...baseFilter, { term: { space_id: spaceId } }];
}
const dslQuery = {
allow_no_indices: true,
index: componentTemplateExists ? `${ACTIONS_INDEX}*` : AGENT_ACTIONS_INDEX,
ignore_unavailable: true,
query: { bool: { filter: filterQuery } },
query: { bool: { filter: extendedFilter } },
size: 1,
fields: ['*'],
};

View file

@ -53,12 +53,15 @@ export const osquerySearchStrategyProvider = <T extends FactoryQueryTypes>(
...('actionId' in request ? { actionId: request.actionId } : {}),
...('startDate' in request ? { startDate: request.startDate } : {}),
...('agentId' in request ? { agentId: request.agentId } : {}),
...('policyIds' in request ? { policyIds: request.policyIds } : {}),
...('spaceId' in request ? { spaceId: request.spaceId } : {}),
} as StrategyRequestType<T>;
const dsl = queryFactory.buildDsl({
...strictRequest,
componentTemplateExists: actionsIndexExists,
} as StrategyRequestType<T>);
// use internal user for searching .fleet* indices
es =
dsl.index?.includes('fleet') || dsl.index?.includes('logs-osquery_manager.action')

View file

@ -23,6 +23,7 @@ import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
import type { CasesServerSetup } from '@kbn/cases-plugin/server';
import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
import type { createActionService } from './handlers/action/create_action_service';
export interface OsqueryPluginSetup {
@ -41,6 +42,7 @@ export interface SetupPlugins {
taskManager?: TaskManagerPluginSetup;
telemetry?: TelemetryPluginSetup;
licensing: LicensingPluginSetup;
spaces?: SpacesPluginSetup;
}
export interface StartPlugins {
@ -51,4 +53,5 @@ export interface StartPlugins {
taskManager?: TaskManagerPluginStart;
telemetry?: TelemetryPluginStart;
ruleRegistry?: RuleRegistryPluginStartContract;
spaces?: SpacesPluginStart;
}

View file

@ -10,6 +10,7 @@ import { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal'
import { kibanaRequestFactory } from '@kbn/core-http-server-utils';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type { KibanaRequest } from '@kbn/core-http-server';
export async function getInternalSavedObjectsClient(coreStart: CoreStart) {
return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
@ -36,3 +37,13 @@ export function getInternalSavedObjectsClientForSpaceId(
excludedExtensions: [SECURITY_EXTENSION_ID],
}) as SavedObjectsClient;
}
export async function createInternalSavedObjectsClientForSpaceId(
osqueryContext: { service: { getActiveSpace: Function }; getStartServices: Function },
request: KibanaRequest
): Promise<SavedObjectsClient> {
const space = await osqueryContext.service.getActiveSpace(request);
const [core] = await osqueryContext.getStartServices();
return getInternalSavedObjectsClientForSpaceId(core, space?.id ?? DEFAULT_SPACE_ID);
}

View file

@ -80,6 +80,8 @@
"@kbn/zod",
"@kbn/core-lifecycle-server",
"@kbn/core-saved-objects-api-server-internal",
"@kbn/core-http-server-utils"
"@kbn/core-http-server-utils",
"@kbn/spaces-utils",
"@kbn/core-http-server"
]
}

View file

@ -8,15 +8,21 @@
import { each, map, some, uniq } from 'lodash';
import { containsDynamicQuery } from '@kbn/osquery-plugin/common/utils/replace_params_query';
import { requiredOptional } from '@kbn/zod-helpers';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { ResponseActionAlerts } from './types';
import type { SetupPlugins } from '../../../plugin_contract';
import type { RuleResponseOsqueryAction } from '../../../../common/api/detection_engine/model/rule_response_actions';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
export const osqueryResponseAction = (
responseAction: RuleResponseOsqueryAction,
osqueryCreateActionService: SetupPlugins['osquery']['createActionService'],
endpointAppContextService: EndpointAppContextService,
{ alerts }: ResponseActionAlerts
) => {
const logger = osqueryCreateActionService.logger;
const temporaryQueries = responseAction.params.queries?.length
? responseAction.params.queries
: [{ query: responseAction.params.query }];
@ -26,31 +32,75 @@ export const osqueryResponseAction = (
);
const { savedQueryId, packId, queries, ecsMapping, ...rest } = responseAction.params;
// Extract space information from the first alert (all alerts should be from the same space)
let spaceId = alerts[0]?.kibana?.space_ids?.[0];
const processResponseActionClientError = (err: Error, endpointIds: string[]): Promise<void> => {
logger.error(
`attempt to run osquery queries on host IDs [${endpointIds.join(', ')}] returned error: ${
err.message
}`
);
return Promise.resolve();
};
if (endpointAppContextService.experimentalFeatures.endpointManagementSpaceAwarenessEnabled) {
if (!spaceId) {
const ruleId = alerts[0].kibana.alert?.rule.uuid;
const ruleName = alerts[0].kibana.alert?.rule.name;
logger.error(
`Unable to identify the space ID from alert data ('kibana.space_ids') for rule [${ruleName}][${ruleId}]`
);
return;
}
} else {
// force the space to `default` when space awareness is not enabled
spaceId = DEFAULT_SPACE_ID;
}
if (!containsDynamicQueries) {
const agentIds = uniq(map(alerts, 'agent.id'));
const alertIds = map(alerts, '_id');
return osqueryCreateActionService.create({
return osqueryCreateActionService
.create(
{
...rest,
queries: requiredOptional(queries),
ecs_mapping: ecsMapping,
saved_query_id: savedQueryId,
agent_ids: agentIds,
alert_ids: alertIds,
},
{
space: { id: spaceId },
}
)
.catch((err) => {
return processResponseActionClientError(err, agentIds);
});
}
each(alerts, (alert) => {
return osqueryCreateActionService.create(
const agentIds = alert.agent?.id ? [alert.agent.id] : [];
return osqueryCreateActionService
.create(
{
...rest,
queries: requiredOptional(queries),
ecs_mapping: ecsMapping,
saved_query_id: savedQueryId,
agent_ids: alert.agent?.id ? [alert.agent.id] : [],
agent_ids: agentIds,
alert_ids: [(alert as unknown as { _id: string })._id],
},
alert
);
{
alertData: alert as ParsedTechnicalFields & { _index: string },
space: { id: spaceId as string },
}
)
.catch((err) => {
return processResponseActionClientError(err, agentIds);
});
});
};

View file

@ -12,7 +12,7 @@ import { ALERT_RULE_NAME, ALERT_RULE_UUID, SPACE_IDS } from '@kbn/rule-data-util
import { createMockEndpointAppContextService } from '../../../endpoint/mocks';
import { responseActionsClientMock } from '../../../endpoint/services/actions/clients/mocks';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import type { Logger } from '@kbn/logging';
describe('ScheduleNotificationResponseActions', () => {
const getSignals = () => [
{
@ -30,9 +30,13 @@ describe('ScheduleNotificationResponseActions', () => {
];
const osqueryActionMock = {
create: jest.fn(),
create: jest.fn().mockResolvedValue({}),
stop: jest.fn(),
logger: {
error: jest.fn(),
} as unknown as Logger,
};
let mockedResponseActionsClient = responseActionsClientMock.create();
const endpointServiceMock = createMockEndpointAppContextService();
(endpointServiceMock.getInternalResponseActionsClient as jest.Mock).mockImplementation(() => {
@ -47,6 +51,11 @@ describe('ScheduleNotificationResponseActions', () => {
});
describe('Osquery', () => {
beforeEach(() => {
jest.clearAllMocks();
// @ts-expect-error assignment to readonly property
endpointServiceMock.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = true;
});
const simpleQuery = 'select * from uptime';
const defaultQueryParams = {
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
@ -73,15 +82,115 @@ describe('ScheduleNotificationResponseActions', () => {
const defaultResultParams = {
agent_ids: ['agent-id-1', 'agent-id-2'],
alert_ids: ['alert-id-1', 'alert-id-2'],
};
const defaultQueryResultParams = {
...defaultResultParams,
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
ecsMapping: undefined,
saved_query_id: 'testSavedQueryId',
savedQueryId: undefined,
queries: [],
};
it('should pass correct space id from alert.kibana.space_ids[0] when space awareness is enabled', () => {
const signals = getSignals();
scheduleNotificationResponseActions({
signals,
signalsCount: signals.length,
responseActions: [
{
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: { ...defaultQueryParams, queries: [{ id: 'query-1', query: simpleQuery }] },
} as RuleResponseAction,
],
});
expect(osqueryActionMock.create).toHaveBeenCalledWith(
expect.objectContaining({
queries: expect.any(Array),
}),
expect.objectContaining({
space: { id: DEFAULT_SPACE_ID },
})
);
});
it('should use default space id when space awareness is disabled', () => {
// @ts-expect-error assignment to readonly property
endpointServiceMock.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = false;
const signals = getSignals();
scheduleNotificationResponseActions({
signals,
signalsCount: signals.length,
responseActions: [
{
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: { ...defaultQueryParams, queries: [{ id: 'query-1', query: simpleQuery }] },
} as RuleResponseAction,
],
});
expect(osqueryActionMock.create).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
space: { id: DEFAULT_SPACE_ID },
})
);
});
it('should log error if space awareness is enabled and space id is missing', () => {
const signals = [{ ...getSignals()[0], [SPACE_IDS]: undefined }];
scheduleNotificationResponseActions({
signals,
signalsCount: signals.length,
responseActions: [
{
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: { ...defaultQueryParams, queries: [{ id: 'query-1', query: simpleQuery }] },
} as RuleResponseAction,
],
});
expect(osqueryActionMock.logger.error).toHaveBeenCalledWith(
expect.stringContaining('Unable to identify the space ID')
);
expect(osqueryActionMock.create).not.toHaveBeenCalled();
});
it('should handle errors from osqueryActionMock.create and log them', async () => {
const signals = getSignals();
const testError = new Error('Simulated create failure');
osqueryActionMock.create.mockRejectedValueOnce(testError);
await scheduleNotificationResponseActions({
signals,
signalsCount: signals.length,
responseActions: [
{
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: { ...defaultQueryParams, queries: [{ id: 'query-1', query: simpleQuery }] },
} as RuleResponseAction,
],
});
expect(osqueryActionMock.logger.error).toHaveBeenCalledWith(
expect.stringContaining('Simulated create failure')
);
});
it('should pass alertData when dynamic queries are present', () => {
const dynamicQuery = 'select * from uptime where id = {{host.id}}';
const signals = getSignals();
scheduleNotificationResponseActions({
signals,
signalsCount: signals.length,
responseActions: [
{
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: { ...defaultQueryParams, queries: [{ id: 'query-1', query: dynamicQuery }] },
} as RuleResponseAction,
],
});
expect(osqueryActionMock.create).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
alertData: expect.objectContaining({ _id: signals[0]._id }),
})
);
});
const defaultPackResultParams = {
...defaultResultParams,
query: undefined,
@ -106,10 +215,19 @@ describe('ScheduleNotificationResponseActions', () => {
});
expect(response).not.toBeUndefined();
expect(osqueryActionMock.create).toHaveBeenCalledWith({
...defaultQueryResultParams,
expect(osqueryActionMock.create).toHaveBeenCalledWith(
expect.objectContaining({
agent_ids: ['agent-id-1', 'agent-id-2'],
alert_ids: ['alert-id-1', 'alert-id-2'],
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
queries: [],
query: simpleQuery,
});
saved_query_id: 'testSavedQueryId',
}),
expect.objectContaining({
space: { id: DEFAULT_SPACE_ID },
})
);
});
it('should handle osquery response actions with packs', async () => {
@ -138,10 +256,15 @@ describe('ScheduleNotificationResponseActions', () => {
});
expect(response).not.toBeUndefined();
expect(osqueryActionMock.create).toHaveBeenCalledWith({
expect(osqueryActionMock.create).toHaveBeenCalledWith(
expect.objectContaining({
...defaultPackResultParams,
queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }],
});
}),
expect.objectContaining({
space: { id: DEFAULT_SPACE_ID },
})
);
});
});
describe('Endpoint', () => {

View file

@ -44,9 +44,14 @@ export const getScheduleNotificationResponseActionsService =
responseAction.actionTypeId === ResponseActionTypesEnum['.osquery'] &&
osqueryCreateActionService
) {
await osqueryResponseAction(responseAction, osqueryCreateActionService, {
await osqueryResponseAction(
responseAction,
osqueryCreateActionService,
endpointAppContextService,
{
alerts,
});
}
);
}
if (responseAction.actionTypeId === ResponseActionTypesEnum['.endpoint']) {
// We currently support only automated response actions for Elastic Defend. This will

View file

@ -254,5 +254,6 @@
"@kbn/triggers-actions-ui-types",
"@kbn/unified-histogram",
"@kbn/react-kibana-context-theme",
"@kbn/spaces-utils",
]
}