Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Initial rule upgrade/install endpoints implementation #155517

Merged
merged 1 commit into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
*/

export interface GetPrebuiltRulesStatusResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all prebuilt rules */
stats: PrebuiltRulesStatusStats;
};
/** Aggregated info about all prebuilt rules */
stats: PrebuiltRulesStatusStats;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the endpoint open to returning other kind of information other than stats in the future; do we have any future use cases for that?

Just wandering why we would hold all data within the single stats property instead of making the stats themselves first-class properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I do not anticipate extending the endpoint to return information other than stats. However, it's always good practice to keep future use cases in mind and design with flexibility for potential changes. If a new use case does arise in the future, it would be pretty straightforward to adapt this endpoint.

}

export interface PrebuiltRulesStatusStats {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 * as t from 'io-ts';

export const RuleVersionSpecifier = t.exact(
t.type({
rule_id: t.string,
version: t.number,
})
);
export type RuleVersionSpecifier = t.TypeOf<typeof RuleVersionSpecifier>;

export const InstallSpecificRulesRequest = t.exact(
t.type({
mode: t.literal(`SPECIFIC_RULES`),
rules: t.array(RuleVersionSpecifier),
})
);

export const InstallAllRulesRequest = t.exact(
t.type({
mode: t.literal(`ALL_RULES`),
})
);

export const PerformRuleInstallationRequestBody = t.union([
InstallAllRulesRequest,
InstallSpecificRulesRequest,
]);

export type PerformRuleInstallationRequestBody = t.TypeOf<
typeof PerformRuleInstallationRequestBody
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { RuleResponse } from '../../../rule_schema/model/rule_schemas';
import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error';

export enum SkipRuleInstallReason {
ALREADY_INSTALLED = 'ALREADY_INSTALLED',
}

export interface SkippedRuleInstall {
rule_id: string;
reason: SkipRuleInstallReason;
}

export interface PerformRuleInstallationResponseBody {
summary: {
total: number;
succeeded: number;
skipped: number;
failed: number;
};
results: {
created: RuleResponse[];
skipped: SkippedRuleInstall[];
};
errors: AggregatedPrebuiltRuleError[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { enumeration } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';

export enum PickVersionValues {
BASE = 'BASE',
CURRENT = 'CURRENT',
TARGET = 'TARGET',
}

export const TPickVersionValues = enumeration('PickVersionValues', PickVersionValues);

export const RuleUpgradeSpecifier = t.exact(
t.intersection([
t.type({
rule_id: t.string,
/**
* This parameter is needed for handling race conditions with Optimistic Concurrency Control.
* Two or more users can call installation/_review and installation/_perform endpoints concurrently.
* Also, in general the time between these two calls can be anything.
* The idea is to only allow the user to install a rule if the user has reviewed the exact version
* of it that had been returned from the _review endpoint. If the version changed on the BE,
* installation/_perform endpoint will return a version mismatch error for this rule.
*/
revision: t.number,
/**
* The target version to upgrade to.
*/
version: t.number,
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);
export type RuleUpgradeSpecifier = t.TypeOf<typeof RuleUpgradeSpecifier>;

export const UpgradeSpecificRulesRequest = t.exact(
t.intersection([
t.type({
mode: t.literal(`SPECIFIC_RULES`),
rules: t.array(RuleUpgradeSpecifier),
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);

export const UpgradeAllRulesRequest = t.exact(
t.intersection([
t.type({
mode: t.literal(`ALL_RULES`),
}),
t.partial({
pick_version: TPickVersionValues,
}),
])
);

export const PerformRuleUpgradeRequestBody = t.union([
UpgradeAllRulesRequest,
UpgradeSpecificRulesRequest,
]);
export type PerformRuleUpgradeRequestBody = t.TypeOf<typeof PerformRuleUpgradeRequestBody>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { RuleResponse } from '../../../rule_schema';
import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error';

export enum SkipRuleUpgradeReason {
RULE_UP_TO_DATE = 'RULE_UP_TO_DATE',
}

export interface SkippedRuleUpgrade {
rule_id: string;
reason: SkipRuleUpgradeReason;
}

export interface PerformRuleUpgradeResponseBody {
summary: {
total: number;
succeeded: number;
skipped: number;
failed: number;
};
results: {
updated: RuleResponse[];
skipped: SkippedRuleUpgrade[];
};
errors: AggregatedPrebuiltRuleError[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ import type { RuleSignatureId, RuleTagArray, RuleVersion } from '../../../rule_s
import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule';

export interface ReviewRuleInstallationResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all rules available for installation */
stats: RuleInstallationStatsForReview;
/** Aggregated info about all rules available for installation */
stats: RuleInstallationStatsForReview;

/** Info about individual rules: one object per each rule available for installation */
rules: RuleInstallationInfoForReview[];
};
/** Info about individual rules: one object per each rule available for installation */
rules: RuleInstallationInfoForReview[];
}

export interface RuleInstallationStatsForReview {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@ import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule'
import type { PartialRuleDiff } from '../../model/diff/rule_diff/rule_diff';

export interface ReviewRuleUpgradeResponseBody {
status_code: number;
message: string;
attributes: {
/** Aggregated info about all rules available for upgrade */
stats: RuleUpgradeStatsForReview;
/** Aggregated info about all rules available for upgrade */
stats: RuleUpgradeStatsForReview;

/** Info about individual rules: one object per each rule available for upgrade */
rules: RuleUpgradeInfoForReview[];
};
/** Info about individual rules: one object per each rule available for upgrade */
rules: RuleUpgradeInfoForReview[];
}

export interface RuleUpgradeStatsForReview {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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.
*/

export interface AggregatedPrebuiltRuleError {
message: string;
status_code?: number;
rules: Array<{
rule_id: string;
name?: string;
}>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { getPrebuiltRulesAndTimelinesStatusRoute } from './route';
import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status_route';

import {
getEmptyFindResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@
*/

import { transformError } from '@kbn/securitysolution-es-utils';

import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/detection_engine/prebuilt_rules';
import type {
GetPrebuiltRulesStatusResponseBody,
PrebuiltRulesStatusStats,
} from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema';

import type { GetPrebuiltRulesStatusResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';

import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import type { VersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';

export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => {
Expand All @@ -40,23 +34,18 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient);
const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient);

const [latestVersions, { installedVersions }] = await Promise.all([
ruleAssetsClient.fetchLatestVersions(),
ruleObjectsClient.fetchInstalledRules(),
]);

const versionBuckets = getVersionBuckets({
latestVersions,
installedVersions,
const ruleVersionsMap = await fetchRuleVersionsTriad({
ruleAssetsClient,
ruleObjectsClient,
});

const stats = calculateRuleStats(versionBuckets);
const { currentRules, installableRules, upgradeableRules } =
getVersionBuckets(ruleVersionsMap);

const body: GetPrebuiltRulesStatusResponseBody = {
status_code: 200,
message: 'OK',
attributes: {
stats,
stats: {
num_prebuilt_rules_installed: currentRules.length,
num_prebuilt_rules_to_install: installableRules.length,
num_prebuilt_rules_to_upgrade: upgradeableRules.length,
},
};

Expand All @@ -71,13 +60,3 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
}
);
};

const calculateRuleStats = (buckets: VersionBuckets): PrebuiltRulesStatusStats => {
const { installedVersions, latestVersionsToInstall, installedVersionsToUpgrade } = buckets;

return {
num_prebuilt_rules_installed: installedVersions.length,
num_prebuilt_rules_to_install: latestVersionsToInstall.length,
num_prebuilt_rules_to_upgrade: installedVersionsToUpgrade.length,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
getBasicEmptySearchResponse,
} from '../../../routes/__mocks__/request_responses';
import { requestContextMock, serverMock } from '../../../routes/__mocks__';
import { installPrebuiltRulesAndTimelinesRoute, createPrepackagedRules } from './route';
import {
installPrebuiltRulesAndTimelinesRoute,
createPrepackagedRules,
} from './install_prebuilt_rules_and_timelines_route';
import { listMock } from '@kbn/lists-plugin/server/mocks';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
Expand Down
Loading