Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
01523c0
[Fleet] Validate SSL certificate paths for whitespace
juliaElastic Apr 29, 2026
a5c085a
[Fleet] Fix SSL path validation UX: real-time feedback and save butto…
juliaElastic Apr 29, 2026
32b0264
Merge branch 'main' into fix-ssl-path-spaces-validation
juliaElastic Apr 29, 2026
1ad66a9
Gate SSL isInvalid checks in output form isDisabled by output type
juliaElastic Apr 29, 2026
7870c48
[Fleet] Fix 500 error when saving Kafka output with compression disabled
juliaElastic Apr 29, 2026
83e8ac3
Changes from make api-docs
kibanamachine Apr 29, 2026
702da8a
Update x-pack/platform/plugins/shared/fleet/public/applications/fleet…
juliaElastic Apr 30, 2026
107e4bb
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 30, 2026
487a4b3
[Fleet] Address review comments on SSL path validation PR
juliaElastic Apr 30, 2026
1cf1a8c
[Fleet] Fix Cypress test: Logstash SSL certificate/key fields are PEM…
juliaElastic Apr 30, 2026
8310876
[Fleet] Fix API integration test: use paths without spaces in ssl con…
juliaElastic Apr 30, 2026
fa78d08
[Fleet] Restore path validation for Logstash SSL cert/key; fix Cypres…
juliaElastic Apr 30, 2026
4d5b193
fix review comments
juliaElastic Apr 30, 2026
f3741b7
fix checks
juliaElastic Apr 30, 2026
43cd5e1
validate secret key fields
juliaElastic Apr 30, 2026
95c8d4d
[Fleet] Clean up SSL validation: trim comments, fix stale-error re-re…
juliaElastic Apr 30, 2026
d440ce0
[Fleet] Compare full error array with join() to skip unnecessary re-r…
juliaElastic Apr 30, 2026
ede4730
[Fleet] Fix SSL key path validation for secret inputs and download so…
juliaElastic Apr 30, 2026
57676fc
[Fleet] Fix integration tests: replace spaced SSL cert values with pa…
juliaElastic Apr 30, 2026
c2221be
[Fleet] Disable save button when required fields are empty
juliaElastic May 1, 2026
fa333bf
[Fleet] Fix Cypress tests for outputs error messages after save butto…
juliaElastic May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94275,6 +94275,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down Expand Up @@ -95477,6 +95478,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down Expand Up @@ -97738,6 +97740,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down
3 changes: 3 additions & 0 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106685,6 +106685,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down Expand Up @@ -107887,6 +107888,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down Expand Up @@ -110148,6 +110150,7 @@ components:
- none
type: string
compression_level:
nullable: true
type: number
config_yaml:
nullable: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export {
// Cloud Connector accessor module
export * from './cloud_connectors';

export { validateSslCertPath } from './ssl_validators';

export type { YamlModule } from './yaml_utils';
export { createYamlKeysSorter, toYaml } from './yaml_utils';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { validateSslCertPath } from './ssl_validators';

describe('validateSslCertPath', () => {
describe('valid inputs (returns undefined)', () => {
it('empty string', () => {
expect(validateSslCertPath('')).toBeUndefined();
});

it('Linux path without spaces', () => {
expect(validateSslCertPath('/etc/certs/ca.pem')).toBeUndefined();
});

it('relative path without spaces', () => {
expect(validateSslCertPath('./certs/ca.pem')).toBeUndefined();
});

it('Windows absolute path without spaces', () => {
expect(validateSslCertPath('C:\\certs\\server.pem')).toBeUndefined();
});

it('Windows forward-slash path without spaces', () => {
expect(validateSslCertPath('C:/certs/server.pem')).toBeUndefined();
});

it('UNC path without spaces', () => {
expect(validateSslCertPath('\\\\server\\share\\cert.pem')).toBeUndefined();
});

it('PEM certificate content', () => {
expect(
validateSslCertPath(
'-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAJC1\n-----END CERTIFICATE-----'
)
).toBeUndefined();
});

it('PEM RSA private key', () => {
expect(
validateSslCertPath(
'-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA\n-----END RSA PRIVATE KEY-----'
)
).toBeUndefined();
});

it('PEM EC private key', () => {
expect(
validateSslCertPath(
'-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIO\n-----END EC PRIVATE KEY-----'
)
).toBeUndefined();
});

it('PEM content with leading whitespace', () => {
expect(
validateSslCertPath(' -----BEGIN CERTIFICATE-----\nMIID\n-----END CERTIFICATE-----')
).toBeUndefined();
});
});

describe('invalid inputs (returns error string)', () => {
it('Linux path with spaces', () => {
expect(validateSslCertPath('/path/to my cert.pem')).toBeDefined();
});

it('relative path with spaces', () => {
expect(validateSslCertPath('./my certs/ca.pem')).toBeDefined();
});

it('Windows path with spaces', () => {
expect(validateSslCertPath('C:\\Program Files\\certs\\server.pem')).toBeDefined();
});

it('Windows forward-slash path with spaces', () => {
expect(validateSslCertPath('C:/Program Files/certs/server.pem')).toBeDefined();
});

it('UNC path with spaces in share name', () => {
expect(validateSslCertPath('\\\\server\\my share\\cert.pem')).toBeDefined();
});

it('path with tab character', () => {
expect(validateSslCertPath('/path/to\tcert.pem')).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

// PEM content (-----BEGIN ...) is exempt — it naturally contains whitespace
export function validateSslCertPath(value: string): string | undefined {
if (!value || value.trimStart().startsWith('-----BEGIN')) return undefined;
if (/\s/.test(value)) {
return i18n.translate('xpack.fleet.sslValidation.pathSpacesError', {
defaultMessage: 'SSL certificate path cannot contain whitespace',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export interface KafkaOutput extends NewBaseOutput {
version?: string;
key?: string;
compression?: ValueOf<KafkaCompressionType>;
compression_level?: number;
compression_level?: number | null;
auth_type?: ValueOf<KafkaAuthType>;
connection_type?: ValueOf<KafkaConnectionTypeType>;
username?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,12 @@ describe('Edit settings', () => {
cy.getBySel(SETTINGS_OUTPUTS.TYPE_INPUT).select('logstash');
cy.get('[placeholder="Specify host"').clear().type('logstash:5044');
cy.getBySel(SETTINGS_OUTPUTS.SSL_BUTTON).click();
cy.get('[placeholder="Specify SSL certificate"]').clear().type('SSL CERTIFICATE');
cy.get('[placeholder="Specify certificate key"]').clear().type('SSL KEY');
cy.get('[placeholder="Specify SSL certificate"]')
.clear()
.type('-----BEGIN CERTIFICATE-----', { parseSpecialCharSequences: false });
cy.get('[placeholder="Specify certificate key"]')
.clear()
.type('-----BEGIN PRIVATE KEY-----', { parseSpecialCharSequences: false });

cy.intercept('/api/fleet/outputs', {
items: [
Expand All @@ -157,8 +161,8 @@ describe('Edit settings', () => {
is_default: false,
is_default_monitoring: false,
ssl: {
certificate: "SSL CERTIFICATE');",
key: 'SSL KEY',
certificate: '-----BEGIN CERTIFICATE-----',
key: '-----BEGIN PRIVATE KEY-----',
},
}).as('postLogstashOutput');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,11 @@ queue:
describe('Remote ES', () => {
it('displays proper error messages', () => {
selectRemoteESOutput();
cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT).type('name');
cy.get('[placeholder="Specify host URL"').clear().type('https://localhost:5000');
cy.getBySel(SETTINGS_SAVE_BTN).click();

cy.contains('Name is required');
cy.contains('URL is required');
cy.contains('Service token is required');
shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT);
shouldDisplayError('serviceTokenSecretInput');
});

Expand Down Expand Up @@ -392,18 +391,17 @@ queue:

it('displays proper error messages', () => {
selectKafkaOutput();
cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT).type('name');
cy.get('[placeholder="Specify host"').type('localhost:5000');
cy.getBySel(SETTINGS_OUTPUTS_KAFKA.HEADERS_CLIENT_ID_INPUT).clear();
cy.getBySel(SETTINGS_SAVE_BTN).click();

cy.contains('Name is required');
cy.contains('Host is required');
cy.contains('Username is required');
cy.contains('Password is required');
cy.contains('Default topic is required');
cy.contains(
'Client ID is invalid. Only letters, numbers, dots, underscores, and dashes are allowed.'
);
shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT);
Expand Down Expand Up @@ -564,8 +562,12 @@ queue:
cy.get('[placeholder="Specify host"').clear().type('localhost:5000');

cy.getBySel(SETTINGS_OUTPUTS.SSL_BUTTON).click();
cy.get('[placeholder="Specify SSL certificate"]').clear().type('SSL CERTIFICATE');
cy.get('[placeholder="Specify certificate key"]').clear().type('SSL KEY');
cy.get('[placeholder="Specify SSL certificate"]')
.clear()
.type('-----BEGIN CERTIFICATE-----', { parseSpecialCharSequences: false });
cy.get('[placeholder="Specify certificate key"]')
.clear()
.type('-----BEGIN PRIVATE KEY-----', { parseSpecialCharSequences: false });

cy.intercept('PUT', '**/api/fleet/outputs/**').as('saveOutput');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,116 @@
* 2.0.
*/

import { act } from '@testing-library/react';

import { createFleetTestRendererMock } from '../../../../../../mock';

import {
validateHost,
validateDownloadSourceHeaders,
useDowloadSourceFlyoutForm,
type AuthType,
} from './use_download_source_flyout_form';

jest.mock('../../../../../../hooks/use_authz', () => ({
useAuthz: () => ({
fleet: {
allSettings: true,
},
}),
}));

describe('useDowloadSourceFlyoutForm SSL certificate path validation', () => {
it('should block submission when certificate path contains spaces', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
const { result } = testRenderer.renderHook(() =>
useDowloadSourceFlyoutForm(onSuccess, undefined)
);

act(() => {
result.current.inputs.nameInput.setValue('My Source');
result.current.inputs.hostInput.setValue('https://artifacts.example.com');
result.current.inputs.sslCertificateInput.setValue('/path with spaces/cert.pem');
});

await act(() => result.current.submit());

await testRenderer.waitFor(() => {
expect(result.current.inputs.sslCertificateInput.errors).toBeDefined();
expect(onSuccess).not.toBeCalled();
expect(result.current.isDisabled).toBeTruthy();
});
});

it('should block submission when certificate key path contains spaces', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
const { result } = testRenderer.renderHook(() =>
useDowloadSourceFlyoutForm(onSuccess, undefined)
);

act(() => {
result.current.inputs.nameInput.setValue('My Source');
result.current.inputs.hostInput.setValue('https://artifacts.example.com');
result.current.inputs.sslKeyInput.setValue('/path with spaces/key.pem');
});

await act(() => result.current.submit());

await testRenderer.waitFor(() => {
expect(result.current.inputs.sslKeyInput.errors).toBeDefined();
expect(onSuccess).not.toBeCalled();
expect(result.current.isDisabled).toBeTruthy();
});
});

it('should block submission when certificate authorities path contains spaces', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
const { result } = testRenderer.renderHook(() =>
useDowloadSourceFlyoutForm(onSuccess, undefined)
);

act(() => {
result.current.inputs.nameInput.setValue('My Source');
result.current.inputs.hostInput.setValue('https://artifacts.example.com');
result.current.inputs.sslCertificateAuthoritiesInput.props.onChange([
'/path with spaces/ca.pem',
]);
});

await act(() => result.current.submit());

await testRenderer.waitFor(() => {
expect(result.current.inputs.sslCertificateAuthoritiesInput.props.errors).toBeDefined();
expect(onSuccess).not.toBeCalled();
expect(result.current.isDisabled).toBeTruthy();
});
});

it('should allow submission when all SSL paths are valid', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
testRenderer.startServices.http.post.mockResolvedValue({ item: {} });
const { result } = testRenderer.renderHook(() =>
useDowloadSourceFlyoutForm(onSuccess, undefined)
);

act(() => {
result.current.inputs.nameInput.setValue('My Source');
result.current.inputs.hostInput.setValue('https://artifacts.example.com');
result.current.inputs.sslCertificateInput.setValue('/valid/path/cert.pem');
result.current.inputs.sslKeyInput.setValue('/valid/path/key.pem');
result.current.inputs.sslCertificateAuthoritiesInput.props.onChange(['/valid/ca.pem']);
});

await act(() => result.current.submit());

await testRenderer.waitFor(() => expect(onSuccess).toBeCalled());
});
});

describe('Download source form validation', () => {
describe('validateHost', () => {
it('should not work without any urls', () => {
Expand Down
Loading
Loading