diff --git a/src/core/applicationGatewaySettings.js b/src/core/applicationGatewaySettings.js index e3d6f4a..c2c81aa 100644 --- a/src/core/applicationGatewaySettings.js +++ b/src/core/applicationGatewaySettings.js @@ -39,7 +39,7 @@ const APPLICATIONGATEWAY_SETTINGS_DEFAULTS = { urlPathMaps: [], requestRoutingRules: [ { - ruleType: 'Basic' // this is set by default, should be here or not ? + ruleType: 'Basic' //TODO: this is set by default, should be here or not ? } ], probes: [ @@ -89,7 +89,7 @@ function merge({ settings, buildingBlockSettings, defaultSettings }) { function defaultsCustomizer(objValue, srcValue, key) { if (key === 'frontendIPConfigurations') { - if (_.isNil(srcValue) || srcValue.length === 0) { + if (_.isUndefined(srcValue) || !_.isArray(srcValue) || srcValue.length === 0) { return objValue; } else { delete objValue[0].name; @@ -131,11 +131,18 @@ let validApplicationGatewaySslCipherSuites = [ 'TLS_RSA_WITH_3DES_EDE_CBC_SHA' ]; let validSslProtocols = ['TLSv1_0', 'TLSv1_1', 'TLSv1_2']; -let validSslPolicyTypes = ['Predefined', 'Predefined']; +let validSslPolicyTypes = ['Predefined', 'Custom']; let validApplicationGatewayRequestRoutingRuleTypes = ['Basic', 'PathBasedRouting']; let validCookieBasedAffinityValues = ['Enabled', 'Disabled']; let validPrivateIPAllocationMethods = ['Static', 'Dynamic']; +let validRuleSetTypes = ['OWASP']; +let isNilOrInRange = (value, from, to) => { + return { + result: _.isUndefined(value) || _.inRange(_.toSafeInteger(value), from, to), + message: `Valid values are from ${from} to ${to}` + }; +}; let isValidSkuName = (skuName) => { return v.utilities.isStringInArray(skuName, validSkuNames); @@ -185,6 +192,10 @@ let isValidPrivateIPAllocationMethod = (privateIPAllocationMethod) => { return v.utilities.isStringInArray(privateIPAllocationMethod, validPrivateIPAllocationMethods); }; +let isValidRuleSetType = (ruleSetType) => { + return v.utilities.isStringInArray(ruleSetType, validRuleSetTypes); +}; + let frontendIPConfigurationValidations = { name: v.validationUtilities.isNotNullOrWhitespace, applicationGatewayType: (value) => { @@ -195,7 +206,7 @@ let frontendIPConfigurationValidations = { }, internalApplicationGatewaySettings: (value, parent) => { if (parent.applicationGatewayType === 'Public') { - if (!_.isNil(value)) { + if (!_.isUndefined(value)) { return { result: false, message: 'If applicationGatewayType is Public, internalApplicationGatewaySettings cannot be specified' @@ -212,7 +223,7 @@ let frontendIPConfigurationValidations = { }; }, publicIpAddress: (value) => { - return _.isNil(value) ? { + return _.isUndefined(value) ? { result: true } : { validations: publicIpAddressSettings.validations @@ -240,7 +251,7 @@ let frontendPortsValidations = { }; let protocolValidation = (protocol) => { - if (_.isNil(protocol)) { + if (_.isUndefined(protocol)) { return { result: true }; } @@ -269,8 +280,24 @@ let backendHttpSettingsCollectionValidations = { protocol: protocolValidation, cookieBasedAffinity: cookieBasedAffinityValidation, pickHostNameFromBackendAddress: v.validationUtilities.isBoolean, - probeEnabled: v.validationUtilities.isBoolean, - // probeName: v.validationUtilities.isNotNullOrWhitespace NOT a required field! + probeEnabled: v.validationUtilities.isBoolean +}; + +let disabledRuleGroupsValidations = (value) => { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { + return { result: true }; + } + let errorMessage = ''; + value.forEach((ruleGroup, index) => { + let result = v.validationUtilities.isNotNullOrWhitespace(ruleGroup.ruleGroupName); + if (result.result === false) { + errorMessage += `disabledRuleGroups[${index}].ruleGroupName ` + result.message + `.${os.EOL}`; + } + }); + return { + result: errorMessage === '', + message: errorMessage + }; }; let applicationGatewayValidations = { @@ -322,7 +349,7 @@ let applicationGatewayValidations = { return { validations: backendHttpSettingsCollectionValidations }; }, httpListeners: (value, parent) => { - if (_.isNil(value) || value.length === 0) { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { return { result: true }; } @@ -352,7 +379,7 @@ let applicationGatewayValidations = { }; }, urlPathMaps: (value, parent) => { - if (_.isNil(value) || value.length === 0) { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { return { result: true }; } @@ -375,7 +402,7 @@ let applicationGatewayValidations = { return (baseSettings.backendHttpSettingsCollection.length > 0 && matched.length === 0) ? result : { result: true }; }, pathRules: (value) => { - if (_.isNil(value) || value.length === 0) { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { return { result: false, message: 'pathRules must be specified' @@ -383,7 +410,7 @@ let applicationGatewayValidations = { } let errorMessage = ''; value.forEach((pathRule, index) => { - if (_.isNil(pathRule.paths) || pathRule.paths.length === 0) { + if (_.isUndefined(pathRule.paths) || pathRule.paths.length === 0) { errorMessage += `At least one path must be specified pathRules[${index}].paths.${os.EOL}`; } }); @@ -419,7 +446,7 @@ let applicationGatewayValidations = { }; }, requestRoutingRules: (value, parent) => { - if (_.isNil(value) || value.length === 0) { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { return { result: true }; } @@ -451,7 +478,7 @@ let applicationGatewayValidations = { return (baseSettings.httpListeners.length > 0 && matched.length === 0) ? result : { result: true }; }, ruleType: (value) => { - if (value === 'PathBasedRouting' && (_.isNil(baseSettings.urlPathMaps) || baseSettings.urlPathMaps.length === 0)) { + if (value === 'PathBasedRouting' && (_.isUndefined(baseSettings.urlPathMaps) || baseSettings.urlPathMaps.length === 0)) { return { result: false, message: 'At least one urlPathMaps must be specified when ruleType is PathBasedRouting' @@ -461,7 +488,7 @@ let applicationGatewayValidations = { return { validations: requestRoutingRuleTypeValidation }; }, urlPathMapName: (value, parent) => { - if (_.isNil(value) && parent.ruleType !== 'PathBasedRouting') { + if (_.isUndefined(value) && parent.ruleType !== 'PathBasedRouting') { return { result: true }; } let result = { @@ -477,21 +504,25 @@ let applicationGatewayValidations = { }; }, probes: (value) => { - if (_.isNil(value)) { + if (_.isUndefined(value)) { return { result: true }; } let probesValidation = { + name: v.validationUtilities.isNotNullOrWhitespace, protocol: protocolValidation, - pickHostNameFromBackendHttpSettings: v.validationUtilities.isBoolean - // TODO: valid host - // TODO: valid path - // TODO: valid interval - // TODO: valid timeout - // TODO: valid unhealthyThreshold - // TODO: valid pickHostNameFromBackendHttpSettings - // TODO: valid minServers - // TODO: match + pickHostNameFromBackendHttpSettings: v.validationUtilities.isBoolean, + interval: (value) => isNilOrInRange(value, 1, 86400), + timeout: (value) => isNilOrInRange(value, 1, 86400), + unhealthyThreshold: (value) => isNilOrInRange(value, 1, 20), + path: (value) => { + return { + result: _.isUndefined(value) || value.indexOf('/') === 0, + message: 'Path must start with "/"' + }; + } + // TODO: valid minServers + // TODO: match }; return { validations: probesValidation }; }, @@ -499,17 +530,55 @@ let applicationGatewayValidations = { return { result: true }; // TODO: if provided, than in correct schema }, - webApplicationFirewallConfiguration: () => { - return { result: true }; - // TODO: valid enabled - // TODO: valid firewallMode - // TODO: valid ruleSetType - // TODO: valid ruleSetVersion - // TODO: valid disabledRuleGroups + webApplicationFirewallConfiguration: (value) => { + if (_.isUndefined(value)) { + return { result: true }; + } + + let webApplicationFirewallConfigurationValidations = { + enabled: v.validationUtilities.isBoolean, + firewallMode: (value) => { + return { + result: isValidFirewallMode(value), + message: `Valid values are ${validFirewallModes.join(' ,')}` + }; + }, + ruleSetType: (value) => { + return { + result: isValidRuleSetType(value), + message: `Valid values for ruleSetType are ${validRuleSetTypes.join(', ')}` + }; + }, + ruleSetVersion: v.validationUtilities.isNotNullOrWhitespace, + disabledRuleGroups: disabledRuleGroupsValidations + }; + return { validations: webApplicationFirewallConfigurationValidations }; }, - sslPolicy: () => { - return { result: true }; - // TODO: if provided, than in correct schema + sslPolicy: (value) => { + if (_.isUndefined(value)) { + return { result: true }; + } + + let sslPolicyValidations = { + disabledSslProtocols: (value) => { + if (_.isUndefined(value) || (_.isArray(value) && value.length === 0)) { + return { result: true }; + } + let errorMessage = ''; + value.forEach((sslProtocol, index) => { + if (!isValidSslProtocol(sslProtocol)) { + errorMessage += `Valid values for sslPolicy.disabledSslProtocols[${index}] are ${validSslProtocols.join(', ')}.${os.EOL}`; + } + }); + + //TODO: Does it make sense to have the 3 disabled? + return { + result: errorMessage === '', + message: errorMessage + }; + } + }; + return { validations: sslPolicyValidations }; } }; @@ -580,9 +649,9 @@ let processProperties = { properties: {} }; - if (!_.isNil(pool.backendAddresses) && pool.backendAddresses.length > 0) { + if (!_.isUndefined(pool.backendAddresses) && pool.backendAddresses.length > 0) { addressPool.properties.backendAddresses = pool.backendAddresses; - } else if (!_.isNil(pool.backendIPConfigurations) && pool.backendIPConfigurations.length > 0) { + } else if (!_.isUndefined(pool.backendIPConfigurations) && pool.backendIPConfigurations.length > 0) { // TODO: should get the machines dynamically from parent/nameprefix addressPool.properties.backendIPConfigurations = pool.backendIPConfigurations; } @@ -603,7 +672,7 @@ let processProperties = { requestTimeout: httpSetting.requestTimeout } }; - if (!_.isNil(httpSetting.probeName)) { + if (!_.isUndefined(httpSetting.probeName)) { setting.properties.probe = { id: resources.resourceId(parent.subscriptionId, parent.resourceGroupName, 'Microsoft.Network/applicationGateways/probes', parent.name, httpSetting.probeName) }; diff --git a/test/applicationGatewaySettingsSpec.js b/test/applicationGatewaySettingsSpec.js index 1b1b734..28e6a2b 100644 --- a/test/applicationGatewaySettingsSpec.js +++ b/test/applicationGatewaySettingsSpec.js @@ -814,6 +814,296 @@ describe('applicationGatewaySettings:', () => { expect(result[0].name).toEqual('.requestRoutingRules[0].ruleType'); }); + it('valid probes', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 30, + timeout: 30, + unhealthyThreshold: 3, + pickHostNameFromBackendHttpSettings: false, + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('valid probes', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 30, + timeout: 30, + unhealthyThreshold: 3, + pickHostNameFromBackendHttpSettings: false + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('probes must have a name', () => { + settings.probes = [ + { + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 30, + timeout: 30, + unhealthyThreshold: 3, + pickHostNameFromBackendHttpSettings: false + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].name'); + }); + it('probes protocol must be Http or Https', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'invalid', + host: 'contoso.com', + path: '/', + interval: 30, + timeout: 30, + unhealthyThreshold: 3 + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].protocol'); + }); + it('probes path must start with /', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: 'foo/', + interval: 30, + timeout: 30, + unhealthyThreshold: 3 + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].path'); + }); + it('probes interval must be between 1 and 86400', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 864999, + timeout: 30, + unhealthyThreshold: 3 + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].interval'); + }); + it('probes timeout must be between 1 and 86400', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 1, + timeout: 0, + unhealthyThreshold: 3 + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].timeout'); + }); + it('probes unhealthyThreshold must be between 1 and 20', () => { + settings.probes = [ + { + name: 'p1', + protocol: 'Http', + host: 'contoso.com', + path: '/', + interval: 5, + timeout: 30, + unhealthyThreshold: 21 + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.probes[0].unhealthyThreshold'); + }); + + it('valid webApplicationFirewallConfiguration', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: false, + firewallMode: 'Prevention', + ruleSetType: 'OWASP', + ruleSetVersion: '3.0', + disabledRuleGroups: [] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('webApplicationFirewallConfiguration enabled must be boolean', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: 'invalid', + firewallMode: 'Prevention', + ruleSetType: 'OWASP', + ruleSetVersion: '3.0', + disabledRuleGroups: [] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.webApplicationFirewallConfiguration[0].enabled'); + }); + it('webApplicationFirewallConfiguration firewallMode must be Detection or Prevention', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'invalid', + ruleSetType: 'OWASP', + ruleSetVersion: '3.0', + disabledRuleGroups: [] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.webApplicationFirewallConfiguration[0].firewallMode'); + }); + it('webApplicationFirewallConfiguration ruleSetType must be OWASP', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'invalid', + ruleSetVersion: '3.0' + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.webApplicationFirewallConfiguration[0].ruleSetType'); + }); + it('webApplicationFirewallConfiguration ruleSetVersion takes default when not specified', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'OWASP', + disabledRuleGroups: [] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('valid webApplicationFirewallConfiguration disabledRuleGroups', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'OWASP', + disabledRuleGroups: [ + { + ruleGroupName: 'rule1', + rules: [1,3] + } + ] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('webApplicationFirewallConfiguration disabledRuleGroups rules can be undefined', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'OWASP', + disabledRuleGroups: [ + { + ruleGroupName: 'rule1' + } + ] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('webApplicationFirewallConfiguration disabledRuleGroups rules can be empty', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'OWASP', + disabledRuleGroups: [ + { + ruleGroupName: 'rule1', + rules: [] + } + ] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('webApplicationFirewallConfiguration disabledRuleGroups ruleGroupName must be specified', () => { + settings.webApplicationFirewallConfiguration = [ + { + enabled: true, + firewallMode: 'Detection', + ruleSetType: 'OWASP', + disabledRuleGroups: [ + { + rules: [1,2,3] + } + ] + } + ]; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.webApplicationFirewallConfiguration[0].disabledRuleGroups'); + }); + + it('valid sslPolicy', () => { + settings.sslPolicy = { + disabledSslProtocols: [ 'TLSv1_0', 'TLSv1_1' ] + }; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('sslPolicy disabledSslProtocols can be undefined', () => { + settings.sslPolicy = {}; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('sslPolicy disabledSslProtocols can be empty', () => { + settings.sslPolicy = { + disabledSslProtocols: [] + }; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(0); + }); + it('sslPolicy disabledSslProtocols must be one of TLSv1_0, TLSv1_1 or TLSv1_2', () => { + settings.sslPolicy = { + disabledSslProtocols: [ 'invalid', 'TLSv1_1' ] + }; + let result = mergeAndValidate(settings, buildingBlockSettings); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('.sslPolicy.disabledSslProtocols'); + }); }); if (global.testConfiguration.runTransform) {