Skip to content

Commit

Permalink
[Security Solution][Detections] Updates Get Installed Integrations ro…
Browse files Browse the repository at this point in the history
…ute to use Internal SO Client (#134373)

## Summary

Follow up to #134299 that updates the `Get Installed Integrations` route to use the Internal SO Client and removes client-side privilege checks. This change ensures the `Related Integrations` feature works regardless of client users Fleet/Integration/SOM privileges, enabling all Security Solution users the ability to see which integrations are installed/configured with relation to their Detection Rules.

This change is helpful for low privilege users like T1/T2 Analysts that may not have more broad privileges like SOM or Fleet to still see which of their Detection Rules have the necessary integrations.

### Test instructions

To test, configure a role with the 3 mentioned privileges (SOM/Integrations/Fleet) as `None`, e.g.

<p align="center">
  <img width="500" src="https://user-images.githubusercontent.com/2946766/173156872-dfaece7e-a6ef-4774-b01d-e2fa7b66a068.png" />
</p>


Then the UI should _still show_ installed details (installed/not installed badges):

##### Rules Table
<p align="center">
  <img width="500" src="https://user-images.githubusercontent.com/2946766/173658501-9b384ce6-ddef-4643-a36e-3da3082c8972.png" />
</p>


##### Rules Details
<p align="center">
  <img width="500" src="https://user-images.githubusercontent.com/2946766/173658533-a60b6e96-7283-441a-8faa-ed186476c1ac.png" />
</p>


### Checklist

Delete any items that are not applicable to this PR.

- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
  * Working docs as part of elastic/security-docs#2015
- [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

### Risk Ma
  • Loading branch information
spong authored Jun 20, 2022
1 parent e3aa999 commit 01b8482
Show file tree
Hide file tree
Showing 6 changed files with 15 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,18 @@
*/

import { calculateIntegrationDetails } from './integration_details';
import { IntegrationPrivileges } from './integration_privileges';

describe('Integration Details', () => {
describe('calculateIntegrationDetails', () => {
const stubPrivileges: IntegrationPrivileges = {
canReadInstalledIntegrations: true,
};

test('it returns a the correct integrationDetails', () => {
const integrationDetails = calculateIntegrationDetails(stubPrivileges, [], []);
const integrationDetails = calculateIntegrationDetails([], []);

expect(integrationDetails.length).toEqual(0);
});

describe('version is correctly computed', () => {
test('Unknown integration that does not exist', () => {
const integrationDetails = calculateIntegrationDetails(
stubPrivileges,
[
{
package: 'foo1',
Expand All @@ -48,7 +42,6 @@ describe('Integration Details', () => {

test('Integration that is not installed', () => {
const integrationDetails = calculateIntegrationDetails(
stubPrivileges,
[
{
package: 'aws',
Expand All @@ -69,7 +62,6 @@ describe('Integration Details', () => {

test('Integration that is installed, and its version matches required version', () => {
const integrationDetails = calculateIntegrationDetails(
stubPrivileges,
[
{
package: 'aws',
Expand Down Expand Up @@ -114,7 +106,6 @@ describe('Integration Details', () => {

test('Integration that is installed, and its version is less than required version', () => {
const integrationDetails = calculateIntegrationDetails(
stubPrivileges,
[
{
package: 'aws',
Expand Down Expand Up @@ -150,7 +141,6 @@ describe('Integration Details', () => {

test('Integration that is installed, and its version is greater than required version', () => {
const integrationDetails = calculateIntegrationDetails(
stubPrivileges,
[
{
package: 'aws',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
RelatedIntegration,
RelatedIntegrationArray,
} from '../../../../../common/detection_engine/schemas/common';
import { IntegrationPrivileges } from './integration_privileges';

export interface IntegrationDetails {
packageName: string;
Expand Down Expand Up @@ -45,13 +44,12 @@ export interface UnknownInstallationStatus {
* has.
*/
export const calculateIntegrationDetails = (
privileges: IntegrationPrivileges,
relatedIntegrations: RelatedIntegrationArray,
installedIntegrations: InstalledIntegrationArray | undefined
): IntegrationDetails[] => {
const integrationMatches = findIntegrationMatches(relatedIntegrations, installedIntegrations);
const integrationDetails = integrationMatches.map((integration) => {
return createIntegrationDetails(integration, privileges);
return createIntegrationDetails(integration);
});

return integrationDetails.sort((a, b) => {
Expand Down Expand Up @@ -90,19 +88,15 @@ const findIntegrationMatches = (
});
};

const createIntegrationDetails = (
integration: IntegrationMatch,
privileges: IntegrationPrivileges
): IntegrationDetails => {
const createIntegrationDetails = (integration: IntegrationMatch): IntegrationDetails => {
const { related, installed, isLoaded } = integration;
const { canReadInstalledIntegrations } = privileges;

const packageName = related.package;
const integrationName = related.integration ?? null;
const requiredVersion = related.version;

// We don't know whether the integration is installed or not.
if (!canReadInstalledIntegrations || !isLoaded) {
if (!isLoaded) {
const integrationTitle = getCapitalizedTitle(packageName, integrationName);
const targetVersion = getMinimumConcreteVersionMatchingSemver(requiredVersion);
const targetUrl = buildTargetUrl(packageName, integrationName, targetVersion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const IntegrationListItem = styled('li')`
*/
const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopoverProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const { integrations, privileges, isLoaded } = useRelatedIntegrations(relatedIntegrations);
const { integrations, isLoaded } = useRelatedIntegrations(relatedIntegrations);

const enabledIntegrations = useMemo(() => {
return integrations.filter(
Expand All @@ -65,10 +65,10 @@ const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopov
const numIntegrationsEnabled = enabledIntegrations.length;

const badgeTitle = useMemo(() => {
return privileges.canReadInstalledIntegrations && isLoaded
return isLoaded
? `${numIntegrationsEnabled}/${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`
: `${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`;
}, [privileges, isLoaded, numIntegrations, numIntegrationsEnabled]);
}, [isLoaded, numIntegrations, numIntegrationsEnabled]);

return (
<IntegrationsPopoverWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
import { useKibana } from '../../../../common/lib/kibana';
import { IntegrationPrivileges } from './integration_privileges';

/**
* Hook for determining if user has fleet/integrations/SOM privileges for fetching
* installed integrations. Initially used as we weren't using the fleet.internalReadonlySoClient
* for fetching integrations, but keeping this around for a release or two as we add more
* fleet/integration features within Security Solution in case it needs to be leveraged for those.
*/
export const useIntegrationPrivileges = (): IntegrationPrivileges => {
const services = useKibana().services;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,27 @@ import { useMemo } from 'react';

import { RelatedIntegrationArray } from '../../../../../common/detection_engine/schemas/common';
import { calculateIntegrationDetails, IntegrationDetails } from './integration_details';
import { IntegrationPrivileges } from './integration_privileges';
import { useIntegrationPrivileges } from './use_integration_privileges';
import { useInstalledIntegrations } from './use_installed_integrations';

export interface UseRelatedIntegrationsResult {
integrations: IntegrationDetails[];
privileges: IntegrationPrivileges;
isLoaded: boolean;
}

export const useRelatedIntegrations = (
relatedIntegrations: RelatedIntegrationArray
): UseRelatedIntegrationsResult => {
const privileges = useIntegrationPrivileges();
const { data: installedIntegrations } = useInstalledIntegrations({ packages: [] });

return useMemo(() => {
const integrationDetails = calculateIntegrationDetails(
privileges,
relatedIntegrations,
installedIntegrations
);

return {
integrations: integrationDetails,
privileges,
isLoaded: installedIntegrations != null,
};
}, [privileges, relatedIntegrations, installedIntegrations]);
}, [relatedIntegrations, installedIntegrations]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ export const getInstalledIntegrationsRoute = (router: SecuritySolutionPluginRout
try {
const ctx = await context.resolve(['core', 'securitySolution']);
const fleet = ctx.securitySolution.getInternalFleetServices();
const soClient = ctx.core.savedObjects.client;
const set = createInstalledIntegrationSet();

const packagePolicies = await fleet.packagePolicy.list(soClient, {});
const packagePolicies = await fleet.packagePolicy.list(fleet.internalReadonlySoClient, {});

packagePolicies.items.forEach((policy) => {
set.addPackagePolicy(policy);
Expand Down

0 comments on commit 01b8482

Please sign in to comment.