Commit 617c2a69 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'network-policy-stats' into 'master'

Network policy stats

See merge request gitlab-org/gitlab!25858
parents 448671c4 a2ae0a26
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions } from 'vuex';
import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover } from '@gitlab/ui'; import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ThreatMonitoringFilters from './threat_monitoring_filters.vue'; import ThreatMonitoringFilters from './threat_monitoring_filters.vue';
import WafLoadingSkeleton from './waf_loading_skeleton.vue'; import ThreatMonitoringSection from './threat_monitoring_section.vue';
import WafStatisticsSummary from './waf_statistics_summary.vue';
import WafStatisticsHistory from './waf_statistics_history.vue';
export default { export default {
name: 'ThreatMonitoring', name: 'ThreatMonitoring',
...@@ -17,10 +16,9 @@ export default { ...@@ -17,10 +16,9 @@ export default {
GlLink, GlLink,
GlPopover, GlPopover,
ThreatMonitoringFilters, ThreatMonitoringFilters,
WafLoadingSkeleton, ThreatMonitoringSection,
WafStatisticsSummary,
WafStatisticsHistory,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
defaultEnvironmentId: { defaultEnvironmentId: {
type: Number, type: Number,
...@@ -55,19 +53,15 @@ export default { ...@@ -55,19 +53,15 @@ export default {
return { return {
showAlert: this.showUserCallout, showAlert: this.showUserCallout,
// WAF requires the project to have at least one available environment. // We require the project to have at least one available environment.
// An invalid default environment id means there there are no available // An invalid default environment id means there there are no available
// environments, therefore the WAF cannot be set up. A valid default // environments, therefore infrastructure cannot be set up. A valid default
// environment id only means that WAF *might* be set up. // environment id only means that infrastructure *might* be set up.
isWafMaybeSetUp: this.isValidEnvironmentId(this.defaultEnvironmentId), isSetUpMaybe: this.isValidEnvironmentId(this.defaultEnvironmentId),
}; };
}, },
computed: {
...mapState('threatMonitoring', ['isLoadingWafStatistics']),
...mapGetters('threatMonitoring', ['hasHistory']),
},
created() { created() {
if (this.isWafMaybeSetUp) { if (this.isSetUpMaybe) {
this.setCurrentEnvironmentId(this.defaultEnvironmentId); this.setCurrentEnvironmentId(this.defaultEnvironmentId);
this.fetchEnvironments(); this.fetchEnvironments();
} }
...@@ -90,10 +84,19 @@ export default { ...@@ -90,10 +84,19 @@ export default {
application, it can happen. In any event, we ask that you double check your application, it can happen. In any event, we ask that you double check your
settings to make sure you've set up the WAF correctly.`, settings to make sure you've set up the WAF correctly.`,
), ),
wafChartEmptyStateDescription: s__(
`ThreatMonitoring|While it's rare to have no traffic coming to your
application, it can happen. In any event, we ask that you double check your
settings to make sure you've set up the WAF correctly.`,
),
networkPolicyChartEmptyStateDescription: s__(
`ThreatMonitoring|While it's rare to have no traffic coming to your
application, it can happen. In any event, we ask that you double check your
settings to make sure you've set up the Network Policies correctly.`,
),
emptyStateDescription: s__( emptyStateDescription: s__(
`ThreatMonitoring|A Web Application Firewall (WAF) provides monitoring and `ThreatMonitoring|Threat monitoring provides security monitoring and rules
rules to protect production applications. GitLab adds the modsecurity WAF to protect production applications.`,
plug-in when you install the Ingress app in your Kubernetes cluster.`,
), ),
alertText: s__( alertText: s__(
`ThreatMonitoring|The graph below is an overview of traffic coming to your `ThreatMonitoring|The graph below is an overview of traffic coming to your
...@@ -108,9 +111,9 @@ export default { ...@@ -108,9 +111,9 @@ export default {
<template> <template>
<gl-empty-state <gl-empty-state
v-if="!isWafMaybeSetUp" v-if="!isSetUpMaybe"
ref="emptyState" ref="emptyState"
:title="s__('ThreatMonitoring|Web Application Firewall not enabled')" :title="s__('ThreatMonitoring|Threat monitoring is not enabled')"
:description="$options.emptyStateDescription" :description="$options.emptyStateDescription"
:svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:primary-button-link="documentationPath" :primary-button-link="documentationPath"
...@@ -129,7 +132,7 @@ export default { ...@@ -129,7 +132,7 @@ export default {
{{ $options.alertText }} {{ $options.alertText }}
</gl-alert> </gl-alert>
<header class="my-3"> <header class="my-3">
<h2 class="h4 mb-1"> <h2 class="h3 mb-1">
{{ s__('ThreatMonitoring|Threat Monitoring') }} {{ s__('ThreatMonitoring|Threat Monitoring') }}
<gl-link <gl-link
ref="helpLink" ref="helpLink"
...@@ -147,21 +150,34 @@ export default { ...@@ -147,21 +150,34 @@ export default {
<threat-monitoring-filters /> <threat-monitoring-filters />
<waf-loading-skeleton v-if="isLoadingWafStatistics" class="mt-3" /> <threat-monitoring-section
ref="wafSection"
store-namespace="threatMonitoringWaf"
:title="s__('ThreatMonitoring|Web Application Firewall')"
:subtitle="s__('ThreatMonitoring|Requests')"
:anomalous-title="s__('ThreatMonitoring|Anomalous Requests')"
:nominal-title="s__('ThreatMonitoring|Total Requests')"
:y-legend="s__('ThreatMonitoring|Requests')"
:chart-empty-state-text="$options.wafChartEmptyStateDescription"
:chart-empty-state-svg-path="chartEmptyStateSvgPath"
:documentation-path="documentationPath"
/>
<template v-else-if="hasHistory"> <template v-if="glFeatures.networkPolicyUi">
<waf-statistics-summary class="mt-3" /> <hr />
<waf-statistics-history class="mt-3" />
</template>
<gl-empty-state <threat-monitoring-section
v-else ref="networkPolicySection"
ref="chartEmptyState" store-namespace="threatMonitoringNetworkPolicy"
:title="s__('ThreatMonitoring|No traffic to display')" :title="s__('ThreatMonitoring|Container Network Policy')"
:description="$options.chartEmptyStateDescription" :subtitle="s__('ThreatMonitoring|Packet Activity')"
:svg-path="chartEmptyStateSvgPath" :anomalous-title="s__('ThreatMonitoring|Dropped Packets')"
:primary-button-link="documentationPath" :nominal-title="s__('ThreatMonitoring|Total Packets')"
:primary-button-text="__('Learn More')" :y-legend="s__('ThreatMonitoring|Operations Per Second')"
:chart-empty-state-text="$options.networkPolicyChartEmptyStateDescription"
:chart-empty-state-svg-path="chartEmptyStateSvgPath"
:documentation-path="documentationPath"
/> />
</template>
</section> </section>
</template> </template>
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { mapState } from 'vuex';
import { GlResizeObserverDirective } from '@gitlab/ui'; import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { import { COLORS, DATE_FORMATS, TIME } from './constants';
ANOMALOUS_REQUESTS,
COLORS,
DATE_FORMATS,
REQUESTS,
TIME,
TOTAL_REQUESTS,
} from './constants';
export default { export default {
name: 'WafStatisticsHistoryChart', name: 'StatisticsHistoryChart',
components: { components: {
GlAreaChart, GlAreaChart,
}, },
directives: { directives: {
GlResizeObserverDirective, GlResizeObserverDirective,
}, },
props: {
data: {
type: Object,
required: true,
validator: ({ anomalous, nominal, from, to }) =>
Boolean(anomalous?.title && anomalous?.values) &&
Boolean(nominal?.title && nominal?.values) &&
from &&
to,
},
yLegend: {
type: String,
required: true,
},
},
data() { data() {
return { return {
chartInstance: null, chartInstance: null,
...@@ -30,40 +37,45 @@ export default { ...@@ -30,40 +37,45 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('threatMonitoring', ['wafStatistics']),
chartData() { chartData() {
const { anomalous, nominal } = this.wafStatistics.history; const { anomalous, nominal } = this.data;
const anomalousStyle = { color: COLORS.anomalous }; const anomalousStyle = { color: COLORS.anomalous };
const nominalStyle = { color: COLORS.nominal }; const nominalStyle = { color: COLORS.nominal };
return [ return [
{ {
name: ANOMALOUS_REQUESTS, name: anomalous.title,
data: anomalous, data: anomalous.values,
areaStyle: anomalousStyle, areaStyle: anomalousStyle,
lineStyle: anomalousStyle, lineStyle: anomalousStyle,
itemStyle: anomalousStyle, itemStyle: anomalousStyle,
}, },
{ {
name: TOTAL_REQUESTS, name: nominal.title,
data: nominal, data: nominal.values,
areaStyle: nominalStyle, areaStyle: nominalStyle,
lineStyle: nominalStyle, lineStyle: nominalStyle,
itemStyle: nominalStyle, itemStyle: nominalStyle,
}, },
]; ];
}, },
}, chartOptions() {
chartOptions: { const { from, to } = this.data;
return {
xAxis: { xAxis: {
name: TIME, name: TIME,
type: 'time', type: 'time',
axisLabel: { axisLabel: {
formatter: value => dateFormat(value, DATE_FORMATS.defaultDate), formatter: value => dateFormat(value, DATE_FORMATS.defaultDate),
}, },
min: from,
max: to,
}, },
yAxis: { yAxis: {
name: REQUESTS, name: this.yLegend,
},
};
}, },
}, },
methods: { methods: {
...@@ -88,13 +100,13 @@ export default { ...@@ -88,13 +100,13 @@ export default {
<gl-area-chart <gl-area-chart
v-gl-resize-observer-directive="onResize" v-gl-resize-observer-directive="onResize"
:data="chartData" :data="chartData"
:option="$options.chartOptions" :option="chartOptions"
:include-legend-avg-max="false" :include-legend-avg-max="false"
:format-tooltip-text="formatTooltipText" :format-tooltip-text="formatTooltipText"
@created="onChartCreated" @created="onChartCreated"
> >
<template #tooltipTitle> <template #tooltipTitle>
<div>{{ tooltipTitle }} ({{ $options.chartOptions.xAxis.name }})</div> <div>{{ tooltipTitle }} ({{ chartOptions.xAxis.name }})</div>
</template> </template>
<template #tooltipContent> <template #tooltipContent>
......
<script> <script>
import { mapState } from 'vuex';
import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { engineeringNotation } from '@gitlab/ui/src/utils/number_utils'; import { engineeringNotation } from '@gitlab/ui/src/utils/number_utils';
import { ANOMALOUS_REQUESTS, TOTAL_REQUESTS } from './constants';
export default { export default {
name: 'WafStatisticsSummary', name: 'StatisticsSummary',
components: { components: {
GlSingleStat, GlSingleStat,
}, },
props: {
data: {
type: Object,
required: true,
validator: ({ anomalous, nominal }) =>
Boolean(anomalous?.title && anomalous?.value) && Boolean(nominal?.title && nominal?.value),
},
},
computed: { computed: {
...mapState('threatMonitoring', ['wafStatistics']),
statistics() { statistics() {
const { anomalous, nominal } = this.data;
return [ return [
{ {
key: 'anomalousTraffic', key: 'anomalousTraffic',
title: ANOMALOUS_REQUESTS, title: anomalous.title,
value: `${Math.round(this.wafStatistics.anomalousTraffic * 100)}%`, value: `${Math.round(anomalous.value * 100)}%`,
variant: 'warning', variant: 'warning',
}, },
{ {
key: 'totalTraffic', key: 'totalTraffic',
title: TOTAL_REQUESTS, title: nominal.title,
value: engineeringNotation(this.wafStatistics.totalTraffic), value: engineeringNotation(nominal.value),
variant: 'secondary', variant: 'secondary',
}, },
]; ];
......
<script>
import { mapState } from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import LoadingSkeleton from './loading_skeleton.vue';
import StatisticsSummary from './statistics_summary.vue';
import StatisticsHistory from './statistics_history.vue';
export default {
components: {
GlEmptyState,
LoadingSkeleton,
StatisticsSummary,
StatisticsHistory,
},
props: {
storeNamespace: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
subtitle: {
type: String,
required: true,
},
nominalTitle: {
type: String,
required: true,
},
anomalousTitle: {
type: String,
required: true,
},
yLegend: {
type: String,
required: true,
},
chartEmptyStateText: {
type: String,
required: true,
},
chartEmptyStateSvgPath: {
type: String,
required: true,
},
documentationPath: {
type: String,
required: true,
},
},
computed: {
...mapState({
isLoading(state) {
return state[this.storeNamespace].isLoadingStatistics;
},
statistics(state) {
return state[this.storeNamespace].statistics;
},
hasHistory(state, getters) {
return getters[`${this.storeNamespace}/hasHistory`];
},
timeRange(state) {
return state[this.storeNamespace].timeRange;
},
}),
summary() {
const { anomalous, total } = this.statistics;
return {
anomalous: { title: this.anomalousTitle, value: anomalous },
nominal: { title: this.nominalTitle, value: total },
};
},
chart() {
if (!this.hasHistory) return {};
const { anomalous, nominal } = this.statistics.history;
return {
anomalous: { title: this.anomalousTitle, values: anomalous },
nominal: { title: this.nominalTitle, values: nominal },
from: this.timeRange.from,
to: this.timeRange.to,
};
},
},
};
</script>
<template>
<div class="my-3">
<h4 class="h4">{{ title }}</h4>
<h5 class="h5">{{ subtitle }}</h5>
<loading-skeleton v-if="isLoading" class="mt-3" />
<template v-else-if="hasHistory">
<statistics-summary class="mt-3" :data="summary" />
<statistics-history class="mt-3" :data="chart" :y-legend="yLegend" />
</template>
<gl-empty-state
v-else
ref="chartEmptyState"
:title="s__('ThreatMonitoring|No traffic to display')"
:description="chartEmptyStateText"
:svg-path="chartEmptyStateSvgPath"
:primary-button-link="documentationPath"
:primary-button-text="__('Learn More')"
/>
</div>
</template>
...@@ -27,7 +27,7 @@ export const TIME_WINDOWS = { ...@@ -27,7 +27,7 @@ export const TIME_WINDOWS = {
sevenDays: { sevenDays: {
name: __('7 days'), name: __('7 days'),
durationInMilliseconds: 7 * 24 * 60 * 60 * 1000, durationInMilliseconds: 7 * 24 * 60 * 60 * 1000,
interval: INTERVALS.hour, interval: INTERVALS.day,
}, },
thirtyDays: { thirtyDays: {
name: __('30 days'), name: __('30 days'),
......
...@@ -7,6 +7,7 @@ export default () => { ...@@ -7,6 +7,7 @@ export default () => {
const el = document.querySelector('#js-threat-monitoring-app'); const el = document.querySelector('#js-threat-monitoring-app');
const { const {
wafStatisticsEndpoint, wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
environmentsEndpoint, environmentsEndpoint,
chartEmptyStateSvgPath, chartEmptyStateSvgPath,
emptyStateSvgPath, emptyStateSvgPath,
...@@ -20,6 +21,7 @@ export default () => { ...@@ -20,6 +21,7 @@ export default () => {
const store = createStore(); const store = createStore();
store.dispatch('threatMonitoring/setEndpoints', { store.dispatch('threatMonitoring/setEndpoints', {
wafStatisticsEndpoint, wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
environmentsEndpoint, environmentsEndpoint,
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import threatMonitoring from './modules/threat_monitoring'; import threatMonitoring from './modules/threat_monitoring';
import threatMonitoringStatistics from './modules/threat_monitoring_statistics';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -8,5 +10,25 @@ export default () => ...@@ -8,5 +10,25 @@ export default () =>
new Vuex.Store({ new Vuex.Store({
modules: { modules: {
threatMonitoring: threatMonitoring(), threatMonitoring: threatMonitoring(),
threatMonitoringWaf: threatMonitoringStatistics(payload => {
const { totalTraffic, anomalousTraffic, history } = convertObjectPropsToCamelCase(payload);
return { total: totalTraffic, anomalous: anomalousTraffic, history };
}),
threatMonitoringNetworkPolicy: threatMonitoringStatistics(payload => {
const {
opsRate,
opsTotal: { total, drops },
} = convertObjectPropsToCamelCase(payload);
const formatFunc = ([timestamp, val]) => [new Date(timestamp * 1000), val];
return {
total,
anomalous: drops / total,
history: {
nominal: opsRate.total.map(formatFunc),
anomalous: opsRate.drops.map(formatFunc),
},
};
}),
}, },
}); });
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import httpStatusCodes from '~/lib/utils/http_status';
import createFlash from '~/flash'; import createFlash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import createState from './state';
import { getTimeWindowParams } from './utils';
export const setEndpoints = ({ commit }, endpoints) => commit(types.SET_ENDPOINTS, endpoints); export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINT, endpoints.environmentsEndpoint);
commit(`threatMonitoringWaf/${types.SET_ENDPOINT}`, endpoints.wafStatisticsEndpoint, {
root: true,
});
commit(
`threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
endpoints.networkPolicyStatisticsEndpoint,
{ root: true },
);
};
export const requestEnvironments = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS); export const requestEnvironments = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS);
export const receiveEnvironmentsSuccess = ({ commit }, environments) => export const receiveEnvironmentsSuccess = ({ commit }, environments) =>
...@@ -50,45 +56,18 @@ export const fetchEnvironments = ({ state, dispatch }) => { ...@@ -50,45 +56,18 @@ export const fetchEnvironments = ({ state, dispatch }) => {
export const setCurrentEnvironmentId = ({ commit, dispatch }, environmentId) => { export const setCurrentEnvironmentId = ({ commit, dispatch }, environmentId) => {
commit(types.SET_CURRENT_ENVIRONMENT_ID, environmentId); commit(types.SET_CURRENT_ENVIRONMENT_ID, environmentId);
return dispatch('fetchWafStatistics'); dispatch(`threatMonitoringWaf/fetchStatistics`, null, { root: true });
if (window.gon.features?.networkPolicyUi) {
dispatch(`threatMonitoringNetworkPolicy/fetchStatistics`, null, { root: true });
}
}; };
export const setCurrentTimeWindow = ({ commit, dispatch }, timeWindow) => { export const setCurrentTimeWindow = ({ commit, dispatch }, timeWindow) => {
commit(types.SET_CURRENT_TIME_WINDOW, timeWindow); commit(types.SET_CURRENT_TIME_WINDOW, timeWindow);
return dispatch('fetchWafStatistics'); dispatch(`threatMonitoringWaf/fetchStatistics`, null, { root: true });
};
export const requestWafStatistics = ({ commit }) => commit(types.REQUEST_WAF_STATISTICS);
export const receiveWafStatisticsSuccess = ({ commit }, statistics) =>
commit(types.RECEIVE_WAF_STATISTICS_SUCCESS, statistics);
export const receiveWafStatisticsError = ({ commit }) => {
commit(types.RECEIVE_WAF_STATISTICS_ERROR);
createFlash(s__('ThreatMonitoring|Something went wrong, unable to fetch WAF statistics'));
};
export const fetchWafStatistics = ({ state, dispatch }) => { if (window.gon.features?.networkPolicyUi) {
if (!state.wafStatisticsEndpoint) { dispatch(`threatMonitoringNetworkPolicy/fetchStatistics`, null, { root: true });
return dispatch('receiveWafStatisticsError');
} }
dispatch('requestWafStatistics');
return pollUntilComplete(state.wafStatisticsEndpoint, {
params: {
environment_id: state.currentEnvironmentId,
...getTimeWindowParams(state.currentTimeWindow, Date.now()),
},
})
.then(({ data }) => dispatch('receiveWafStatisticsSuccess', data))
.catch(error => {
// A NOT_FOUND resonse from the endpoint means that there is no data for
// the given parameters. There are various reasons *why* there could be
// no data, but we can't distinguish between them, yet. So, just render
// no data.
if (error.response.status === httpStatusCodes.NOT_FOUND) {
dispatch('receiveWafStatisticsSuccess', createState().wafStatistics);
} else {
dispatch('receiveWafStatisticsError');
}
});
}; };
import { INVALID_CURRENT_ENVIRONMENT_NAME } from '../../../constants'; import { INVALID_CURRENT_ENVIRONMENT_NAME } from '../../../constants';
import { getTimeWindowConfig } from './utils'; import { getTimeWindowConfig } from '../../utils';
export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => { export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => {
const environment = environments.find(({ id }) => id === currentEnvironmentId); const environment = environments.find(({ id }) => id === currentEnvironmentId);
...@@ -8,6 +8,3 @@ export const currentEnvironmentName = ({ currentEnvironmentId, environments }) = ...@@ -8,6 +8,3 @@ export const currentEnvironmentName = ({ currentEnvironmentId, environments }) =
export const currentTimeWindowName = ({ currentTimeWindow }) => export const currentTimeWindowName = ({ currentTimeWindow }) =>
getTimeWindowConfig(currentTimeWindow).name; getTimeWindowConfig(currentTimeWindow).name;
export const hasHistory = ({ wafStatistics }) =>
Boolean(wafStatistics.history.nominal.length || wafStatistics.history.anomalous.length);
export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS'; export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS'; export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
...@@ -6,7 +6,3 @@ export const RECEIVE_ENVIRONMENTS_ERROR = 'RECEIVE_ENVIRONMENTS_ERROR'; ...@@ -6,7 +6,3 @@ export const RECEIVE_ENVIRONMENTS_ERROR = 'RECEIVE_ENVIRONMENTS_ERROR';
export const SET_CURRENT_ENVIRONMENT_ID = 'SET_CURRENT_ENVIRONMENT_ID'; export const SET_CURRENT_ENVIRONMENT_ID = 'SET_CURRENT_ENVIRONMENT_ID';
export const SET_CURRENT_TIME_WINDOW = 'SET_CURRENT_TIME_WINDOW'; export const SET_CURRENT_TIME_WINDOW = 'SET_CURRENT_TIME_WINDOW';
export const REQUEST_WAF_STATISTICS = 'REQUEST_WAF_STATISTICS';
export const RECEIVE_WAF_STATISTICS_SUCCESS = 'RECEIVE_WAF_STATISTICS_SUCCESS';
export const RECEIVE_WAF_STATISTICS_ERROR = 'RECEIVE_WAF_STATISTICS_ERROR';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_ENDPOINTS](state, { wafStatisticsEndpoint, environmentsEndpoint }) { [types.SET_ENDPOINT](state, endpoint) {
state.wafStatisticsEndpoint = wafStatisticsEndpoint; state.environmentsEndpoint = endpoint;
state.environmentsEndpoint = environmentsEndpoint;
}, },
[types.REQUEST_ENVIRONMENTS](state) { [types.REQUEST_ENVIRONMENTS](state) {
state.isLoadingEnvironments = true; state.isLoadingEnvironments = true;
...@@ -25,17 +23,4 @@ export default { ...@@ -25,17 +23,4 @@ export default {
[types.SET_CURRENT_TIME_WINDOW](state, payload) { [types.SET_CURRENT_TIME_WINDOW](state, payload) {
state.currentTimeWindow = payload; state.currentTimeWindow = payload;
}, },
[types.REQUEST_WAF_STATISTICS](state) {
state.isLoadingWafStatistics = true;
state.errorLoadingWafStatistics = false;
},
[types.RECEIVE_WAF_STATISTICS_SUCCESS](state, payload) {
state.wafStatistics = convertObjectPropsToCamelCase(payload);
state.isLoadingWafStatistics = false;
state.errorLoadingWafStatistics = false;
},
[types.RECEIVE_WAF_STATISTICS_ERROR](state) {
state.isLoadingWafStatistics = false;
state.errorLoadingWafStatistics = true;
},
}; };
...@@ -7,15 +7,4 @@ export default () => ({ ...@@ -7,15 +7,4 @@ export default () => ({
errorLoadingEnvironments: false, errorLoadingEnvironments: false,
currentEnvironmentId: -1, currentEnvironmentId: -1,
currentTimeWindow: DEFAULT_TIME_WINDOW, currentTimeWindow: DEFAULT_TIME_WINDOW,
wafStatisticsEndpoint: '',
wafStatistics: {
totalTraffic: 0,
anomalousTraffic: 0,
history: {
nominal: [],
anomalous: [],
},
},
isLoadingWafStatistics: false,
errorLoadingWafStatistics: false,
}); });
import { s__ } from '~/locale';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import httpStatusCodes from '~/lib/utils/http_status';
import createFlash from '~/flash';
import * as types from './mutation_types';
import createState from './state';
import { getTimeWindowParams } from '../../utils';
export const requestStatistics = ({ commit }, timeWindowParams) => {
commit(types.REQUEST_STATISTICS, timeWindowParams);
};
export const receiveStatisticsSuccess = ({ commit }, statistics) =>
commit(types.RECEIVE_STATISTICS_SUCCESS, statistics);
export const receiveStatisticsError = ({ commit }) => {
commit(types.RECEIVE_STATISTICS_ERROR);
createFlash(s__('ThreatMonitoring|Something went wrong, unable to fetch statistics'));
};
export const fetchStatistics = ({ state, dispatch, rootState }) => {
const { currentEnvironmentId, currentTimeWindow } = rootState.threatMonitoring;
if (!state.statisticsEndpoint) {
return dispatch('receiveStatisticsError');
}
const timeWindowParams = getTimeWindowParams(currentTimeWindow, Date.now());
dispatch('requestStatistics', timeWindowParams);
return pollUntilComplete(state.statisticsEndpoint, {
params: {
environment_id: currentEnvironmentId,
...timeWindowParams,
},
})
.then(({ data }) => dispatch('receiveStatisticsSuccess', data))
.catch(error => {
// A NOT_FOUND response from the endpoint means that there is no data for
// the given parameters. There are various reasons *why* there could be
// no data, but we can't distinguish between them, yet. So, just render
// no data.
if (error.response.status === httpStatusCodes.NOT_FOUND) {
dispatch('receiveStatisticsSuccess', createState().statistics);
} else {
dispatch('receiveStatisticsError');
}
});
};
// eslint-disable-next-line import/prefer-default-export
export const hasHistory = ({ statistics: { history } }) =>
Boolean(history.nominal.length || history.anomalous.length);
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default transformFunc => ({
namespaced: true,
actions,
getters,
mutations: mutations(transformFunc),
state,
});
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_STATISTICS = 'REQUEST_STATISTICS';
export const RECEIVE_STATISTICS_SUCCESS = 'RECEIVE_STATISTICS_SUCCESS';
export const RECEIVE_STATISTICS_ERROR = 'RECEIVE_STATISTICS_ERROR';
import * as types from './mutation_types';
export default transformFunc => ({
[types.SET_ENDPOINT](state, endpoint) {
state.statisticsEndpoint = endpoint;
},
[types.REQUEST_STATISTICS](state, timeRange) {
state.isLoadingStatistics = true;
state.errorLoadingStatistics = false;
state.timeRange = timeRange;
},
[types.RECEIVE_STATISTICS_SUCCESS](state, payload) {
state.statistics = transformFunc(payload);
state.isLoadingStatistics = false;
state.errorLoadingStatistics = false;
},
[types.RECEIVE_STATISTICS_ERROR](state) {
state.isLoadingStatistics = false;
state.errorLoadingStatistics = true;
},
});
export default () => ({
statisticsEndpoint: '',
statistics: {
total: 0,
anomalous: 0,
history: {
nominal: [],
anomalous: [],
},
},
timeRange: {
from: null,
to: null,
},
isLoadingStatistics: false,
errorLoadingStatistics: false,
});
...@@ -3,5 +3,9 @@ ...@@ -3,5 +3,9 @@
module Projects module Projects
class ThreatMonitoringController < Projects::ApplicationController class ThreatMonitoringController < Projects::ApplicationController
before_action :authorize_read_threat_monitoring! before_action :authorize_read_threat_monitoring!
before_action only: [:show] do
push_frontend_feature_flag(:network_policy_ui)
end
end end
end end
...@@ -7,9 +7,10 @@ ...@@ -7,9 +7,10 @@
chart_empty_state_svg_path: image_path('illustrations/chart-empty-state.svg'), chart_empty_state_svg_path: image_path('illustrations/chart-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
waf_statistics_endpoint: summary_project_security_waf_anomalies_path(@project, format: :json), waf_statistics_endpoint: summary_project_security_waf_anomalies_path(@project, format: :json),
network_policy_statistics_endpoint: summary_project_security_network_policies_path(@project, format: :json),
environments_endpoint: project_environments_path(@project), environments_endpoint: project_environments_path(@project),
default_environment_id: default_environment_id, default_environment_id: default_environment_id,
user_callouts_path: user_callouts_path, user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::THREAT_MONITORING_INFO, user_callout_id: UserCalloutsHelper::THREAT_MONITORING_INFO,
show_user_callout: show_threat_monitoring_info?.to_s, show_user_callout: false,
} } } }
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThreatMonitoringApp component given there is a default environment with data given the networkPolicyUi feature flag is enabled renders the network policy section 1`] = `
<threat-monitoring-section-stub
anomaloustitle="Dropped Packets"
chartemptystatesvgpath="/chart-svgs"
chartemptystatetext="While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the Network Policies correctly."
documentationpath="/docs"
nominaltitle="Total Packets"
storenamespace="threatMonitoringNetworkPolicy"
subtitle="Packet Activity"
title="Container Network Policy"
ylegend="Operations Per Second"
/>
`;
exports[`ThreatMonitoringApp component given there is a default environment with data renders the waf section 1`] = `
<threat-monitoring-section-stub
anomaloustitle="Anomalous Requests"
chartemptystatesvgpath="/chart-svgs"
chartemptystatetext="While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the WAF correctly."
documentationpath="/docs"
nominaltitle="Total Requests"
storenamespace="threatMonitoringWaf"
subtitle="Requests"
title="Web Application Firewall"
ylegend="Requests"
/>
`;
exports[`ThreatMonitoringApp component given there is a default environment with data shows the alert 1`] = ` exports[`ThreatMonitoringApp component given there is a default environment with data shows the alert 1`] = `
<gl-alert-stub <gl-alert-stub
class="my-3" class="my-3"
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WafStatisticsHistory component chart tooltip renders the title and series data correctly 1`] = ` exports[`StatisticsHistory component chart tooltip renders the title and series data correctly 1`] = `
<div <div
data="[object Object],[object Object]" data="[object Object],[object Object]"
option="[object Object]" option="[object Object]"
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WafStatisticsSummary component renders the anomalous traffic percentage 1`] = ` exports[`StatisticsSummary component renders the anomalous traffic percentage 1`] = `
<gl-single-stat-stub <gl-single-stat-stub
class="col-sm-6 col-md-4 col-lg-3" class="col-sm-6 col-md-4 col-lg-3"
title="Anomalous Requests" title="Anomalous"
value="3%" value="20%"
variant="warning" variant="warning"
/> />
`; `;
exports[`WafStatisticsSummary component renders the nominal traffic count 1`] = ` exports[`StatisticsSummary component renders the nominal traffic count 1`] = `
<gl-single-stat-stub <gl-single-stat-stub
class="col-sm-6 col-md-4 col-lg-3" class="col-sm-6 col-md-4 col-lg-3"
title="Total Requests" title="Total"
value="2.7k" value="100"
variant="secondary" variant="secondary"
/> />
`; `;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThreatMonitoringSection component given the statistics are loading shows the loading skeleton 1`] = `
<loading-skeleton-stub
class="mt-3"
/>
`;
exports[`ThreatMonitoringSection component given there is a default environment with no data to display shows the chart empty state 1`] = `
<gl-empty-state-stub
description="Empty Text"
primarybuttonlink="documentation_path"
primarybuttontext="Learn More"
svgpath="svg_path"
title="No traffic to display"
/>
`;
...@@ -6,10 +6,6 @@ import { TEST_HOST } from 'helpers/test_constants'; ...@@ -6,10 +6,6 @@ import { TEST_HOST } from 'helpers/test_constants';
import createStore from 'ee/threat_monitoring/store'; import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue'; import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue'; import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
import WafLoadingSkeleton from 'ee/threat_monitoring/components/waf_loading_skeleton.vue';
import WafStatisticsHistory from 'ee/threat_monitoring/components/waf_statistics_history.vue';
import WafStatisticsSummary from 'ee/threat_monitoring/components/waf_statistics_summary.vue';
import { mockWafStatisticsResponse } from '../mock_data';
const defaultEnvironmentId = 3; const defaultEnvironmentId = 3;
const documentationPath = '/docs'; const documentationPath = '/docs';
...@@ -17,6 +13,7 @@ const chartEmptyStateSvgPath = '/chart-svgs'; ...@@ -17,6 +13,7 @@ const chartEmptyStateSvgPath = '/chart-svgs';
const emptyStateSvgPath = '/svgs'; const emptyStateSvgPath = '/svgs';
const environmentsEndpoint = `${TEST_HOST}/environments`; const environmentsEndpoint = `${TEST_HOST}/environments`;
const wafStatisticsEndpoint = `${TEST_HOST}/waf`; const wafStatisticsEndpoint = `${TEST_HOST}/waf`;
const networkPolicyStatisticsEndpoint = `${TEST_HOST}/network_policy`;
const userCalloutId = 'threat_monitoring_info'; const userCalloutId = 'threat_monitoring_info';
const userCalloutsPath = `${TEST_HOST}/user_callouts`; const userCalloutsPath = `${TEST_HOST}/user_callouts`;
...@@ -24,11 +21,12 @@ describe('ThreatMonitoringApp component', () => { ...@@ -24,11 +21,12 @@ describe('ThreatMonitoringApp component', () => {
let store; let store;
let wrapper; let wrapper;
const factory = ({ propsData, state } = {}) => { const factory = ({ propsData, state, options } = {}) => {
store = createStore(); store = createStore();
Object.assign(store.state.threatMonitoring, { Object.assign(store.state.threatMonitoring, {
environmentsEndpoint, environmentsEndpoint,
wafStatisticsEndpoint, wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
...state, ...state,
}); });
...@@ -46,16 +44,15 @@ describe('ThreatMonitoringApp component', () => { ...@@ -46,16 +44,15 @@ describe('ThreatMonitoringApp component', () => {
...propsData, ...propsData,
}, },
store, store,
...options,
}); });
}; };
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const findFilters = () => wrapper.find(ThreatMonitoringFilters); const findFilters = () => wrapper.find(ThreatMonitoringFilters);
const findWafLoadingSkeleton = () => wrapper.find(WafLoadingSkeleton); const findWafSection = () => wrapper.find({ ref: 'wafSection' });
const findWafStatisticsHistory = () => wrapper.find(WafStatisticsHistory); const findNetworkPolicySection = () => wrapper.find({ ref: 'networkPolicySection' });
const findWafStatisticsSummary = () => wrapper.find(WafStatisticsSummary);
const findEmptyState = () => wrapper.find({ ref: 'emptyState' }); const findEmptyState = () => wrapper.find({ ref: 'emptyState' });
const findChartEmptyState = () => wrapper.find({ ref: 'chartEmptyState' });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -89,11 +86,7 @@ describe('ThreatMonitoringApp component', () => { ...@@ -89,11 +86,7 @@ describe('ThreatMonitoringApp component', () => {
describe('given there is a default environment with data', () => { describe('given there is a default environment with data', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory();
state: {
wafStatistics: mockWafStatisticsResponse,
},
});
}); });
it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => { it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
...@@ -111,13 +104,30 @@ describe('ThreatMonitoringApp component', () => { ...@@ -111,13 +104,30 @@ describe('ThreatMonitoringApp component', () => {
expect(findFilters().exists()).toBe(true); expect(findFilters().exists()).toBe(true);
}); });
it('shows the summary and history statistics', () => { it('renders the waf section', () => {
expect(findWafStatisticsSummary().exists()).toBe(true); expect(findWafSection().element).toMatchSnapshot();
expect(findWafStatisticsHistory().exists()).toBe(true); });
it('does not render the network policy section', () => {
expect(findNetworkPolicySection().exists()).toBe(false);
});
describe('given the networkPolicyUi feature flag is enabled', () => {
beforeEach(() => {
factory({
options: {
provide: {
glFeatures: {
networkPolicyUi: true,
},
},
},
});
}); });
it('does not show the loading skeleton', () => { it('renders the network policy section', () => {
expect(findWafLoadingSkeleton().exists()).toBe(false); expect(findNetworkPolicySection().element).toMatchSnapshot();
});
}); });
describe('dismissing the alert', () => { describe('dismissing the alert', () => {
...@@ -157,39 +167,5 @@ describe('ThreatMonitoringApp component', () => { ...@@ -157,39 +167,5 @@ describe('ThreatMonitoringApp component', () => {
it('does not render the alert', () => { it('does not render the alert', () => {
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
}); });
describe('given the statistics are loading', () => {
beforeEach(() => {
store.state.threatMonitoring.isLoadingWafStatistics = true;
});
it('does not show the summary or history statistics', () => {
expect(findWafStatisticsSummary().exists()).toBe(false);
expect(findWafStatisticsHistory().exists()).toBe(false);
});
it('displays the loading skeleton', () => {
expect(findWafLoadingSkeleton().exists()).toBe(true);
});
});
});
describe('given there is a default environment with no data to display', () => {
beforeEach(() => {
factory();
});
it('shows the filter bar', () => {
expect(findFilters().exists()).toBe(true);
});
it('does not show the summary or history statistics', () => {
expect(findWafStatisticsSummary().exists()).toBe(false);
expect(findWafStatisticsHistory().exists()).toBe(false);
});
it('shows the chart empty state', () => {
expect(findChartEmptyState().exists()).toBe(true);
});
}); });
}); });
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { GlAreaChart } from '@gitlab/ui/dist/charts';
import createStore from 'ee/threat_monitoring/store'; import StatisticsHistory from 'ee/threat_monitoring/components/statistics_history.vue';
import WafStatisticsHistory from 'ee/threat_monitoring/components/waf_statistics_history.vue';
import { TOTAL_REQUESTS, ANOMALOUS_REQUESTS } from 'ee/threat_monitoring/components/constants'; import { TOTAL_REQUESTS, ANOMALOUS_REQUESTS } from 'ee/threat_monitoring/components/constants';
import { mockWafStatisticsResponse } from '../mock_data'; import { mockNominalHistory, mockAnomalousHistory } from '../mock_data';
let resizeCallback = null; let resizeCallback = null;
const MockResizeObserverDirective = { const MockResizeObserverDirective = {
...@@ -24,17 +23,21 @@ const MockResizeObserverDirective = { ...@@ -24,17 +23,21 @@ const MockResizeObserverDirective = {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.directive('gl-resize-observer-directive', MockResizeObserverDirective); localVue.directive('gl-resize-observer-directive', MockResizeObserverDirective);
describe('WafStatisticsHistory component', () => { describe('StatisticsHistory component', () => {
let store;
let wrapper; let wrapper;
const factory = ({ state, options } = {}) => { const factory = ({ options } = {}) => {
store = createStore(); wrapper = shallowMount(StatisticsHistory, {
Object.assign(store.state.threatMonitoring, state);
wrapper = shallowMount(WafStatisticsHistory, {
localVue, localVue,
store, propsData: {
data: {
anomalous: { title: 'Anomoulous', values: mockAnomalousHistory },
nominal: { title: 'Total', values: mockNominalHistory },
from: 'foo',
to: 'bar',
},
yLegend: 'Requests',
},
...options, ...options,
}); });
}; };
...@@ -47,18 +50,29 @@ describe('WafStatisticsHistory component', () => { ...@@ -47,18 +50,29 @@ describe('WafStatisticsHistory component', () => {
describe('the data passed to the chart', () => { describe('the data passed to the chart', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory();
state: {
wafStatistics: {
history: mockWafStatisticsResponse.history,
},
},
});
}); });
it('is structured correctly', () => { it('is structured correctly', () => {
const { nominal, anomalous } = mockWafStatisticsResponse.history; expect(findChart().props('data')).toMatchObject([
expect(findChart().props('data')).toMatchObject([{ data: anomalous }, { data: nominal }]); { data: mockAnomalousHistory },
{ data: mockNominalHistory },
]);
});
});
describe('the options passed to the chart', () => {
beforeEach(() => {
factory();
});
it('sets the xAxis range', () => {
expect(findChart().props('option')).toMatchObject({
xAxis: {
min: 'foo',
max: 'bar',
},
});
}); });
}); });
...@@ -96,17 +110,15 @@ describe('WafStatisticsHistory component', () => { ...@@ -96,17 +110,15 @@ describe('WafStatisticsHistory component', () => {
describe('chart tooltip', () => { describe('chart tooltip', () => {
beforeEach(() => { beforeEach(() => {
const mockTotalSeriesDatum = mockWafStatisticsResponse.history.nominal[0];
const mockAnomalousSeriesDatum = mockWafStatisticsResponse.history.anomalous[0];
const mockParams = { const mockParams = {
seriesData: [ seriesData: [
{ {
seriesName: ANOMALOUS_REQUESTS, seriesName: ANOMALOUS_REQUESTS,
value: mockAnomalousSeriesDatum, value: mockAnomalousHistory[0],
}, },
{ {
seriesName: TOTAL_REQUESTS, seriesName: TOTAL_REQUESTS,
value: mockTotalSeriesDatum, value: mockNominalHistory[0],
}, },
], ],
}; };
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createStore from 'ee/threat_monitoring/store'; import StatisticsSummary from 'ee/threat_monitoring/components/statistics_summary.vue';
import WafStatisticsSummary from 'ee/threat_monitoring/components/waf_statistics_summary.vue';
import { mockWafStatisticsResponse } from '../mock_data';
describe('WafStatisticsSummary component', () => { describe('StatisticsSummary component', () => {
let store;
let wrapper; let wrapper;
const factory = state => { const factory = options => {
store = createStore(); wrapper = shallowMount(StatisticsSummary, {
Object.assign(store.state.threatMonitoring, state); ...options,
wrapper = shallowMount(WafStatisticsSummary, {
store,
}); });
}; };
...@@ -22,9 +16,11 @@ describe('WafStatisticsSummary component', () => { ...@@ -22,9 +16,11 @@ describe('WafStatisticsSummary component', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
wafStatistics: { propsData: {
totalTraffic: mockWafStatisticsResponse.total_traffic, data: {
anomalousTraffic: mockWafStatisticsResponse.anomalous_traffic, anomalous: { title: 'Anomalous', value: 0.2 },
nominal: { title: 'Total', value: 100 },
},
}, },
}); });
}); });
......
import { shallowMount } from '@vue/test-utils';
import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringSection from 'ee/threat_monitoring/components/threat_monitoring_section.vue';
import LoadingSkeleton from 'ee/threat_monitoring/components/loading_skeleton.vue';
import StatisticsHistory from 'ee/threat_monitoring/components/statistics_history.vue';
import StatisticsSummary from 'ee/threat_monitoring/components/statistics_summary.vue';
import { mockNominalHistory, mockAnomalousHistory } from '../mock_data';
describe('ThreatMonitoringSection component', () => {
let store;
let wrapper;
const timeRange = {
from: new Date(Date.UTC(2020, 2, 6)).toISOString(),
to: new Date(Date.UTC(2020, 2, 13)).toISOString(),
};
const factory = ({ propsData, state } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoringWaf, {
isLoadingStatistics: false,
statistics: {
total: 100,
anomalous: 0.2,
history: {
nominal: mockNominalHistory,
anomalous: mockAnomalousHistory,
},
},
timeRange,
...state,
});
wrapper = shallowMount(ThreatMonitoringSection, {
propsData: {
storeNamespace: 'threatMonitoringWaf',
title: 'Web Application Firewall',
subtitle: 'Requests',
nominalTitle: 'Total Requests',
anomalousTitle: 'Anomalous Requests',
yLegend: 'Requests',
chartEmptyStateText: 'Empty Text',
chartEmptyStateSvgPath: 'svg_path',
documentationPath: 'documentation_path',
...propsData,
},
store,
});
};
const findLoadingSkeleton = () => wrapper.find(LoadingSkeleton);
const findStatisticsHistory = () => wrapper.find(StatisticsHistory);
const findStatisticsSummary = () => wrapper.find(StatisticsSummary);
const findChartEmptyState = () => wrapper.find({ ref: 'chartEmptyState' });
beforeEach(() => {
factory({});
});
afterEach(() => {
wrapper.destroy();
});
it('does not show the loading skeleton', () => {
expect(findLoadingSkeleton().exists()).toBe(false);
});
it('sets data to the summary', () => {
const summary = findStatisticsSummary();
expect(summary.exists()).toBe(true);
expect(summary.props('data')).toStrictEqual({
anomalous: {
title: 'Anomalous Requests',
value: 0.2,
},
nominal: {
title: 'Total Requests',
value: 100,
},
});
});
it('sets data to the chart', () => {
const chart = findStatisticsHistory();
expect(chart.exists()).toBe(true);
expect(chart.props('data')).toStrictEqual({
anomalous: { title: 'Anomalous Requests', values: mockAnomalousHistory },
nominal: { title: 'Total Requests', values: mockNominalHistory },
...timeRange,
});
expect(chart.props('yLegend')).toEqual('Requests');
});
it('does not show the chart empty state', () => {
expect(findChartEmptyState().exists()).toBe(false);
});
describe('given the statistics are loading', () => {
beforeEach(() => {
factory({
state: { isLoadingStatistics: true },
});
});
it('shows the loading skeleton', () => {
expect(findLoadingSkeleton().element).toMatchSnapshot();
});
it('does not show the summary or history statistics', () => {
expect(findStatisticsSummary().exists()).toBe(false);
expect(findStatisticsHistory().exists()).toBe(false);
});
it('does not show the chart empty state', () => {
expect(findChartEmptyState().exists()).toBe(false);
});
});
describe('given there is a default environment with no data to display', () => {
beforeEach(() => {
factory({
state: {
statistics: {
total: 100,
anoumalous: 0.2,
history: { nominal: [], anomalous: [] },
},
},
});
});
it('does not show the loading skeleton', () => {
expect(findLoadingSkeleton().exists()).toBe(false);
});
it('does not show the summary or history statistics', () => {
expect(findStatisticsSummary().exists()).toBe(false);
expect(findStatisticsHistory().exists()).toBe(false);
});
it('shows the chart empty state', () => {
expect(findChartEmptyState().element).toMatchSnapshot();
});
});
});
...@@ -15,11 +15,43 @@ export const mockEnvironmentsResponse = { ...@@ -15,11 +15,43 @@ export const mockEnvironmentsResponse = {
stopped_count: 5, stopped_count: 5,
}; };
export const mockNominalHistory = [
['2019-12-04T00:00:00.000Z', 56],
['2019-12-05T00:00:00.000Z', 2647],
];
export const mockAnomalousHistory = [
['2019-12-04T00:00:00.000Z', 1],
['2019-12-05T00:00:00.000Z', 83],
];
export const mockWafStatisticsResponse = { export const mockWafStatisticsResponse = {
total_traffic: 2703, total_traffic: 2703,
anomalous_traffic: 0.03, anomalous_traffic: 0.03,
history: { history: {
nominal: [['2019-12-04T00:00:00.000Z', 56], ['2019-12-05T00:00:00.000Z', 2647]], nominal: mockNominalHistory,
anomalous: [['2019-12-04T00:00:00.000Z', 1], ['2019-12-05T00:00:00.000Z', 83]], anomalous: mockAnomalousHistory,
},
};
export const mockNetworkPolicyStatisticsResponse = {
ops_total: {
total: 2703,
drops: 84,
},
ops_rate: {
total: [[1575417600, 56], [1575504000, 2647]],
drops: [[1575417600, 1], [1575504000, 83]],
},
};
export const formattedMockNetworkPolicyStatisticsResponse = {
opsRate: {
drops: [[new Date('2019-12-04T00:00:00.000Z'), 1], [new Date('2019-12-05T00:00:00.000Z'), 83]],
total: [
[new Date('2019-12-04T00:00:00.000Z'), 56],
[new Date('2019-12-05T00:00:00.000Z'), 2647],
],
}, },
opsTotal: { drops: 84, total: 2703 },
}; };
...@@ -8,12 +8,23 @@ import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/a ...@@ -8,12 +8,23 @@ import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/a
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types'; import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types';
import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state'; import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
import { mockEnvironmentsResponse, mockWafStatisticsResponse } from '../../../mock_data'; import { mockEnvironmentsResponse } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn()); jest.mock('~/flash', () => jest.fn());
const environmentsEndpoint = 'environmentsEndpoint'; const environmentsEndpoint = 'environmentsEndpoint';
const wafStatisticsEndpoint = 'wafStatisticsEndpoint'; const wafStatisticsEndpoint = 'wafStatisticsEndpoint';
const networkPolicyStatisticsEndpoint = 'networkPolicyStatisticsEndpoint';
const stubFeatureFlags = features => {
beforeEach(() => {
window.gon.features = features;
});
afterEach(() => {
delete window.gon.features;
});
};
describe('Threat Monitoring actions', () => { describe('Threat Monitoring actions', () => {
let state; let state;
...@@ -27,15 +38,23 @@ describe('Threat Monitoring actions', () => { ...@@ -27,15 +38,23 @@ describe('Threat Monitoring actions', () => {
}); });
describe('setEndpoints', () => { describe('setEndpoints', () => {
it('commits the SET_ENDPOINTS mutation', () => it('commits the SET_ENDPOINT mutation', () =>
testAction( testAction(
actions.setEndpoints, actions.setEndpoints,
{ environmentsEndpoint, wafStatisticsEndpoint }, { environmentsEndpoint, wafStatisticsEndpoint, networkPolicyStatisticsEndpoint },
state, state,
[ [
{ {
type: types.SET_ENDPOINTS, type: types.SET_ENDPOINT,
payload: { environmentsEndpoint, wafStatisticsEndpoint }, payload: environmentsEndpoint,
},
{
type: `threatMonitoringWaf/${types.SET_ENDPOINT}`,
payload: wafStatisticsEndpoint,
},
{
type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
payload: networkPolicyStatisticsEndpoint,
}, },
], ],
[], [],
...@@ -189,179 +208,58 @@ describe('Threat Monitoring actions', () => { ...@@ -189,179 +208,58 @@ describe('Threat Monitoring actions', () => {
describe('setCurrentEnvironmentId', () => { describe('setCurrentEnvironmentId', () => {
const environmentId = 1; const environmentId = 1;
it('commits the SET_CURRENT_ENVIRONMENT_ID mutation and dispatches fetchWafStatistics', () => it('commits the SET_CURRENT_ENVIRONMENT_ID mutation and dispatches WAF fetch action', () =>
testAction( testAction(
actions.setCurrentEnvironmentId, actions.setCurrentEnvironmentId,
environmentId, environmentId,
state, state,
[{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }], [{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }],
[{ type: 'fetchWafStatistics' }], [{ type: 'threatMonitoringWaf/fetchStatistics', payload: null }],
)); ));
});
describe('setCurrentTimeWindow', () => {
const timeWindow = 'foo';
it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches fetchWafStatistics', () => describe('given the networkPolicyUi feature flag is enabled', () => {
testAction( stubFeatureFlags({ networkPolicyUi: true });
actions.setCurrentTimeWindow,
timeWindow,
state,
[{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[{ type: 'fetchWafStatistics' }],
));
});
describe('requestWafStatistics', () => { it('commits the SET_CURRENT_ENVIRONMENT_ID mutation and dispatches WAF and Network Policy fetch actions', () =>
it('commits the REQUEST_WAF_STATISTICS mutation', () =>
testAction( testAction(
actions.requestWafStatistics, actions.setCurrentEnvironmentId,
undefined, environmentId,
state,
[
{
type: types.REQUEST_WAF_STATISTICS,
},
],
[],
));
});
describe('receiveWafStatisticsSuccess', () => {
it('commits the RECEIVE_WAF_STATISTICS_SUCCESS mutation', () =>
testAction(
actions.receiveWafStatisticsSuccess,
mockWafStatisticsResponse,
state, state,
[{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }],
[ [
{ { type: 'threatMonitoringWaf/fetchStatistics', payload: null },
type: types.RECEIVE_WAF_STATISTICS_SUCCESS, { type: 'threatMonitoringNetworkPolicy/fetchStatistics', payload: null },
payload: mockWafStatisticsResponse,
},
], ],
[],
)); ));
}); });
describe('receiveWafStatisticsError', () => {
it('commits the RECEIVE_WAF_STATISTICS_ERROR mutation', () =>
testAction(
actions.receiveWafStatisticsError,
undefined,
state,
[
{
type: types.RECEIVE_WAF_STATISTICS_ERROR,
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('fetchWafStatistics', () => {
let mock;
const currentEnvironmentId = 3;
beforeEach(() => {
state.wafStatisticsEndpoint = wafStatisticsEndpoint;
state.currentEnvironmentId = currentEnvironmentId;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
}); });
describe('on success', () => { describe('setCurrentTimeWindow', () => {
beforeEach(() => { const timeWindow = 'foo';
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(2019, 0, 31).getTime());
mock
.onGet(wafStatisticsEndpoint, {
params: {
environment_id: currentEnvironmentId,
from: '2019-01-01T00:00:00.000Z',
to: '2019-01-31T00:00:00.000Z',
interval: 'day',
},
})
.replyOnce(httpStatus.OK, mockWafStatisticsResponse);
});
it('should dispatch the request and success actions', () => it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches WAF fetch action', () =>
testAction( testAction(
actions.fetchWafStatistics, actions.setCurrentTimeWindow,
undefined, timeWindow,
state, state,
[], [{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[ [{ type: 'threatMonitoringWaf/fetchStatistics', payload: null }],
{ type: 'requestWafStatistics' },
{
type: 'receiveWafStatisticsSuccess',
payload: mockWafStatisticsResponse,
},
],
)); ));
});
describe('on NOT_FOUND', () => { describe('given the networkPolicyUi feature flag is enabled', () => {
beforeEach(() => { stubFeatureFlags({ networkPolicyUi: true });
mock.onGet(wafStatisticsEndpoint).replyOnce(httpStatus.NOT_FOUND);
});
it('should dispatch the request and success action with empty data', () => it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches WAF and Network Policy fetch actions', () =>
testAction( testAction(
actions.fetchWafStatistics, actions.setCurrentTimeWindow,
undefined, timeWindow,
state, state,
[], [{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[ [
{ type: 'requestWafStatistics' }, { type: 'threatMonitoringWaf/fetchStatistics', payload: null },
{ { type: 'threatMonitoringNetworkPolicy/fetchStatistics', payload: null },
type: 'receiveWafStatisticsSuccess',
payload: expect.objectContaining({
totalTraffic: 0,
anomalousTraffic: 0,
history: {
nominal: [],
anomalous: [],
},
}),
},
], ],
)); ));
}); });
describe('on error', () => {
beforeEach(() => {
mock.onGet(wafStatisticsEndpoint).replyOnce(500);
});
it('should dispatch the request and error actions', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[{ type: 'requestWafStatistics' }, { type: 'receiveWafStatisticsError' }],
));
});
describe('with an empty endpoint', () => {
beforeEach(() => {
state.wafStatisticsEndpoint = '';
});
it('should dispatch receiveWafStatisticsError', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[{ type: 'receiveWafStatisticsError' }],
));
});
}); });
}); });
...@@ -40,17 +40,4 @@ describe('threatMonitoring module getters', () => { ...@@ -40,17 +40,4 @@ describe('threatMonitoring module getters', () => {
expect(getters.currentTimeWindowName(state)).toBe('30 days'); expect(getters.currentTimeWindowName(state)).toBe('30 days');
}); });
}); });
describe('hasHistory', () => {
it.each(['nominal', 'anomalous'])('returns true if there is any %s history data', type => {
state.wafStatistics.history[type] = ['foo'];
expect(getters.hasHistory(state)).toBe(true);
});
it('returns false if there is no history', () => {
state.wafStatistics.history.nominal = [];
state.wafStatistics.history.anomalous = [];
expect(getters.hasHistory(state)).toBe(false);
});
});
}); });
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types'; import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types';
import mutations from 'ee/threat_monitoring/store/modules/threat_monitoring/mutations'; import mutations from 'ee/threat_monitoring/store/modules/threat_monitoring/mutations';
import { mockWafStatisticsResponse } from '../../../mock_data';
describe('Threat Monitoring mutations', () => { describe('Threat Monitoring mutations', () => {
let state; let state;
...@@ -9,13 +8,10 @@ describe('Threat Monitoring mutations', () => { ...@@ -9,13 +8,10 @@ describe('Threat Monitoring mutations', () => {
state = {}; state = {};
}); });
describe(types.SET_ENDPOINTS, () => { describe(types.SET_ENDPOINT, () => {
it('sets the endpoints', () => { it('sets the endpoints', () => {
const endpoints = { wafStatisticsEndpoint: 'waf', environmentsEndpoint: 'envs' }; mutations[types.SET_ENDPOINT](state, 'envs');
expect(state.environmentsEndpoint).toEqual('envs');
mutations[types.SET_ENDPOINTS](state, endpoints);
expect(state).toEqual(expect.objectContaining(endpoints));
}); });
}); });
...@@ -91,54 +87,4 @@ describe('Threat Monitoring mutations', () => { ...@@ -91,54 +87,4 @@ describe('Threat Monitoring mutations', () => {
expect(state.currentTimeWindow).toBe(timeWindow); expect(state.currentTimeWindow).toBe(timeWindow);
}); });
}); });
describe(types.REQUEST_WAF_STATISTICS, () => {
beforeEach(() => {
mutations[types.REQUEST_WAF_STATISTICS](state);
});
it('sets isLoadingWafStatistics to true', () => {
expect(state.isLoadingWafStatistics).toBe(true);
});
it('sets errorLoadingWafStatistics to false', () => {
expect(state.errorLoadingWafStatistics).toBe(false);
});
});
describe(types.RECEIVE_WAF_STATISTICS_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_WAF_STATISTICS_SUCCESS](state, mockWafStatisticsResponse);
});
it('sets wafStatistics according to the payload', () => {
expect(state.wafStatistics).toEqual({
totalTraffic: mockWafStatisticsResponse.total_traffic,
anomalousTraffic: mockWafStatisticsResponse.anomalous_traffic,
history: mockWafStatisticsResponse.history,
});
});
it('sets isLoadingWafStatistics to false', () => {
expect(state.isLoadingWafStatistics).toBe(false);
});
it('sets errorLoadingWafStatistics to false', () => {
expect(state.errorLoadingWafStatistics).toBe(false);
});
});
describe(types.RECEIVE_WAF_STATISTICS_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_WAF_STATISTICS_ERROR](state);
});
it('sets isLoadingWafStatistics to false', () => {
expect(state.isLoadingWafStatistics).toBe(false);
});
it('sets errorLoadingWafStatistics to true', () => {
expect(state.errorLoadingWafStatistics).toBe(true);
});
});
}); });
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/actions';
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/mutation_types';
import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/state';
import { mockWafStatisticsResponse } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn());
const statisticsEndpoint = 'statisticsEndpoint';
const timeRange = {
from: '2019-01-01T00:00:00.000Z',
to: '2019-01-31T00:00:00.000Z',
};
describe('threatMonitoringStatistics actions', () => {
let state;
beforeEach(() => {
state = getInitialState();
});
afterEach(() => {
createFlash.mockClear();
});
describe('requestStatistics', () => {
const payload = { foo: true };
it('commits the REQUEST_STATISTICS mutation and passes on the payload', () =>
testAction(
actions.requestStatistics,
payload,
state,
[
{
type: types.REQUEST_STATISTICS,
payload,
},
],
[],
));
});
describe('receiveStatisticsSuccess', () => {
it('commits the RECEIVE_STATISTICS_SUCCESS mutation', () =>
testAction(
actions.receiveStatisticsSuccess,
mockWafStatisticsResponse,
state,
[
{
type: types.RECEIVE_STATISTICS_SUCCESS,
payload: mockWafStatisticsResponse,
},
],
[],
));
});
describe('receiveStatisticsError', () => {
it('commits the RECEIVE_STATISTICS_ERROR mutation', () =>
testAction(
actions.receiveStatisticsError,
undefined,
state,
[
{
type: types.RECEIVE_STATISTICS_ERROR,
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('fetchStatistics', () => {
let mock;
const currentEnvironmentId = 3;
beforeEach(() => {
state.statisticsEndpoint = statisticsEndpoint;
state.threatMonitoring = { currentEnvironmentId };
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(2019, 0, 31).getTime());
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock
.onGet(statisticsEndpoint, {
params: {
environment_id: currentEnvironmentId,
interval: 'day',
...timeRange,
},
})
.replyOnce(httpStatus.OK, mockWafStatisticsResponse);
});
it('should dispatch the request and success actions', () =>
testAction(
actions.fetchStatistics,
undefined,
state,
[],
[
{
type: 'requestStatistics',
payload: expect.objectContaining(timeRange),
},
{
type: 'receiveStatisticsSuccess',
payload: mockWafStatisticsResponse,
},
],
));
});
describe('on NOT_FOUND', () => {
beforeEach(() => {
mock.onGet(statisticsEndpoint).replyOnce(httpStatus.NOT_FOUND);
});
it('should dispatch the request and success action with empty data', () =>
testAction(
actions.fetchStatistics,
undefined,
state,
[],
[
{ type: 'requestStatistics', payload: expect.any(Object) },
{
type: 'receiveStatisticsSuccess',
payload: expect.objectContaining({
total: 0,
anomalous: 0,
history: {
nominal: [],
anomalous: [],
},
}),
},
],
));
});
describe('on error', () => {
beforeEach(() => {
mock.onGet(statisticsEndpoint).replyOnce(500);
});
it('should dispatch the request and error actions', () =>
testAction(
actions.fetchStatistics,
undefined,
state,
[],
[
{ type: 'requestStatistics', payload: expect.any(Object) },
{ type: 'receiveStatisticsError' },
],
));
});
describe('with an empty endpoint', () => {
beforeEach(() => {
state.statisticsEndpoint = '';
});
it('should dispatch receiveStatisticsError', () =>
testAction(
actions.fetchStatistics,
undefined,
state,
[],
[{ type: 'receiveStatisticsError' }],
));
});
});
});
import createState from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/state';
import * as getters from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/getters';
describe('threatMonitoringStatistics module getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('hasHistory', () => {
it.each(['nominal', 'anomalous'])('returns true if there is any %s history data', type => {
state.statistics.history[type] = ['foo'];
expect(getters.hasHistory(state)).toBe(true);
});
it('returns false if there is no history', () => {
state.statistics.history.nominal = [];
state.statistics.history.anomalous = [];
expect(getters.hasHistory(state)).toBe(false);
});
});
});
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/mutation_types';
import mutationsFactory from 'ee/threat_monitoring/store/modules/threat_monitoring_statistics/mutations';
import { mockWafStatisticsResponse } from '../../../mock_data';
describe('threatMonitoringStatistics mutations', () => {
let state;
const mutations = mutationsFactory(payload => payload);
beforeEach(() => {
state = {};
});
describe(types.SET_ENDPOINT, () => {
it('sets the endpoint', () => {
mutations[types.SET_ENDPOINT](state, 'endpoint');
expect(state.statisticsEndpoint).toEqual('endpoint');
});
});
describe(types.REQUEST_STATISTICS, () => {
const payload = { foo: true };
beforeEach(() => {
mutations[types.REQUEST_STATISTICS](state, payload);
});
it('sets isLoadingStatistics to true', () => {
expect(state.isLoadingStatistics).toBe(true);
});
it('sets errorLoadingStatistics to false', () => {
expect(state.errorLoadingStatistics).toBe(false);
});
it('sets timeRange to the payload', () => {
expect(state.timeRange).toBe(payload);
});
});
describe(types.RECEIVE_STATISTICS_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_STATISTICS_SUCCESS](state, mockWafStatisticsResponse);
});
it('sets statistics according to the payload', () => {
expect(state.statistics).toEqual(mockWafStatisticsResponse);
});
it('sets isLoadingStatistics to false', () => {
expect(state.isLoadingStatistics).toBe(false);
});
it('sets errorLoadingStatistics to false', () => {
expect(state.errorLoadingStatistics).toBe(false);
});
});
describe(types.RECEIVE_STATISTICS_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_STATISTICS_ERROR](state);
});
it('sets isLoadingStatistics to false', () => {
expect(state.isLoadingStatistics).toBe(false);
});
it('sets errorLoadingStatistics to true', () => {
expect(state.errorLoadingStatistics).toBe(true);
});
});
});
import { import { getTimeWindowConfig, getTimeWindowParams } from 'ee/threat_monitoring/store/utils';
getTimeWindowConfig,
getTimeWindowParams,
} from 'ee/threat_monitoring/store/modules/threat_monitoring/utils';
import { DEFAULT_TIME_WINDOW, TIME_WINDOWS } from 'ee/threat_monitoring/constants'; import { DEFAULT_TIME_WINDOW, TIME_WINDOWS } from 'ee/threat_monitoring/constants';
describe('threatMonitoring module utils', () => { describe('threatMonitoring module utils', () => {
...@@ -25,7 +22,7 @@ describe('threatMonitoring module utils', () => { ...@@ -25,7 +22,7 @@ describe('threatMonitoring module utils', () => {
${'thirtyMinutes'} | ${'2020-01-01T09:30:00.000Z'} | ${'minute'} ${'thirtyMinutes'} | ${'2020-01-01T09:30:00.000Z'} | ${'minute'}
${'oneHour'} | ${'2020-01-01T09:00:00.000Z'} | ${'minute'} ${'oneHour'} | ${'2020-01-01T09:00:00.000Z'} | ${'minute'}
${'twentyFourHours'} | ${'2019-12-31T10:00:00.000Z'} | ${'hour'} ${'twentyFourHours'} | ${'2019-12-31T10:00:00.000Z'} | ${'hour'}
${'sevenDays'} | ${'2019-12-25T10:00:00.000Z'} | ${'hour'} ${'sevenDays'} | ${'2019-12-25T10:00:00.000Z'} | ${'day'}
${'thirtyDays'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'} ${'thirtyDays'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'}
${'foo'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'} ${'foo'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'}
`( `(
......
...@@ -20484,31 +20484,40 @@ msgstr "" ...@@ -20484,31 +20484,40 @@ msgstr ""
msgid "Threat Monitoring" msgid "Threat Monitoring"
msgstr "" msgstr ""
msgid "ThreatMonitoring|A Web Application Firewall (WAF) provides monitoring and rules to protect production applications. GitLab adds the modsecurity WAF plug-in when you install the Ingress app in your Kubernetes cluster."
msgstr ""
msgid "ThreatMonitoring|Anomalous Requests" msgid "ThreatMonitoring|Anomalous Requests"
msgstr "" msgstr ""
msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data." msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data."
msgstr "" msgstr ""
msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Dropped Packets"
msgstr ""
msgid "ThreatMonitoring|Environment" msgid "ThreatMonitoring|Environment"
msgstr "" msgstr ""
msgid "ThreatMonitoring|No traffic to display" msgid "ThreatMonitoring|No traffic to display"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Operations Per Second"
msgstr ""
msgid "ThreatMonitoring|Packet Activity"
msgstr ""
msgid "ThreatMonitoring|Requests" msgid "ThreatMonitoring|Requests"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Show last" msgid "ThreatMonitoring|Show last"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch WAF statistics" msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments" msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr "" msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below." msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
...@@ -20520,13 +20529,25 @@ msgstr "" ...@@ -20520,13 +20529,25 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link" msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Threat monitoring is not enabled"
msgstr ""
msgid "ThreatMonitoring|Threat monitoring provides security monitoring and rules to protect production applications."
msgstr ""
msgid "ThreatMonitoring|Time" msgid "ThreatMonitoring|Time"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Total Packets"
msgstr ""
msgid "ThreatMonitoring|Total Requests" msgid "ThreatMonitoring|Total Requests"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Web Application Firewall not enabled" msgid "ThreatMonitoring|Web Application Firewall"
msgstr ""
msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the Network Policies correctly."
msgstr "" msgstr ""
msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the WAF correctly." msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the WAF correctly."
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment