Skip to content

Commit

Permalink
[Serverless][Security Solution][Endpoint] Gate endpoint exceptions on…
Browse files Browse the repository at this point in the history
… rule details and API changes (#165613)

## What this PR changes

Follow up of /pull/164107/

For serverless ES/Kibana, it gates exception list API for endpoint
exceptions and restricts endpoint exceptions tab on Endpoint Security
rule details based on project PLIs. If no endpoint PLIs, endpoint
exceptions should not be accessible.

- [x] Add upselling to `app/security/exceptions/details/endpoint_list`
page
- [ ] Tests (WIP) - in a follow up PR

### How to review

Best to follow along commits for a code review. Below are details to
manually test the changes.

- Setup for _Servlerless_
- Run `yarn es serverless --kill --clean --license trial -E
xpack.security.authc.api_key.enabled=true` on a terminal window to start
ES.
- Copy `config/serverless.security.yml` to
`config/serverless.security.dev.yml`
- Run `yarn serverless-security --no-base-path` on another terminal
window to start kibana in serverless mode
  - Log in using `serverless_security` user.

### Tests (Serverless)
This needs to be tested with a custom user/role and not
`elastic_serverless` which has `superuser` role.

1. ### PLI configs
`{ product_line: 'security', product_tier: 'essentials' }` or `{
product_line: 'security', product_tier: 'complete' }`
and
`{ product_line: 'endpoint', product_tier: 'essentials' }` or `{
product_line: 'endpoint', product_tier: 'complete' }`

- #### UX
1. Navigate to Rules via `http://localhost:5601/app/security/rules/`.
Click on `Add Elastic rules`.
  2. Select and add `Endpoint Security` rule.
3. Click `Endpoint Security` and navigate to the rules details page, and
you should see `Endpoint exceptions` tab. The tabs visible are `Alerts`,
`Endpoint exceptions`, `Rule exceptions`, `Execution results`.
4. Navigate to Rules>Shared Exception Lists > Endpoint Security
Exception List via `app/security/exceptions/details/endpoint_list` and
you should be able to see the page with any added endpoint exceptions.

- #### API requests (with user `serverless_security`)
  1. should get a status `200` on`POST api/exception_lists/items`
2. should get a status `200` on `POST
api/exception_lists/_export?id=endpoint_list&list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true`
  3. should get a status `200` on `PUT api/exception_lists/items`
  5. should get a status `200` on `DELETE api/exception_lists/items`
6. should get a status `200` on `GET
api/exception_lists/items/_find?list_id=endpoint_list&namespace_type=agnostic`

2. ### PLI configs
`{ product_line: 'security', product_tier: 'essentials' }` or `{
product_line: 'security', product_tier: 'complete' }`

- #### UX
1. Navigate to Rules via `http://localhost:5601/app/security/rules/`.
Click on `Add Elastic rules`.
  2. Select and add `Endpoint Security` rule. 
3. Click `Endpoint Security` and navigate to the rules details page, and
you should not see `Endpoint exceptions` tab. The only tabs visible are
`Alerts`, `Rule exceptions`, `Execution results`.
![Screenshot 2023-09-14 at 3 33 24
PM](https://github.com/elastic/kibana/assets/1849116/185ea210-c457-4469-a824-cdcaa2893cb6)
4. Navigate to Rules>Shared Exception Lists > Endpoint Security
Exception List via `app/security/exceptions/details/endpoint_list` and
you should see an upsell message.
![Screenshot 2023-09-14 at 3 29 14
PM](https://github.com/elastic/kibana/assets/1849116/6700fc2d-9a9d-4a57-ad7f-5505e02cec71)


- #### API requests
  1. should get a status `403` on`POST api/exception_lists/items`
2. should get a status `403` on `POST
api/exception_lists/_export?id=endpoint_list&list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true`
  3. should get a status `403` on `PUT api/exception_lists/items`
  6. should get a status `403` on `DELETE api/exception_lists/items`
7. should get a status `403` on `GET
api/exception_lists/items/_find?list_id=endpoint_list&namespace_type=agnostic`
---


**Flaky FTRs**
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3248
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3255


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
ashokaditya and kibanamachine authored Oct 2, 2023
1 parent db6fa73 commit a8de031
Show file tree
Hide file tree
Showing 46 changed files with 916 additions and 189 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/common/components/popover_items @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions @elastic/security-defend-workflows
/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/rule_management @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management
Expand Down Expand Up @@ -1336,6 +1337,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows
/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows

## Security Solution sub teams - security-telemetry (Data Engineering)
Expand Down
3 changes: 2 additions & 1 deletion x-pack/packages/security-solution/upselling/service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type SectionUpsellings = Partial<Record<UpsellingSectionId, React.Compone
export type UpsellingSectionId =
| 'entity_analytics_panel'
| 'endpointPolicyProtections'
| 'osquery_automated_response_actions';
| 'osquery_automated_response_actions'
| 'ruleDetailsEndpointExceptions';

export type UpsellingMessageId = 'investigation_guide';
135 changes: 135 additions & 0 deletions x-pack/plugins/fleet/common/authz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import { TRANSFORM_PLUGIN_ID } from './constants/plugin';

import {
calculateEndpointExceptionsPrivilegesFromCapabilities,
calculateEndpointExceptionsPrivilegesFromKibanaPrivileges,
calculatePackagePrivilegesFromCapabilities,
calculatePackagePrivilegesFromKibanaPrivileges,
getAuthorizationFromPrivileges,
} from './authz';
import { ENDPOINT_PRIVILEGES } from './constants';

Expand Down Expand Up @@ -74,6 +77,56 @@ describe('fleet authz', () => {
});
});

describe('#calculateEndpointExceptionsPrivilegesFromCapabilities', () => {
it('calculates endpoint exceptions privileges correctly', () => {
const endpointExceptionsCapabilities = {
showEndpointExceptions: false,
crudEndpointExceptions: true,
};

const expected = {
actions: {
showEndpointExceptions: false,
crudEndpointExceptions: true,
},
};

const actual = calculateEndpointExceptionsPrivilegesFromCapabilities({
navLinks: {},
management: {},
catalogue: {},
siem: endpointExceptionsCapabilities,
});

expect(actual).toEqual(expected);
});

it('calculates endpoint exceptions privileges correctly when no matching capabilities', () => {
const endpointCapabilities = {
writeEndpointList: true,
writeTrustedApplications: true,
writePolicyManagement: false,
readPolicyManagement: true,
writeHostIsolationExceptions: true,
writeHostIsolation: false,
};
const expected = {
actions: {
showEndpointExceptions: false,
crudEndpointExceptions: false,
},
};
const actual = calculateEndpointExceptionsPrivilegesFromCapabilities({
navLinks: {},
management: {},
catalogue: {},
siem: endpointCapabilities,
});

expect(actual).toEqual(expected);
});
});

describe('calculatePackagePrivilegesFromKibanaPrivileges', () => {
it('calculates privileges correctly', () => {
const endpointPrivileges = [
Expand Down Expand Up @@ -111,4 +164,86 @@ describe('fleet authz', () => {
expect(actual).toEqual(expected);
});
});

describe('#calculateEndpointExceptionsPrivilegesFromKibanaPrivileges', () => {
it('calculates endpoint exceptions privileges correctly', () => {
const endpointExceptionsPrivileges = [
{ privilege: `${SECURITY_SOLUTION_ID}-showEndpointExceptions`, authorized: true },
{ privilege: `${SECURITY_SOLUTION_ID}-crudEndpointExceptions`, authorized: false },
{ privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: true },
];
const expected = {
actions: {
showEndpointExceptions: true,
crudEndpointExceptions: false,
},
};
const actual = calculateEndpointExceptionsPrivilegesFromKibanaPrivileges(
endpointExceptionsPrivileges
);
expect(actual).toEqual(expected);
});
});

describe('#getAuthorizationFromPrivileges', () => {
it('returns `false` when no `prefix` nor `searchPrivilege`', () => {
expect(
getAuthorizationFromPrivileges({
kibanaPrivileges: [
{
privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`,
authorized: true,
},
],
})
).toEqual(false);
});

it('returns correct Boolean when `prefix` and `searchPrivilege` are given', () => {
const kibanaPrivileges = [
{ privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolationExceptions`, authorized: false },
{ privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true },
{ privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false },
];

expect(
getAuthorizationFromPrivileges({
kibanaPrivileges,
prefix: `${SECURITY_SOLUTION_ID}-`,
searchPrivilege: `writeHostIsolation`,
})
).toEqual(true);
});

it('returns correct Boolean when only `prefix` is given', () => {
const kibanaPrivileges = [
{ privilege: `ignore-me-writeHostIsolationExceptions`, authorized: false },
{ privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true },
{ privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false },
];

expect(
getAuthorizationFromPrivileges({
kibanaPrivileges,
prefix: `${SECURITY_SOLUTION_ID}-`,
searchPrivilege: `writeHostIsolation`,
})
).toEqual(true);
});

it('returns correct Boolean when only `searchPrivilege` is given', () => {
const kibanaPrivileges = [
{ privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolationExceptions`, authorized: false },
{ privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true },
{ privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false },
];

expect(
getAuthorizationFromPrivileges({
kibanaPrivileges,
searchPrivilege: `writeHostIsolation`,
})
).toEqual(true);
});
});
});
107 changes: 85 additions & 22 deletions x-pack/plugins/fleet/common/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Capabilities } from '@kbn/core-capabilities-common';

import { TRANSFORM_PLUGIN_ID } from './constants/plugin';

import { ENDPOINT_PRIVILEGES } from './constants';
import { ENDPOINT_EXCEPTIONS_PRIVILEGES, ENDPOINT_PRIVILEGES } from './constants';

export type TransformPrivilege =
| 'canGetTransform'
Expand Down Expand Up @@ -49,6 +49,13 @@ export interface FleetAuthz {
};
};
};

endpointExceptionsPrivileges?: {
actions: {
crudEndpointExceptions: boolean;
showEndpointExceptions: boolean;
};
};
}

interface CalculateParams {
Expand Down Expand Up @@ -135,19 +142,50 @@ export function calculatePackagePrivilegesFromCapabilities(
};
}

function getAuthorizationFromPrivileges(
export function calculateEndpointExceptionsPrivilegesFromCapabilities(
capabilities: Capabilities | undefined
): FleetAuthz['endpointExceptionsPrivileges'] {
if (!capabilities || !capabilities.siem) {
return;
}

const endpointExceptionsActions = Object.keys(ENDPOINT_EXCEPTIONS_PRIVILEGES).reduce<
Record<string, boolean>
>((acc, privilegeName) => {
acc[privilegeName] = (capabilities.siem[privilegeName] as boolean) || false;
return acc;
}, {});

return {
actions: endpointExceptionsActions,
} as FleetAuthz['endpointExceptionsPrivileges'];
}

export function getAuthorizationFromPrivileges({
kibanaPrivileges,
searchPrivilege = '',
prefix = '',
}: {
kibanaPrivileges: Array<{
resource?: string;
privilege: string;
authorized: boolean;
}>,
prefix: string,
searchPrivilege: string
): boolean {
const privilege = kibanaPrivileges.find((p) =>
p.privilege.endsWith(`${prefix}${searchPrivilege}`)
);
return privilege?.authorized || false;
}>;
prefix?: string;
searchPrivilege?: string;
}): boolean {
const privilege = kibanaPrivileges.find((p) => {
if (prefix.length && searchPrivilege.length) {
return p.privilege.endsWith(`${prefix}${searchPrivilege}`);
} else if (prefix.length) {
return p.privilege.endsWith(`${prefix}`);
} else if (searchPrivilege.length) {
return p.privilege.endsWith(`${searchPrivilege}`);
}
return false;
});

return !!privilege?.authorized;
}

export function calculatePackagePrivilegesFromKibanaPrivileges(
Expand All @@ -165,11 +203,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges(

const endpointActions = Object.entries(ENDPOINT_PRIVILEGES).reduce<PrivilegeMap>(
(acc, [privilege, { appId, privilegeSplit, privilegeName }]) => {
const kibanaPrivilege = getAuthorizationFromPrivileges(
const kibanaPrivilege = getAuthorizationFromPrivileges({
kibanaPrivileges,
`${appId}${privilegeSplit}`,
privilegeName
);
prefix: `${appId}${privilegeSplit}`,
searchPrivilege: privilegeName,
});
acc[privilege] = {
executePackageAction: kibanaPrivilege,
};
Expand All @@ -178,11 +216,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges(
{}
);

const hasTransformAdmin = getAuthorizationFromPrivileges(
const hasTransformAdmin = getAuthorizationFromPrivileges({
kibanaPrivileges,
`${TRANSFORM_PLUGIN_ID}-`,
`admin`
);
prefix: `${TRANSFORM_PLUGIN_ID}-`,
searchPrivilege: `admin`,
});
const transformActions: {
[key in TransformPrivilege]: {
executePackageAction: boolean;
Expand All @@ -198,11 +236,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges(
executePackageAction: hasTransformAdmin,
},
canGetTransform: {
executePackageAction: getAuthorizationFromPrivileges(
executePackageAction: getAuthorizationFromPrivileges({
kibanaPrivileges,
`${TRANSFORM_PLUGIN_ID}-`,
`read`
),
prefix: `${TRANSFORM_PLUGIN_ID}-`,
searchPrivilege: `read`,
}),
},
};

Expand All @@ -215,3 +253,28 @@ export function calculatePackagePrivilegesFromKibanaPrivileges(
},
};
}

export function calculateEndpointExceptionsPrivilegesFromKibanaPrivileges(
kibanaPrivileges:
| Array<{
resource?: string;
privilege: string;
authorized: boolean;
}>
| undefined
): FleetAuthz['endpointExceptionsPrivileges'] {
if (!kibanaPrivileges || !kibanaPrivileges.length) {
return;
}
const endpointExceptionsActions = Object.entries(ENDPOINT_EXCEPTIONS_PRIVILEGES).reduce<
Record<string, boolean>
>((acc, [privilege, { appId, privilegeSplit, privilegeName }]) => {
acc[privilege] = getAuthorizationFromPrivileges({
kibanaPrivileges,
searchPrivilege: privilegeName,
});
return acc;
}, {});

return { actions: endpointExceptionsActions } as FleetAuthz['endpointExceptionsPrivileges'];
}
17 changes: 16 additions & 1 deletion x-pack/plugins/fleet/common/constants/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';

const SECURITY_SOLUTION_APP_ID = 'siem';

interface PrivilegeMapObject {
export interface PrivilegeMapObject {
appId: string;
privilegeSplit: string;
privilegeType: 'ui' | 'api';
Expand Down Expand Up @@ -163,3 +163,18 @@ export const ENDPOINT_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreez
privilegeName: 'writeExecuteOperations',
},
});

export const ENDPOINT_EXCEPTIONS_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreeze({
showEndpointExceptions: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'showEndpointExceptions',
},
crudEndpointExceptions: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'crudEndpointExceptions',
},
});
10 changes: 8 additions & 2 deletions x-pack/plugins/fleet/common/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
*/

import type {
PostDeletePackagePoliciesResponse,
AgentPolicy,
NewPackagePolicy,
PackagePolicy,
AgentPolicy,
PostDeletePackagePoliciesResponse,
} from './types';
import type { FleetAuthz } from './authz';
import { dataTypes, ENDPOINT_PRIVILEGES } from './constants';
Expand Down Expand Up @@ -108,6 +108,12 @@ export const createFleetAuthzMock = (): FleetAuthz => {
},
},
},
endpointExceptionsPrivileges: {
actions: {
showEndpointExceptions: true,
crudEndpointExceptions: true,
},
},
};
};

Expand Down
Loading

0 comments on commit a8de031

Please sign in to comment.