Skip to content

Commit 43a57af

Browse files
authored
Trendline namespace support
1 parent ed56c63 commit 43a57af

2 files changed

Lines changed: 80 additions & 15 deletions

File tree

x-pack/solutions/security/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
*/
77

88
import { ElasticsearchClient, Logger } from '@kbn/core/server';
9+
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
910
import { calculatePostureScore } from '../../../common/utils/helpers';
10-
import {
11-
BENCHMARK_SCORE_INDEX_DEFAULT_NS,
12-
CSPM_FINDINGS_STATS_INTERVAL,
13-
} from '../../../common/constants';
11+
import { BENCHMARK_SCORE_INDEX_DEFAULT_NS } from '../../../common/constants';
1412
import type { PosturePolicyTemplate, Stats } from '../../../common/types_old';
1513
import { toBenchmarkDocFieldKey } from '../../lib/mapping_field_util';
1614

@@ -45,11 +43,31 @@ export type Trends = Array<{
4543
clusters: Record<string, Stats>;
4644
benchmarks: Record<string, Stats>;
4745
}>;
48-
49-
export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate) => ({
46+
export interface ScoreTrendAggregateResponse {
47+
by_namespace: {
48+
buckets: Array<{
49+
key: string; // namespace name, e.g., "default"
50+
doc_count: number;
51+
all_scores: {
52+
hits: {
53+
hits: Array<{
54+
_source: {
55+
'@timestamp': string;
56+
total_findings: number;
57+
passed_findings: number;
58+
failed_findings: number;
59+
score_by_cluster_id: ScoreByClusterId;
60+
score_by_benchmark_id: ScoreByBenchmarkId;
61+
};
62+
}>;
63+
};
64+
};
65+
}>;
66+
};
67+
}
68+
export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate): SearchRequest => ({
5069
index: BENCHMARK_SCORE_INDEX_DEFAULT_NS,
51-
// Amount of samples of the last 24 hours (accounting that we take a sample every 5 minutes)
52-
size: (24 * 60) / CSPM_FINDINGS_STATS_INTERVAL,
70+
size: 0,
5371
sort: '@timestamp:desc',
5472
query: {
5573
bool: {
@@ -69,6 +87,31 @@ export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate) => ({
6987
],
7088
},
7189
},
90+
aggs: {
91+
by_namespace: {
92+
terms: {
93+
field: 'namespace',
94+
},
95+
aggs: {
96+
all_scores: {
97+
top_hits: {
98+
size: 100, // size: 100, // Maximum top hits result window is 100 which represents > 8 hours of scores samples (CSPM_FINDINGS_STATS_INTERVAL)
99+
sort: [{ '@timestamp': { order: 'desc' } }],
100+
_source: {
101+
includes: [
102+
'@timestamp',
103+
'total_findings',
104+
'passed_findings',
105+
'failed_findings',
106+
'score_by_cluster_id',
107+
'score_by_benchmark_id',
108+
],
109+
},
110+
},
111+
},
112+
},
113+
},
114+
},
72115
});
73116

74117
export const formatTrends = (scoreTrendDocs: ScoreTrendDoc[]): Trends => {
@@ -123,16 +166,28 @@ export const getTrends = async (
123166
logger: Logger
124167
): Promise<Trends> => {
125168
try {
126-
const trendsQueryResult = await esClient.search<ScoreTrendDoc>(getTrendsQuery(policyTemplate));
169+
const trendsQueryResult = await esClient.search<unknown, ScoreTrendAggregateResponse>(
170+
getTrendsQuery(policyTemplate)
171+
);
172+
if (!trendsQueryResult.aggregations?.by_namespace?.buckets)
173+
throw new Error('missing trend results from score index');
127174

128-
if (!trendsQueryResult.hits.hits) throw new Error('missing trend results from score index');
175+
const scoreTrendDocs =
176+
trendsQueryResult.aggregations.by_namespace.buckets.map((bucket) => {
177+
const namespace = bucket.key;
178+
const documents = bucket.all_scores?.hits?.hits?.map((hit) => hit._source) || [];
179+
return { [namespace]: { documents } };
180+
}) ?? [];
129181

130-
const scoreTrendDocs = trendsQueryResult.hits.hits.map((hit) => {
131-
if (!hit._source) throw new Error('missing _source data for one or more of trend results');
132-
return hit._source;
133-
});
182+
if (!scoreTrendDocs.length) return []; // No trends data available
183+
const result = Object.fromEntries(
184+
scoreTrendDocs.map((entry) => {
185+
const [key, value] = Object.entries(entry)[0];
186+
return [key, value.documents];
187+
})
188+
);
134189

135-
return formatTrends(scoreTrendDocs);
190+
return formatTrends(result.default); // Return the trends for the default namespace until namespace support will be visible to users.
136191
} catch (err) {
137192
logger.error(`Failed to fetch trendlines data ${err.message}`);
138193
logger.error(err);

x-pack/solutions/security/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ const getScoreQuery = (filteredRules: QueryDslQueryContainer[]): SearchRequest =
224224
runtime_mappings: { ...getIdentifierRuntimeMapping(), ...getSafePostureTypeRuntimeMapping() },
225225
query: {
226226
bool: {
227+
must: [
228+
{
229+
range: {
230+
'@timestamp': {
231+
gte: 'now-1d',
232+
lte: 'now',
233+
},
234+
},
235+
},
236+
],
227237
must_not: filteredRules,
228238
},
229239
},

0 commit comments

Comments
 (0)