Skip to content

Commit

Permalink
[Cloud Security] [Agentless] Improving error log metadata and sending…
Browse files Browse the repository at this point in the history
… APM trace id (elastic#192235)

## Summary

This PR includes a few improvements in the communication between Kibana
and the Agentless API.

- Adding a `X-Request-ID` Header on all HTTP calls from the Kibana
server to the Agentless API. X-Request-ID is the
[currentTraceparent](https://www.elastic.co/guide/en/apm/agent/nodejs/current/agent-api.html#apm-current-traceparent)
string captured from Apm Service and is unique per request.
- Also, this PR enhances some error logs metadata with the relevant
fields, and all documents logged to ES also includes the
[trace.id](https://www.elastic.co/guide/en/ecs/8.11/ecs-tracing.html)
field
- Also this PR redacts the fleet token sent to the debug logs to prevent
credential leaking
  • Loading branch information
opauloh authored Sep 24, 2024
1 parent bcd2cc1 commit 10bcc62
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 36 deletions.
12 changes: 6 additions & 6 deletions x-pack/plugins/fleet/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export interface FleetConfigType {
};
agentless?: {
enabled: boolean;
api: {
url: string;
tls: {
certificate: string;
key: string;
ca: string;
api?: {
url?: string;
tls?: {
certificate?: string;
key?: string;
ca?: string;
};
};
};
Expand Down
197 changes: 196 additions & 1 deletion x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { securityMock } from '@kbn/security-plugin/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import type { Logger } from '@kbn/core/server';

import type { AxiosError } from 'axios';
import axios from 'axios';

import { AgentlessAgentCreateError } from '../../errors';
Expand All @@ -20,7 +21,14 @@ import { listFleetServerHosts } from '../fleet_server_host';

import { agentlessAgentService } from './agentless_agent';

jest.mock('axios', () => jest.fn());
jest.mock('axios');
// Add a mock implementation for `isAxiosError` to simulate that the error is an Axios error
(axios.isAxiosError as unknown as jest.Mock).mockImplementation(
(error: any): error is AxiosError => {
return error.isAxiosError === true; // Simulate that the error is an Axios error if it has `isAxiosError` property
}
);

jest.mock('../fleet_server_host');
jest.mock('../api_keys');
jest.mock('../output');
Expand Down Expand Up @@ -361,4 +369,191 @@ describe('Agentless Agent service', () => {
})
);
});

it('should redact sensitive information from debug logs', async () => {
const returnValue = {
id: 'mocked',
regional_id: 'mocked',
};

(axios as jest.MockedFunction<typeof axios>).mockResolvedValueOnce(returnValue);
const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

jest.spyOn(appContextService, 'getConfig').mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'http://api.agentless.com',
tls: {
certificate: '/path/to/cert',
key: '/path/to/key',
ca: '/path/to/ca',
},
},
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');

mockedListFleetServerHosts.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
host: 'http://fleetserver:8220',
active: true,
is_default: true,
host_urls: ['http://fleetserver:8220'],
},
],
} as any);

mockedListEnrollmentApiKeys.mockResolvedValue({
items: [
{
id: 'mocked-fleet-enrollment-token-id',
policy_id: 'mocked-fleet-enrollment-policy-id',
api_key: 'mocked-fleet-enrollment-api-key',
},
],
} as any);

await agentlessAgentService.createAgentlessAgent(esClient, soClient, {
id: 'mocked-agentless-agent-policy-id',
name: 'agentless agent policy',
namespace: 'default',
supports_agentless: true,
} as AgentPolicy);

// Assert that sensitive information is redacted
expect(mockedLogger.debug).toHaveBeenCalledWith(
expect.stringContaining('fleet_token: [REDACTED]')
);
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: [REDACTED]'));
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: [REDACTED]'));
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: [REDACTED]'));
});

it('should log "undefined" on debug logs when tls configuration is missing', async () => {
const returnValue = {
id: 'mocked',
regional_id: 'mocked',
};

(axios as jest.MockedFunction<typeof axios>).mockResolvedValueOnce(returnValue);
const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

jest.spyOn(appContextService, 'getConfig').mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'http://api.agentless.com',
},
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');

mockedListFleetServerHosts.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
host: 'http://fleetserver:8220',
active: true,
is_default: true,
host_urls: ['http://fleetserver:8220'],
},
],
} as any);

mockedListEnrollmentApiKeys.mockResolvedValue({
items: [
{
id: 'mocked-fleet-enrollment-token-id',
policy_id: 'mocked-fleet-enrollment-policy-id',
api_key: 'mocked-fleet-enrollment-api-key',
},
],
} as any);

await expect(
agentlessAgentService.createAgentlessAgent(esClient, soClient, {
id: 'mocked-agentless-agent-policy-id',
name: 'agentless agent policy',
namespace: 'default',
supports_agentless: true,
} as AgentPolicy)
).rejects.toThrowError();

// Assert that tls configuration is missing
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: undefined'));
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: undefined'));
expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: undefined'));
});

it('should redact sensitive information from error logs', async () => {
const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

jest.spyOn(appContextService, 'getConfig').mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'http://api.agentless.com',
tls: {
certificate: '/path/to/cert',
key: '/path/to/key',
ca: '/path/to/ca',
},
},
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);

mockedListFleetServerHosts.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
host: 'http://fleetserver:8220',
active: true,
is_default: true,
host_urls: ['http://fleetserver:8220'],
},
],
} as any);

mockedListEnrollmentApiKeys.mockResolvedValue({
items: [
{
id: 'mocked-fleet-enrollment-token-id',
policy_id: 'mocked-fleet-enrollment-policy-id',
api_key: 'mocked-fleet-enrollment-api-key',
},
],
} as any);
// Force axios to throw an AxiosError to simulate an error response
const axiosError = new Error('Test Error') as AxiosError;
axiosError.isAxiosError = true; // Mark it as an AxiosError
(axios as jest.MockedFunction<typeof axios>).mockRejectedValueOnce(axiosError);

await expect(
agentlessAgentService.createAgentlessAgent(esClient, soClient, {
id: 'mocked-agentless-agent-policy-id',
name: 'agentless agent policy',
namespace: 'default',
supports_agentless: true,
} as AgentPolicy)
).rejects.toThrowError();

// Assert that sensitive information is redacted
expect(mockedLogger.error).toHaveBeenCalledWith(
expect.stringContaining(`\"fleet_token\":\"[REDACTED]\"`),
expect.any(Object)
);
});
});
Loading

0 comments on commit 10bcc62

Please sign in to comment.