Skip to content

Commit

Permalink
[backend/frontend] adding decay chart in indicator lifecycle overview. (
Browse files Browse the repository at this point in the history
  • Loading branch information
aHenryJard committed Jan 26, 2024
1 parent d0758d5 commit 0530e3c
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { FunctionComponent } from 'react';
import { IndicatorDetails_indicator$data } from '@components/observations/indicators/__generated__/IndicatorDetails_indicator.graphql';
import Chart from '@components/common/charts/Chart';
import { ApexOptions } from 'apexcharts';
import moment from 'moment';
import { useTheme } from '@mui/styles';
import { Theme } from '@mui/material/styles/createTheme';

interface DecayChartProps {
indicator: IndicatorDetails_indicator$data,
}

const DecayChart : FunctionComponent<DecayChartProps> = ({ indicator }) => {
const theme = useTheme<Theme>();

const liveScoreColor = theme.palette.success.main;
const reactionPointColor = theme.palette.text.primary;
const scoreColor = theme.palette.info.main;
const revokeColor = theme.palette.secondary.main;

const chartLabelsTextColor = theme.palette.info.contrastText;
const chartInfoTextColor = theme.palette.text.primary;
const chartBackgroundColor = theme.palette.background.default;

const graphLinesAnnotations = [];

// Horizontal lines that shows reaction points
if (indicator.decay_applied_rule?.decay_points) {
indicator.decay_applied_rule.decay_points.forEach((reactionPoint) => {
const lineReactionValue = {
y: reactionPoint,
borderColor: reactionPoint === indicator.x_opencti_score ? scoreColor : reactionPointColor,
label: {
borderColor: reactionPoint === indicator.x_opencti_score ? scoreColor : reactionPointColor,
offsetY: 0,
style: {
color: chartLabelsTextColor,
background: reactionPoint === indicator.x_opencti_score ? scoreColor : reactionPointColor,
},
text: reactionPoint === indicator.x_opencti_score ? `Score: ${reactionPoint}` : `${reactionPoint}`,
},
};
graphLinesAnnotations.push(lineReactionValue);
});

// Horizontal "red" area that show the revoke zone
const revokeScoreArea = {
y: indicator.decay_applied_rule.decay_revoke_score + 1, // trick to have a red line even if revoke score is 0
y2: 0,
borderColor: revokeColor,
fillColor: revokeColor,
label: {
text: `Revoke zone: ${indicator.decay_applied_rule.decay_revoke_score}`,
borderColor: revokeColor,
style: {
color: chartLabelsTextColor,
background: revokeColor,
},
},
};
graphLinesAnnotations.push(revokeScoreArea);
}

// Horizontal line that the current / live score
const liveScoreLine = {
y: indicator.decayLiveDetails?.live_score,
borderColor: liveScoreColor,
label: {
borderColor: liveScoreColor,
style: {
color: chartLabelsTextColor,
background: liveScoreColor,
},
text: `Live score:${indicator.decayLiveDetails?.live_score}`,
},
};
graphLinesAnnotations.push(liveScoreLine);

// Time in millisecond cannot be set as number in GraphQL because it's too long
// So the time in data is stored as Date and must be converted to time in ms to be drawn on the chart.
const liveScoreApexFormat: { x: number; y: number }[] = [];
if (indicator.decayChartData && indicator.decayChartData.live_score_serie) {
indicator.decayChartData.live_score_serie.forEach((dataPoint) => {
liveScoreApexFormat.push({
x: moment(dataPoint.time).valueOf(),
y: dataPoint.score,
});
});
}

const pointAnnotations = [];
pointAnnotations.push({
x: new Date().getTime(),
y: indicator.decayLiveDetails?.live_score,
marker: {
fillColor: liveScoreColor,
},
});

const chartOptions: ApexOptions = {
chart: {
id: 'Decay graph',
toolbar: { show: false },
type: 'line',
background: chartBackgroundColor,
},
xaxis: {
type: 'datetime',
title: {
text: 'Days',
style: {
color: chartInfoTextColor,
},
},
labels: {
style: {
colors: chartInfoTextColor,
},
},
},
yaxis: {
min: 0,
max: 100,
title: {
text: 'Score',
style: {
color: chartInfoTextColor,
},
},
labels: {
style: {
colors: chartInfoTextColor,
},
},
},
annotations: {
yaxis: graphLinesAnnotations,
points: pointAnnotations,
},
grid: { show: false },
colors: [
theme.palette.primary.main,
],
stroke: {
curve: 'smooth',
},
tooltip: {
theme: theme.palette.mode, // ApexChart uses 'dark'/'light', exactly the same values as we use in OpenCTI.
},
};

const series = [
{
name: 'Score with decay', // this is the text on the popover
data: liveScoreApexFormat,
},
];

return (
<Chart
series={series}
options={chartOptions}
/>
);
};

export default DecayChart;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SxProps } from '@mui/material';
import { Theme } from '@mui/material/styles/createTheme';
import { useTheme } from '@mui/styles';
import { IndicatorDetails_indicator$data } from '@components/observations/indicators/__generated__/IndicatorDetails_indicator.graphql';
import DecayChart from '@components/observations/indicators/DecayChart';
import { useFormatter } from '../../../../components/i18n';

interface DecayDialogContentProps {
Expand All @@ -34,22 +35,32 @@ const DecayDialogContent : FunctionComponent<DecayDialogContentProps> = ({ indic

const decayHistory = indicator.decay_history ? [...indicator.decay_history] : [];
const decayLivePoints = indicatorDecayDetails?.live_points ? [...indicatorDecayDetails.live_points] : [];
const decayReactionPoints = indicator.decay_applied_rule?.decay_points ?? [];

const currentScoreLineStyle = {
const currentLiveScoreLineStyle = {
color: theme.palette.success.main,
fontWeight: 'bold',
};

const currentScoreLineStyle = {
color: theme.palette.info.main,
fontWeight: 'bold',
};

const revokeScoreLineStyle = {
color: theme.palette.error.main,
color: theme.palette.secondary.main,
};

const normalHistoryLineStyle = {
color: theme.palette.text.primary,
};

const decayFullHistory: LabelledDecayHistory[] = [];
decayHistory.map((history, index) => (
decayFullHistory.push({
score: history.score,
updated_at: history.updated_at,
label: index === 0 ? 'Score at creation' : 'Score updated',
style: index === decayHistory.length - 1 ? currentScoreLineStyle : {},
style: history.score === indicator.x_opencti_score ? currentScoreLineStyle : normalHistoryLineStyle,
})
));

Expand All @@ -58,7 +69,7 @@ const DecayDialogContent : FunctionComponent<DecayDialogContentProps> = ({ indic
score: history.score,
updated_at: history.updated_at,
label: index === decayLivePoints.length - 1 ? 'Revoke score' : 'Score update planned',
style: index === decayLivePoints.length - 1 ? revokeScoreLineStyle : {},
style: index === decayLivePoints.length - 1 ? revokeScoreLineStyle : normalHistoryLineStyle,
})
));

Expand All @@ -67,7 +78,7 @@ const DecayDialogContent : FunctionComponent<DecayDialogContentProps> = ({ indic
score: indicatorDecayDetails.live_score,
updated_at: new Date(),
label: 'Current live score',
style: currentScoreLineStyle,
style: currentLiveScoreLineStyle,
});
}

Expand All @@ -82,7 +93,10 @@ const DecayDialogContent : FunctionComponent<DecayDialogContentProps> = ({ indic
spacing={3}
style={{ borderColor: 'white', borderWidth: 1 }}
>
<Grid item={true} xs={8}>
<Grid item={true} xs={6}>
<DecayChart indicator={indicator}/>
</Grid>
<Grid item={true} xs={6}>
<Typography variant="h6">
{t_i18n('Lifecycle key information')}
</Typography>
Expand All @@ -109,18 +123,7 @@ const DecayDialogContent : FunctionComponent<DecayDialogContentProps> = ({ indic
</Table>
</TableContainer>
</Grid>
<Grid item={true} xs={4}>
<Typography variant="h6">
{t_i18n('Applied decay rule')}
</Typography>
<ul>
<li>{t_i18n('Base score:')} { indicator.decay_base_score }</li>
<li>{t_i18n('Lifetime (in days):')} { indicator.decay_applied_rule?.decay_lifetime ?? 'Not set'}</li>
<li>{t_i18n('Pound factor:')} { indicator.decay_applied_rule?.decay_pound ?? 'Not set'}</li>
<li>{t_i18n('Revoke score:')} { indicator.decay_applied_rule?.decay_revoke_score ?? 'Not set'}</li>
<li>{t_i18n('Reaction points:')} {decayReactionPoints.join(', ')}</li>
</ul>
</Grid>

</Grid>
</DialogContent>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const IndicatorDetailsComponent: FunctionComponent<IndicatorDetailsComponentProp
TransitionComponent={Transition}
onClose={onDecayLifecycleClose}
fullWidth
maxWidth="md"
maxWidth='lg'
>
<DialogTitle>{t_i18n('Lifecycle details')}</DialogTitle>
<DecayDialogContent indicator={indicator} />
Expand Down Expand Up @@ -235,6 +235,12 @@ const IndicatorDetails = createFragmentContainer(IndicatorDetailsComponent, {
updated_at
}
}
decayChartData {
live_score_serie {
time
score
}
}
objectLabel {
edges {
node {
Expand Down
10 changes: 10 additions & 0 deletions opencti-platform/opencti-front/src/schema/relay.schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -10529,6 +10529,15 @@ type DecayLiveDetails {
live_points: [DecayHistory!]
}

type DecayChartPoint {
time: DateTime!
score: Int!
}

type DecayChartData {
live_score_serie: [DecayChartPoint!]
}

type Indicator implements BasicObject & StixObject & StixCoreObject & StixDomainObject {
id: ID!
standard_id: String!
Expand Down Expand Up @@ -10585,6 +10594,7 @@ type Indicator implements BasicObject & StixObject & StixCoreObject & StixDomain
decay_applied_rule: DecayRule
decay_history: [DecayHistory!]
decayLiveDetails: DecayLiveDetails
decayChartData: DecayChartData
creators: [Creator!]
toStix: String
importFiles(first: Int, prefixMimeType: String): FileConnection
Expand Down
30 changes: 30 additions & 0 deletions opencti-platform/opencti-graphql/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5159,6 +5159,17 @@ export enum DataSourcesOrdering {
XOpenctiWorkflowId = 'x_opencti_workflow_id'
}

export type DecayChartData = {
__typename?: 'DecayChartData';
live_score_serie?: Maybe<Array<DecayChartPoint>>;
};

export type DecayChartPoint = {
__typename?: 'DecayChartPoint';
score: Scalars['Int']['output'];
time: Scalars['DateTime']['output'];
};

export type DecayHistory = {
__typename?: 'DecayHistory';
score: Scalars['Int']['output'];
Expand Down Expand Up @@ -8820,6 +8831,7 @@ export type Indicator = BasicObject & StixCoreObject & StixDomainObject & StixOb
createdBy?: Maybe<Identity>;
created_at: Scalars['DateTime']['output'];
creators?: Maybe<Array<Creator>>;
decayChartData?: Maybe<DecayChartData>;
decayLiveDetails?: Maybe<DecayLiveDetails>;
decay_applied_rule?: Maybe<DecayRule>;
decay_base_score?: Maybe<Scalars['Int']['output']>;
Expand Down Expand Up @@ -27357,6 +27369,8 @@ export type ResolversTypes = ResolversObject<{
DataSourceEdge: ResolverTypeWrapper<Omit<DataSourceEdge, 'node'> & { node: ResolversTypes['DataSource'] }>;
DataSourcesOrdering: DataSourcesOrdering;
DateTime: ResolverTypeWrapper<Scalars['DateTime']['output']>;
DecayChartData: ResolverTypeWrapper<DecayChartData>;
DecayChartPoint: ResolverTypeWrapper<DecayChartPoint>;
DecayHistory: ResolverTypeWrapper<DecayHistory>;
DecayLiveDetails: ResolverTypeWrapper<DecayLiveDetails>;
DecayRule: ResolverTypeWrapper<DecayRule>;
Expand Down Expand Up @@ -28077,6 +28091,8 @@ export type ResolversParentTypes = ResolversObject<{
DataSourceConnection: Omit<DataSourceConnection, 'edges'> & { edges?: Maybe<Array<Maybe<ResolversParentTypes['DataSourceEdge']>>> };
DataSourceEdge: Omit<DataSourceEdge, 'node'> & { node: ResolversParentTypes['DataSource'] };
DateTime: Scalars['DateTime']['output'];
DecayChartData: DecayChartData;
DecayChartPoint: DecayChartPoint;
DecayHistory: DecayHistory;
DecayLiveDetails: DecayLiveDetails;
DecayRule: DecayRule;
Expand Down Expand Up @@ -30280,6 +30296,17 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversT
name: 'DateTime';
}

export type DecayChartDataResolvers<ContextType = any, ParentType extends ResolversParentTypes['DecayChartData'] = ResolversParentTypes['DecayChartData']> = ResolversObject<{
live_score_serie?: Resolver<Maybe<Array<ResolversTypes['DecayChartPoint']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type DecayChartPointResolvers<ContextType = any, ParentType extends ResolversParentTypes['DecayChartPoint'] = ResolversParentTypes['DecayChartPoint']> = ResolversObject<{
score?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
time?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type DecayHistoryResolvers<ContextType = any, ParentType extends ResolversParentTypes['DecayHistory'] = ResolversParentTypes['DecayHistory']> = ResolversObject<{
score?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
updated_at?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
Expand Down Expand Up @@ -31449,6 +31476,7 @@ export type IndicatorResolvers<ContextType = any, ParentType extends ResolversPa
createdBy?: Resolver<Maybe<ResolversTypes['Identity']>, ParentType, ContextType>;
created_at?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
creators?: Resolver<Maybe<Array<ResolversTypes['Creator']>>, ParentType, ContextType>;
decayChartData?: Resolver<Maybe<ResolversTypes['DecayChartData']>, ParentType, ContextType>;
decayLiveDetails?: Resolver<Maybe<ResolversTypes['DecayLiveDetails']>, ParentType, ContextType>;
decay_applied_rule?: Resolver<Maybe<ResolversTypes['DecayRule']>, ParentType, ContextType>;
decay_base_score?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
Expand Down Expand Up @@ -36890,6 +36918,8 @@ export type Resolvers<ContextType = any> = ResolversObject<{
DataSourceConnection?: DataSourceConnectionResolvers<ContextType>;
DataSourceEdge?: DataSourceEdgeResolvers<ContextType>;
DateTime?: GraphQLScalarType;
DecayChartData?: DecayChartDataResolvers<ContextType>;
DecayChartPoint?: DecayChartPointResolvers<ContextType>;
DecayHistory?: DecayHistoryResolvers<ContextType>;
DecayLiveDetails?: DecayLiveDetailsResolvers<ContextType>;
DecayRule?: DecayRuleResolvers<ContextType>;
Expand Down
Loading

0 comments on commit 0530e3c

Please sign in to comment.