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>
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions } from 'vuex';
import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
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 WafLoadingSkeleton from './waf_loading_skeleton.vue';
import WafStatisticsSummary from './waf_statistics_summary.vue';
import WafStatisticsHistory from './waf_statistics_history.vue';
import ThreatMonitoringSection from './threat_monitoring_section.vue';
export default {
name: 'ThreatMonitoring',
......@@ -17,10 +16,9 @@ export default {
GlLink,
GlPopover,
ThreatMonitoringFilters,
WafLoadingSkeleton,
WafStatisticsSummary,
WafStatisticsHistory,
ThreatMonitoringSection,
},
mixins: [glFeatureFlagsMixin()],
props: {
defaultEnvironmentId: {
type: Number,
......@@ -55,19 +53,15 @@ export default {
return {
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
// environments, therefore the WAF cannot be set up. A valid default
// environment id only means that WAF *might* be set up.
isWafMaybeSetUp: this.isValidEnvironmentId(this.defaultEnvironmentId),
// environments, therefore infrastructure cannot be set up. A valid default
// environment id only means that infrastructure *might* be set up.
isSetUpMaybe: this.isValidEnvironmentId(this.defaultEnvironmentId),
};
},
computed: {
...mapState('threatMonitoring', ['isLoadingWafStatistics']),
...mapGetters('threatMonitoring', ['hasHistory']),
},
created() {
if (this.isWafMaybeSetUp) {
if (this.isSetUpMaybe) {
this.setCurrentEnvironmentId(this.defaultEnvironmentId);
this.fetchEnvironments();
}
......@@ -90,10 +84,19 @@ export default {
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.`,
),
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__(
`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.`,
`ThreatMonitoring|Threat monitoring provides security monitoring and rules
to protect production applications.`,
),
alertText: s__(
`ThreatMonitoring|The graph below is an overview of traffic coming to your
......@@ -108,9 +111,9 @@ export default {
<template>
<gl-empty-state
v-if="!isWafMaybeSetUp"
v-if="!isSetUpMaybe"
ref="emptyState"
:title="s__('ThreatMonitoring|Web Application Firewall not enabled')"
:title="s__('ThreatMonitoring|Threat monitoring is not enabled')"
:description="$options.emptyStateDescription"
:svg-path="emptyStateSvgPath"
:primary-button-link="documentationPath"
......@@ -129,7 +132,7 @@ export default {
{{ $options.alertText }}
</gl-alert>
<header class="my-3">
<h2 class="h4 mb-1">
<h2 class="h3 mb-1">
{{ s__('ThreatMonitoring|Threat Monitoring') }}
<gl-link
ref="helpLink"
......@@ -147,21 +150,34 @@ export default {
<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-if="glFeatures.networkPolicyUi">
<hr />
<template v-else-if="hasHistory">
<waf-statistics-summary class="mt-3" />
<waf-statistics-history class="mt-3" />
<threat-monitoring-section
ref="networkPolicySection"
store-namespace="threatMonitoringNetworkPolicy"
:title="s__('ThreatMonitoring|Container Network Policy')"
:subtitle="s__('ThreatMonitoring|Packet Activity')"
:anomalous-title="s__('ThreatMonitoring|Dropped Packets')"
:nominal-title="s__('ThreatMonitoring|Total Packets')"
:y-legend="s__('ThreatMonitoring|Operations Per Second')"
:chart-empty-state-text="$options.networkPolicyChartEmptyStateDescription"
:chart-empty-state-svg-path="chartEmptyStateSvgPath"
:documentation-path="documentationPath"
/>
</template>
<gl-empty-state
v-else
ref="chartEmptyState"
:title="s__('ThreatMonitoring|No traffic to display')"
:description="$options.chartEmptyStateDescription"
:svg-path="chartEmptyStateSvgPath"
:primary-button-link="documentationPath"
:primary-button-text="__('Learn More')"
/>
</section>
</template>
<script>
import _ from 'underscore';
import dateFormat from 'dateformat';
import { mapState } from 'vuex';
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import {
ANOMALOUS_REQUESTS,
COLORS,
DATE_FORMATS,
REQUESTS,
TIME,
TOTAL_REQUESTS,
} from './constants';
import { COLORS, DATE_FORMATS, TIME } from './constants';
export default {
name: 'WafStatisticsHistoryChart',
name: 'StatisticsHistoryChart',
components: {
GlAreaChart,
},
directives: {
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() {
return {
chartInstance: null,
......@@ -30,40 +37,45 @@ export default {
};
},
computed: {
...mapState('threatMonitoring', ['wafStatistics']),
chartData() {
const { anomalous, nominal } = this.wafStatistics.history;
const { anomalous, nominal } = this.data;
const anomalousStyle = { color: COLORS.anomalous };
const nominalStyle = { color: COLORS.nominal };
return [
{
name: ANOMALOUS_REQUESTS,
data: anomalous,
name: anomalous.title,
data: anomalous.values,
areaStyle: anomalousStyle,
lineStyle: anomalousStyle,
itemStyle: anomalousStyle,
},
{
name: TOTAL_REQUESTS,
data: nominal,
name: nominal.title,
data: nominal.values,
areaStyle: nominalStyle,
lineStyle: nominalStyle,
itemStyle: nominalStyle,
},
];
},
},
chartOptions: {
xAxis: {
name: TIME,
type: 'time',
axisLabel: {
formatter: value => dateFormat(value, DATE_FORMATS.defaultDate),
},
},
yAxis: {
name: REQUESTS,
chartOptions() {
const { from, to } = this.data;
return {
xAxis: {
name: TIME,
type: 'time',
axisLabel: {
formatter: value => dateFormat(value, DATE_FORMATS.defaultDate),
},
min: from,
max: to,
},
yAxis: {
name: this.yLegend,
},
};
},
},
methods: {
......@@ -88,13 +100,13 @@ export default {
<gl-area-chart
v-gl-resize-observer-directive="onResize"
:data="chartData"
:option="$options.chartOptions"
:option="chartOptions"
:include-legend-avg-max="false"
:format-tooltip-text="formatTooltipText"
@created="onChartCreated"
>
<template #tooltipTitle>
<div>{{ tooltipTitle }} ({{ $options.chartOptions.xAxis.name }})</div>
<div>{{ tooltipTitle }} ({{ chartOptions.xAxis.name }})</div>
</template>
<template #tooltipContent>
......
<script>
import { mapState } from 'vuex';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { engineeringNotation } from '@gitlab/ui/src/utils/number_utils';
import { ANOMALOUS_REQUESTS, TOTAL_REQUESTS } from './constants';
export default {
name: 'WafStatisticsSummary',
name: 'StatisticsSummary',
components: {
GlSingleStat,
},
props: {
data: {
type: Object,
required: true,
validator: ({ anomalous, nominal }) =>
Boolean(anomalous?.title && anomalous?.value) && Boolean(nominal?.title && nominal?.value),
},
},
computed: {
...mapState('threatMonitoring', ['wafStatistics']),
statistics() {
const { anomalous, nominal } = this.data;
return [
{
key: 'anomalousTraffic',
title: ANOMALOUS_REQUESTS,
value: `${Math.round(this.wafStatistics.anomalousTraffic * 100)}%`,
title: anomalous.title,
value: `${Math.round(anomalous.value * 100)}%`,
variant: 'warning',
},
{
key: 'totalTraffic',
title: TOTAL_REQUESTS,
value: engineeringNotation(this.wafStatistics.totalTraffic),
title: nominal.title,
value: engineeringNotation(nominal.value),
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 = {
sevenDays: {
name: __('7 days'),
durationInMilliseconds: 7 * 24 * 60 * 60 * 1000,
interval: INTERVALS.hour,
interval: INTERVALS.day,
},
thirtyDays: {
name: __('30 days'),
......
......@@ -7,6 +7,7 @@ export default () => {
const el = document.querySelector('#js-threat-monitoring-app');
const {
wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
environmentsEndpoint,
chartEmptyStateSvgPath,
emptyStateSvgPath,
......@@ -20,6 +21,7 @@ export default () => {
const store = createStore();
store.dispatch('threatMonitoring/setEndpoints', {
wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
environmentsEndpoint,
});
......
import Vue from 'vue';
import Vuex from 'vuex';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import threatMonitoring from './modules/threat_monitoring';
import threatMonitoringStatistics from './modules/threat_monitoring_statistics';
Vue.use(Vuex);
......@@ -8,5 +10,25 @@ export default () =>
new Vuex.Store({
modules: {
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 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 * 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 receiveEnvironmentsSuccess = ({ commit }, environments) =>
......@@ -50,45 +56,18 @@ export const fetchEnvironments = ({ state, dispatch }) => {
export const setCurrentEnvironmentId = ({ commit, dispatch }, 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) => {
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 (!state.wafStatisticsEndpoint) {
return dispatch('receiveWafStatisticsError');
if (window.gon.features?.networkPolicyUi) {
dispatch(`threatMonitoringNetworkPolicy/fetchStatistics`, null, { root: true });
}
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 { getTimeWindowConfig } from './utils';
import { getTimeWindowConfig } from '../../utils';
export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => {
const environment = environments.find(({ id }) => id === currentEnvironmentId);
......@@ -8,6 +8,3 @@ export const currentEnvironmentName = ({ currentEnvironmentId, environments }) =
export const currentTimeWindowName = ({ currentTimeWindow }) =>
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 RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
......@@ -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_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';
export default {
[types.SET_ENDPOINTS](state, { wafStatisticsEndpoint, environmentsEndpoint }) {
state.wafStatisticsEndpoint = wafStatisticsEndpoint;
state.environmentsEndpoint = environmentsEndpoint;
[types.SET_ENDPOINT](state, endpoint) {
state.environmentsEndpoint = endpoint;
},
[types.REQUEST_ENVIRONMENTS](state) {
state.isLoadingEnvironments = true;
......@@ -25,17 +23,4 @@ export default {
[types.SET_CURRENT_TIME_WINDOW](state, 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 () => ({
errorLoadingEnvironments: false,
currentEnvironmentId: -1,
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 @@
module Projects
class ThreatMonitoringController < Projects::ApplicationController
before_action :authorize_read_threat_monitoring!
before_action only: [:show] do
push_frontend_feature_flag(:network_policy_ui)
end
end
end
......@@ -7,9 +7,10 @@
chart_empty_state_svg_path: image_path('illustrations/chart-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),
network_policy_statistics_endpoint: summary_project_security_network_policies_path(@project, format: :json),
environments_endpoint: project_environments_path(@project),
default_environment_id: default_environment_id,
user_callouts_path: user_callouts_path,
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
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`] = `
<gl-alert-stub
class="my-3"
......
// 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
data="[object Object],[object Object]"
option="[object Object]"
......
// 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
class="col-sm-6 col-md-4 col-lg-3"
title="Anomalous Requests"
value="3%"
title="Anomalous"
value="20%"
variant="warning"
/>
`;
exports[`WafStatisticsSummary component renders the nominal traffic count 1`] = `
exports[`StatisticsSummary component renders the nominal traffic count 1`] = `
<gl-single-stat-stub
class="col-sm-6 col-md-4 col-lg-3"
title="Total Requests"
value="2.7k"
title="Total"
value="100"
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';
import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.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 documentationPath = '/docs';
......@@ -17,6 +13,7 @@ const chartEmptyStateSvgPath = '/chart-svgs';
const emptyStateSvgPath = '/svgs';
const environmentsEndpoint = `${TEST_HOST}/environments`;
const wafStatisticsEndpoint = `${TEST_HOST}/waf`;
const networkPolicyStatisticsEndpoint = `${TEST_HOST}/network_policy`;
const userCalloutId = 'threat_monitoring_info';
const userCalloutsPath = `${TEST_HOST}/user_callouts`;
......@@ -24,11 +21,12 @@ describe('ThreatMonitoringApp component', () => {
let store;
let wrapper;
const factory = ({ propsData, state } = {}) => {
const factory = ({ propsData, state, options } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
environmentsEndpoint,
wafStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
...state,
});
......@@ -46,16 +44,15 @@ describe('ThreatMonitoringApp component', () => {
...propsData,
},
store,
...options,
});
};
const findAlert = () => wrapper.find(GlAlert);
const findFilters = () => wrapper.find(ThreatMonitoringFilters);
const findWafLoadingSkeleton = () => wrapper.find(WafLoadingSkeleton);
const findWafStatisticsHistory = () => wrapper.find(WafStatisticsHistory);
const findWafStatisticsSummary = () => wrapper.find(WafStatisticsSummary);
const findWafSection = () => wrapper.find({ ref: 'wafSection' });
const findNetworkPolicySection = () => wrapper.find({ ref: 'networkPolicySection' });
const findEmptyState = () => wrapper.find({ ref: 'emptyState' });
const findChartEmptyState = () => wrapper.find({ ref: 'chartEmptyState' });
afterEach(() => {
wrapper.destroy();
......@@ -89,11 +86,7 @@ describe('ThreatMonitoringApp component', () => {
describe('given there is a default environment with data', () => {
beforeEach(() => {
factory({
state: {
wafStatistics: mockWafStatisticsResponse,
},
});
factory();
});
it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
......@@ -111,13 +104,30 @@ describe('ThreatMonitoringApp component', () => {
expect(findFilters().exists()).toBe(true);
});
it('shows the summary and history statistics', () => {
expect(findWafStatisticsSummary().exists()).toBe(true);
expect(findWafStatisticsHistory().exists()).toBe(true);
it('renders the waf section', () => {
expect(findWafSection().element).toMatchSnapshot();
});
it('does not show the loading skeleton', () => {
expect(findWafLoadingSkeleton().exists()).toBe(false);
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('renders the network policy section', () => {
expect(findNetworkPolicySection().element).toMatchSnapshot();
});
});
describe('dismissing the alert', () => {
......@@ -157,39 +167,5 @@ describe('ThreatMonitoringApp component', () => {
it('does not render the alert', () => {
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 { GlAreaChart } from '@gitlab/ui/dist/charts';
import createStore from 'ee/threat_monitoring/store';
import WafStatisticsHistory from 'ee/threat_monitoring/components/waf_statistics_history.vue';
import StatisticsHistory from 'ee/threat_monitoring/components/statistics_history.vue';
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;
const MockResizeObserverDirective = {
......@@ -24,17 +23,21 @@ const MockResizeObserverDirective = {
const localVue = createLocalVue();
localVue.directive('gl-resize-observer-directive', MockResizeObserverDirective);
describe('WafStatisticsHistory component', () => {
let store;
describe('StatisticsHistory component', () => {
let wrapper;
const factory = ({ state, options } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, state);
wrapper = shallowMount(WafStatisticsHistory, {
const factory = ({ options } = {}) => {
wrapper = shallowMount(StatisticsHistory, {
localVue,
store,
propsData: {
data: {
anomalous: { title: 'Anomoulous', values: mockAnomalousHistory },
nominal: { title: 'Total', values: mockNominalHistory },
from: 'foo',
to: 'bar',
},
yLegend: 'Requests',
},
...options,
});
};
......@@ -47,18 +50,29 @@ describe('WafStatisticsHistory component', () => {
describe('the data passed to the chart', () => {
beforeEach(() => {
factory({
state: {
wafStatistics: {
history: mockWafStatisticsResponse.history,
},
},
});
factory();
});
it('is structured correctly', () => {
const { nominal, anomalous } = mockWafStatisticsResponse.history;
expect(findChart().props('data')).toMatchObject([{ data: anomalous }, { data: nominal }]);
expect(findChart().props('data')).toMatchObject([
{ 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', () => {
describe('chart tooltip', () => {
beforeEach(() => {
const mockTotalSeriesDatum = mockWafStatisticsResponse.history.nominal[0];
const mockAnomalousSeriesDatum = mockWafStatisticsResponse.history.anomalous[0];
const mockParams = {
seriesData: [
{
seriesName: ANOMALOUS_REQUESTS,
value: mockAnomalousSeriesDatum,
value: mockAnomalousHistory[0],
},
{
seriesName: TOTAL_REQUESTS,
value: mockTotalSeriesDatum,
value: mockNominalHistory[0],
},
],
};
......
import { shallowMount } from '@vue/test-utils';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createStore from 'ee/threat_monitoring/store';
import WafStatisticsSummary from 'ee/threat_monitoring/components/waf_statistics_summary.vue';
import { mockWafStatisticsResponse } from '../mock_data';
import StatisticsSummary from 'ee/threat_monitoring/components/statistics_summary.vue';
describe('WafStatisticsSummary component', () => {
let store;
describe('StatisticsSummary component', () => {
let wrapper;
const factory = state => {
store = createStore();
Object.assign(store.state.threatMonitoring, state);
wrapper = shallowMount(WafStatisticsSummary, {
store,
const factory = options => {
wrapper = shallowMount(StatisticsSummary, {
...options,
});
};
......@@ -22,9 +16,11 @@ describe('WafStatisticsSummary component', () => {
beforeEach(() => {
factory({
wafStatistics: {
totalTraffic: mockWafStatisticsResponse.total_traffic,
anomalousTraffic: mockWafStatisticsResponse.anomalous_traffic,
propsData: {
data: {
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 = {
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 = {
total_traffic: 2703,
anomalous_traffic: 0.03,
history: {
nominal: [['2019-12-04T00:00:00.000Z', 56], ['2019-12-05T00:00:00.000Z', 2647]],
anomalous: [['2019-12-04T00:00:00.000Z', 1], ['2019-12-05T00:00:00.000Z', 83]],
nominal: mockNominalHistory,
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
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 { mockEnvironmentsResponse, mockWafStatisticsResponse } from '../../../mock_data';
import { mockEnvironmentsResponse } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn());
const environmentsEndpoint = 'environmentsEndpoint';
const wafStatisticsEndpoint = 'wafStatisticsEndpoint';
const networkPolicyStatisticsEndpoint = 'networkPolicyStatisticsEndpoint';
const stubFeatureFlags = features => {
beforeEach(() => {
window.gon.features = features;
});
afterEach(() => {
delete window.gon.features;
});
};
describe('Threat Monitoring actions', () => {
let state;
......@@ -27,15 +38,23 @@ describe('Threat Monitoring actions', () => {
});
describe('setEndpoints', () => {
it('commits the SET_ENDPOINTS mutation', () =>
it('commits the SET_ENDPOINT mutation', () =>
testAction(
actions.setEndpoints,
{ environmentsEndpoint, wafStatisticsEndpoint },
{ environmentsEndpoint, wafStatisticsEndpoint, networkPolicyStatisticsEndpoint },
state,
[
{
type: types.SET_ENDPOINTS,
payload: { environmentsEndpoint, wafStatisticsEndpoint },
type: types.SET_ENDPOINT,
payload: environmentsEndpoint,
},
{
type: `threatMonitoringWaf/${types.SET_ENDPOINT}`,
payload: wafStatisticsEndpoint,
},
{
type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
payload: networkPolicyStatisticsEndpoint,
},
],
[],
......@@ -189,179 +208,58 @@ describe('Threat Monitoring actions', () => {
describe('setCurrentEnvironmentId', () => {
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(
actions.setCurrentEnvironmentId,
environmentId,
state,
[{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }],
[{ type: 'fetchWafStatistics' }],
[{ type: 'threatMonitoringWaf/fetchStatistics', payload: null }],
));
describe('given the networkPolicyUi feature flag is enabled', () => {
stubFeatureFlags({ networkPolicyUi: true });
it('commits the SET_CURRENT_ENVIRONMENT_ID mutation and dispatches WAF and Network Policy fetch actions', () =>
testAction(
actions.setCurrentEnvironmentId,
environmentId,
state,
[{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }],
[
{ type: 'threatMonitoringWaf/fetchStatistics', payload: null },
{ type: 'threatMonitoringNetworkPolicy/fetchStatistics', payload: null },
],
));
});
});
describe('setCurrentTimeWindow', () => {
const timeWindow = 'foo';
it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches fetchWafStatistics', () =>
it('commits the SET_CURRENT_TIME_WINDOW mutation and dispatches WAF fetch action', () =>
testAction(
actions.setCurrentTimeWindow,
timeWindow,
state,
[{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[{ type: 'fetchWafStatistics' }],
[{ type: 'threatMonitoringWaf/fetchStatistics', payload: null }],
));
});
describe('requestWafStatistics', () => {
it('commits the REQUEST_WAF_STATISTICS mutation', () =>
testAction(
actions.requestWafStatistics,
undefined,
state,
[
{
type: types.REQUEST_WAF_STATISTICS,
},
],
[],
));
});
describe('receiveWafStatisticsSuccess', () => {
it('commits the RECEIVE_WAF_STATISTICS_SUCCESS mutation', () =>
testAction(
actions.receiveWafStatisticsSuccess,
mockWafStatisticsResponse,
state,
[
{
type: types.RECEIVE_WAF_STATISTICS_SUCCESS,
payload: mockWafStatisticsResponse,
},
],
[],
));
});
describe('given the networkPolicyUi feature flag is enabled', () => {
stubFeatureFlags({ networkPolicyUi: true });
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', () => {
beforeEach(() => {
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', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[
{ type: 'requestWafStatistics' },
{
type: 'receiveWafStatisticsSuccess',
payload: mockWafStatisticsResponse,
},
],
));
});
describe('on NOT_FOUND', () => {
beforeEach(() => {
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(
actions.fetchWafStatistics,
undefined,
actions.setCurrentTimeWindow,
timeWindow,
state,
[],
[{ type: types.SET_CURRENT_TIME_WINDOW, payload: timeWindow }],
[
{ type: 'requestWafStatistics' },
{
type: 'receiveWafStatisticsSuccess',
payload: expect.objectContaining({
totalTraffic: 0,
anomalousTraffic: 0,
history: {
nominal: [],
anomalous: [],
},
}),
},
{ type: 'threatMonitoringWaf/fetchStatistics', payload: null },
{ type: 'threatMonitoringNetworkPolicy/fetchStatistics', payload: null },
],
));
});
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', () => {
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 mutations from 'ee/threat_monitoring/store/modules/threat_monitoring/mutations';
import { mockWafStatisticsResponse } from '../../../mock_data';
describe('Threat Monitoring mutations', () => {
let state;
......@@ -9,13 +8,10 @@ describe('Threat Monitoring mutations', () => {
state = {};
});
describe(types.SET_ENDPOINTS, () => {
describe(types.SET_ENDPOINT, () => {
it('sets the endpoints', () => {
const endpoints = { wafStatisticsEndpoint: 'waf', environmentsEndpoint: 'envs' };
mutations[types.SET_ENDPOINTS](state, endpoints);
expect(state).toEqual(expect.objectContaining(endpoints));
mutations[types.SET_ENDPOINT](state, 'envs');
expect(state.environmentsEndpoint).toEqual('envs');
});
});
......@@ -91,54 +87,4 @@ describe('Threat Monitoring mutations', () => {
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 {
getTimeWindowConfig,
getTimeWindowParams,
} from 'ee/threat_monitoring/store/modules/threat_monitoring/utils';
import { getTimeWindowConfig, getTimeWindowParams } from 'ee/threat_monitoring/store/utils';
import { DEFAULT_TIME_WINDOW, TIME_WINDOWS } from 'ee/threat_monitoring/constants';
describe('threatMonitoring module utils', () => {
......@@ -25,7 +22,7 @@ describe('threatMonitoring module utils', () => {
${'thirtyMinutes'} | ${'2020-01-01T09:30:00.000Z'} | ${'minute'}
${'oneHour'} | ${'2020-01-01T09:00:00.000Z'} | ${'minute'}
${'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'}
${'foo'} | ${'2019-12-02T10:00:00.000Z'} | ${'day'}
`(
......
......@@ -20484,31 +20484,40 @@ msgstr ""
msgid "Threat Monitoring"
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"
msgstr ""
msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data."
msgstr ""
msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Dropped Packets"
msgstr ""
msgid "ThreatMonitoring|Environment"
msgstr ""
msgid "ThreatMonitoring|No traffic to display"
msgstr ""
msgid "ThreatMonitoring|Operations Per Second"
msgstr ""
msgid "ThreatMonitoring|Packet Activity"
msgstr ""
msgid "ThreatMonitoring|Requests"
msgstr ""
msgid "ThreatMonitoring|Show last"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch WAF statistics"
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
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."
......@@ -20520,13 +20529,25 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link"
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"
msgstr ""
msgid "ThreatMonitoring|Total Packets"
msgstr ""
msgid "ThreatMonitoring|Total Requests"
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 ""
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