Skip to content

Commit 9588cc1

Browse files
#RI-3572-dangerous commands recommendation
1 parent 110acd7 commit 9588cc1

File tree

9 files changed

+245
-100
lines changed

9 files changed

+245
-100
lines changed

redisinsight/api/src/constants/recommendations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export const RECOMMENDATION_NAMES = Object.freeze({
1717
REDIS_VERSION: 'redisVersion',
1818
REDIS_SEARCH: 'redisSearch',
1919
SEARCH_INDEXES: 'searchIndexes',
20+
DANGEROUS_COMMANDS: 'dangerousCommands',
2021
});

redisinsight/api/src/modules/database-analysis/database-analysis.service.ts

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import { HttpException, Injectable, Logger } from '@nestjs/common';
22
import { isNull, flatten, concat } from 'lodash';
33
import { RecommendationService } from 'src/modules/recommendation/recommendation.service';
44
import { catchAclError } from 'src/utils';
5-
import { RECOMMENDATION_NAMES } from 'src/constants';
65
import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer';
76
import { plainToClass } from 'class-transformer';
87
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
98
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
109
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
1110
import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';
12-
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
1311
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
1412
import { ClientMetadata } from 'src/common/models';
1513

@@ -55,41 +53,25 @@ export class DatabaseAnalysisService {
5553
progress.total += nodeResult.progress.total;
5654
});
5755

58-
const recommendations = DatabaseAnalysisService.getRecommendationsSummary(
59-
// flatten(await Promise.all(
60-
// scanResults.map(async (nodeResult, idx) => (
61-
// await this.recommendationService.getRecommendations({
62-
// client: nodeResult.client,
63-
// keys: nodeResult.keys,
64-
// total: progress.total,
65-
// globalClient: client,
66-
// // TODO: create generic solution to exclude recommendations
67-
// exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [],
68-
// })
69-
// )),
70-
// )),
71-
await scanResults.reduce(async (previousPromise, nodeResult) => {
72-
const jobsArray = await previousPromise;
73-
let recommendationToExclude = [];
74-
const nodeRecommendations = await this.recommendationService.getRecommendations({
75-
client: nodeResult.client,
76-
keys: nodeResult.keys,
77-
total: progress.total,
78-
globalClient: client,
79-
exclude: recommendationToExclude,
80-
// TODO: create generic solution to exclude recommendations
81-
// exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [],
82-
});
83-
recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]);
84-
const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation));
85-
const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name);
86-
recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames);
87-
jobsArray.push(foundedRecommendations);
88-
console.log(recommendationToExclude);
89-
console.log(foundedRecommendations);
90-
return flatten(jobsArray);
91-
}, Promise.resolve([])),
92-
);
56+
let recommendationToExclude = [];
57+
58+
const recommendations = await scanResults.reduce(async (previousPromise, nodeResult) => {
59+
const jobsArray = await previousPromise;
60+
const nodeRecommendations = await this.recommendationService.getRecommendations({
61+
client: nodeResult.client,
62+
keys: nodeResult.keys,
63+
total: progress.total,
64+
globalClient: client,
65+
exclude: recommendationToExclude,
66+
});
67+
// recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]);
68+
const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation));
69+
const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name);
70+
recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames);
71+
recommendationToExclude.push(...foundedRecommendationNames);
72+
jobsArray.push(foundedRecommendations);
73+
return flatten(jobsArray);
74+
}, Promise.resolve([]));
9375

9476
const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({
9577
databaseId: clientMetadata.databaseId,
@@ -127,17 +109,4 @@ export class DatabaseAnalysisService {
127109
async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {
128110
return this.databaseAnalysisProvider.list(databaseId);
129111
}
130-
131-
/**
132-
* Get recommendations summary
133-
* @param recommendations
134-
*/
135-
136-
static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] {
137-
// return uniqBy(
138-
// recommendations,
139-
// 'name',
140-
// );
141-
return recommendations;
142-
}
143112
}

redisinsight/api/src/modules/database-analysis/models/recommendation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Expose } from 'class-transformer';
2-
import { ApiProperty } from '@nestjs/swagger';
2+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
33

44
export class Recommendation {
55
@ApiProperty({
@@ -9,4 +9,11 @@ export class Recommendation {
99
})
1010
@Expose()
1111
name: string;
12+
13+
@ApiPropertyOptional({
14+
description: 'Additional recommendation params',
15+
example: 'luaScript',
16+
})
17+
@Expose()
18+
params?: any;
1219
}

redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/u
77
import {
88
RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX,
99
} from 'src/constants';
10+
import ERROR_MESSAGES from 'src/constants/error-messages';
11+
import { ClusterNodeNotFoundError } from 'src/modules/cli/constants/errors';
1012
import { checkRedirectionError, parseRedirectionError } from 'src/utils/cli-helper';
1113
import { RedisDataType } from 'src/modules/browser/dto';
1214
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
@@ -25,6 +27,8 @@ const bigStringMemory = 5_000_000;
2527
const sortedSetCountForCheck = 100;
2628
const minRedisVersion = '6';
2729

30+
const redisInsightCommands = ['info', 'monitor', 'slowlog', 'acl', 'config', 'module'];
31+
2832
@Injectable()
2933
export class RecommendationProvider {
3034
private logger = new Logger('RecommendationProvider');
@@ -411,13 +415,14 @@ export class RecommendationProvider {
411415
return null;
412416
}
413417
}
418+
414419
/**
415420
* Check search indexes recommendation
416421
* @param redisClient
417422
* @param keys
418-
* @param isCluster
423+
* @param client
419424
*/
420-
425+
// eslint-disable-next-line
421426
async determineSearchIndexesRecommendation(
422427
redisClient: Redis,
423428
keys: Key[],
@@ -450,14 +455,11 @@ export class RecommendationProvider {
450455
const nodes = client.nodes('master');
451456

452457
const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address);
453-
// if (!node) {
454-
// node = nodeRole === ClusterNodeRole.All
455-
// ? nodeAddress
456-
// : `${nodeAddress} [${nodeRole.toLowerCase()}]`;
457-
// // throw new ClusterNodeNotFoundError(
458-
// // ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node),
459-
// // );
460-
// }
458+
if (!node) {
459+
throw new ClusterNodeNotFoundError(
460+
ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node),
461+
);
462+
}
461463

462464
keyBySortedSetMember = await node.sendCommand(
463465
new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }),
@@ -490,14 +492,40 @@ export class RecommendationProvider {
490492
member,
491493
]))).exec();
492494

493-
const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash)
495+
const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash);
494496
return isHashOrJSONName ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null;
495497
} catch (err) {
496498
this.logger.error('Can not determine search indexes recommendation', err);
497499
return null;
498500
}
499501
}
500502

503+
/*
504+
* Check dangerous commands recommendation
505+
* @param redisClient
506+
*/
507+
508+
async determineDangerousCommandsRecommendation(
509+
redisClient: Redis | Cluster,
510+
): Promise<Recommendation> {
511+
try {
512+
const dangerousCommands = await redisClient.sendCommand(
513+
new Command('command', ['LIST', 'FILTERBY', 'aclcat', 'dangerous'], { replyEncoding: 'utf8' }),
514+
) as string[];
515+
516+
const filteredDangerousCommands = dangerousCommands.filter((command) => {
517+
const commandName = command.split('|')[0];
518+
return !redisInsightCommands.includes(commandName);
519+
});
520+
const commands = filteredDangerousCommands.join('\r\n').toUpperCase();
521+
return filteredDangerousCommands
522+
? { name: RECOMMENDATION_NAMES.DANGEROUS_COMMANDS, params: { commands } }
523+
: null;
524+
} catch (err) {
525+
this.logger.error('Can not determine dangerous commands recommendation', err);
526+
return null;
527+
}
528+
}
501529

502530
private async checkAuth(redisClient: Redis | Cluster): Promise<boolean> {
503531
try {

redisinsight/api/src/modules/recommendation/recommendation.service.ts

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { Redis, Cluster } from 'ioredis';
3+
import { difference } from 'lodash';
34
import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider';
45
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
56
import { RECOMMENDATION_NAMES } from 'src/constants';
@@ -38,27 +39,88 @@ export class RecommendationService {
3839
exclude,
3940
} = dto;
4041

42+
const recommendations = new Map([
43+
[
44+
RECOMMENDATION_NAMES.LUA_SCRIPT,
45+
async () => await this.recommendationProvider.determineLuaScriptRecommendation(client),
46+
],
47+
[
48+
RECOMMENDATION_NAMES.BIG_HASHES,
49+
async () => await this.recommendationProvider.determineBigHashesRecommendation(keys),
50+
],
51+
[
52+
RECOMMENDATION_NAMES.USE_SMALLER_KEYS,
53+
async () => await this.recommendationProvider.determineBigTotalRecommendation(total),
54+
],
55+
[
56+
RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,
57+
async () => await this.recommendationProvider.determineLogicalDatabasesRecommendation(client),
58+
],
59+
[
60+
RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES,
61+
async () => await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys),
62+
],
63+
[
64+
RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES,
65+
async () => await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys),
66+
],
67+
[
68+
RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST,
69+
async () => await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(client, keys),
70+
],
71+
[
72+
RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES,
73+
async () => await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys),
74+
],
75+
[
76+
RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,
77+
async () => await this.recommendationProvider.determineCompressionForListRecommendation(keys),
78+
],
79+
[
80+
RECOMMENDATION_NAMES.BIG_STRINGS,
81+
async () => await this.recommendationProvider.determineBigStringsRecommendation(keys),
82+
],
83+
[
84+
RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST,
85+
async () => await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys),
86+
],
87+
[
88+
RECOMMENDATION_NAMES.BIG_SETS,
89+
async () => await this.recommendationProvider.determineBigSetsRecommendation(keys),
90+
],
91+
[
92+
RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,
93+
async () => await this.recommendationProvider.determineConnectionClientsRecommendation(client),
94+
],
95+
[
96+
RECOMMENDATION_NAMES.RTS,
97+
async () => await this.recommendationProvider.determineRTSRecommendation(client, keys),
98+
],
99+
[
100+
RECOMMENDATION_NAMES.REDIS_SEARCH,
101+
async () => await this.recommendationProvider.determineRediSearchRecommendation(client, keys),
102+
],
103+
[
104+
RECOMMENDATION_NAMES.REDIS_VERSION,
105+
async () => await this.recommendationProvider.determineRedisVersionRecommendation(client),
106+
],
107+
[
108+
RECOMMENDATION_NAMES.SEARCH_INDEXES,
109+
async () => await this.recommendationProvider.determineSearchIndexesRecommendation(client, keys, globalClient),
110+
],
111+
[
112+
RECOMMENDATION_NAMES.DANGEROUS_COMMANDS,
113+
async () => await this.recommendationProvider.determineDangerousCommandsRecommendation(client),
114+
],
115+
[
116+
RECOMMENDATION_NAMES.SET_PASSWORD,
117+
async () => await this.recommendationProvider.determineSetPasswordRecommendation(client),
118+
],
119+
]);
120+
121+
const recommendationsToDetermine = difference(Object.values(RECOMMENDATION_NAMES), exclude);
122+
41123
return (
42-
Promise.all([
43-
await this.recommendationProvider.determineLuaScriptRecommendation(client),
44-
await this.recommendationProvider.determineBigHashesRecommendation(keys),
45-
await this.recommendationProvider.determineBigTotalRecommendation(total),
46-
await this.recommendationProvider.determineLogicalDatabasesRecommendation(client),
47-
await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys),
48-
await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys),
49-
await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(client, keys),
50-
await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys),
51-
await this.recommendationProvider.determineCompressionForListRecommendation(keys),
52-
await this.recommendationProvider.determineBigStringsRecommendation(keys),
53-
await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys),
54-
await this.recommendationProvider.determineBigSetsRecommendation(keys),
55-
await this.recommendationProvider.determineConnectionClientsRecommendation(client),
56-
// TODO rework, need better solution to do not start determine recommendation
57-
exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys),
58-
await this.recommendationProvider.determineRediSearchRecommendation(client, keys),
59-
await this.recommendationProvider.determineRedisVersionRecommendation(client),
60-
await this.recommendationProvider.determineSearchIndexesRecommendation(client, keys, globalClient),
61-
await this.recommendationProvider.determineSetPasswordRecommendation(client),
62-
]));
124+
Promise.all(recommendationsToDetermine.map((recommendation) => recommendations.get(recommendation)())));
63125
}
64126
}

0 commit comments

Comments
 (0)