From f9ae99ea49b1f9044c268bfcb3383cc9464812e8 Mon Sep 17 00:00:00 2001 From: roybarda Date: Wed, 6 Dec 2023 11:59:21 +0200 Subject: [PATCH] first open-appsec support --- backend/internal/proxy-host.js | 61 ++++++++++++++++++- backend/package.json | 1 + backend/routes/api/main.js | 2 + backend/schema/definitions.json | 17 ++++++ backend/schema/endpoints/proxy-hosts.json | 45 ++++++++++++++ docker/docker-compose.dev.yml | 1 + frontend/html/partials/header.ejs | 1 + frontend/js/app/api.js | 28 +++++++++ frontend/js/app/controller.js | 28 +++++++++ frontend/js/app/nginx/proxy/form.ejs | 35 +++++++++++ frontend/js/app/nginx/proxy/form.js | 45 +++++++++++++- frontend/js/app/nginx/proxy/location-item.ejs | 37 ++++++++++- frontend/js/app/nginx/proxy/location.js | 26 +++++++- frontend/js/app/nginx/redirection/form.js | 1 + frontend/js/app/router.js | 1 + frontend/js/app/settings/main.ejs | 45 +++++++++++++- frontend/js/app/settings/main.js | 55 +++++++++++++++++ frontend/js/app/ui/header/main.ejs | 2 + frontend/js/app/ui/menu/main.ejs | 3 + frontend/js/i18n/messages.json | 4 +- frontend/js/models/proxy-host-location.js | 7 ++- frontend/js/models/proxy-host.js | 3 + 22 files changed, 440 insertions(+), 8 deletions(-) diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 02a98da2..a83413ca 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -4,8 +4,12 @@ const utils = require('../lib/utils'); const proxyHostModel = require('../models/proxy_host'); const internalHost = require('./host'); const internalNginx = require('./nginx'); +const internalNginxOpenappsec= require('./nginx-openappsec'); const internalAuditLog = require('./audit-log'); const internalCertificate = require('./certificate'); +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); function omissions () { return ['is_deleted']; @@ -48,9 +52,15 @@ const internalProxyHost = { data.owner_user_id = access.token.getUserId(1); data = internalHost.cleanSslHstsData(data); + let db_data = _.assign({}, data); + // Remove the openappsec fields from data. they are not in the database. + delete db_data.use_openappsec; + delete db_data.openappsec_mode; + delete db_data.minimum_confidence; + return proxyHostModel .query() - .insertAndFetch(data) + .insertAndFetch(db_data) .then(utils.omitRow(omissions())); }) .then((row) => { @@ -84,6 +94,10 @@ const internalProxyHost = { return row; }); }) + .then(row => { + internalNginxOpenappsec.generateConfig(access, row, data) + return row; + }) .then((row) => { // Audit log data.meta = _.assign({}, data.meta || {}, row.meta); @@ -159,6 +173,11 @@ const internalProxyHost = { return row; } }) + .then(row => { + internalNginxOpenappsec.generateConfig(access, row, data); + // internalNginxOpenappsec.updateConfig(row, data) + return row; + }) .then((row) => { // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. data = _.assign({}, { @@ -167,6 +186,11 @@ const internalProxyHost = { data = internalHost.cleanSslHstsData(data, row); + // Remove the openappsec fields from data. they are not in the database + delete data.use_openappsec; + delete data.openappsec_mode; + delete data.minimum_confidence; + return proxyHostModel .query() .where({id: data.id}) @@ -247,6 +271,22 @@ const internalProxyHost = { if (typeof data.omit !== 'undefined' && data.omit !== null) { row = _.omit(row, data.omit); } + return row; + }) + .then((row) => { + // add openappsec fields to row + try { + const configFilePath = internalNginxOpenappsec.getConfigFilePath(access); + const openappsecConfig = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + let result = internalNginxOpenappsec.getOpenappsecFields(openappsecConfig, row.id); + row.use_openappsec = result.use_openappsec; + row.openappsec_mode = result.mode; + row.minimum_confidence = result.minimum_confidence; + } + catch (e) { + console.log("Error reading openappsec config file: " + e); + } + return row; }); }, @@ -274,6 +314,10 @@ const internalProxyHost = { .patch({ is_deleted: 1 }) + .then(() => { + // Delete openappsec config + internalNginxOpenappsec.deleteConfig(access, row); + }) .then(() => { // Delete Nginx Config return internalNginx.deleteConfig('proxy_host', row) @@ -430,6 +474,21 @@ const internalProxyHost = { return query.then(utils.omitRows(omissions())); }) .then((rows) => { + // add openappsec fields to rows + try { + const configFilePath = internalNginxOpenappsec.getConfigFilePath(access); + const openappsecConfig = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + rows.map(function (row, idx) { + let result = internalNginxOpenappsec.getOpenappsecFields(openappsecConfig, row.id); + rows[idx].use_openappsec = result.use_openappsec; + rows[idx].openappsec_mode = result.mode; + rows[idx].minimum_confidence = result.minimum_confidence; + }); + } + catch (e) { + console.log("Error reading openappsec config file: " + e); + } + if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) { return internalHost.cleanAllRowsCertificateMeta(rows); } diff --git a/backend/package.json b/backend/package.json index e8f58255..3227ec93 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "express": "^4.17.3", "express-fileupload": "^1.1.9", "gravatar": "^1.8.0", + "js-yaml": "^4.1.0", "json-schema-ref-parser": "^8.0.0", "jsonwebtoken": "^9.0.0", "knex": "2.4.2", diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 33cbbc21..5eeea451 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -29,8 +29,10 @@ router.use('/schema', require('./schema')); router.use('/tokens', require('./tokens')); router.use('/users', require('./users')); router.use('/audit-log', require('./audit-log')); +router.use('/openappsec-log', require('./openappsec-log')); router.use('/reports', require('./reports')); router.use('/settings', require('./settings')); +router.use('/openappsec-settings', require('./openappsec-settings')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json index 4b4f3405..b3c87b57 100644 --- a/backend/schema/definitions.json +++ b/backend/schema/definitions.json @@ -137,6 +137,18 @@ } ] }, + "openappsec_mode": { + "description": "openappsec_mode ID", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "minimum_confidence": { + "description": "minimum_confidence ID", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, "access_list_id": { "description": "Access List ID", "example": 1234, @@ -231,6 +243,11 @@ "example": true, "type": "boolean" }, + "use_openappsec": { + "description": "Use openappsec", + "example": true, + "type": "boolean" + }, "caching_enabled": { "description": "Should we cache assets", "example": true, diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json index 9a3fff2f..029fd61f 100644 --- a/backend/schema/endpoints/proxy-hosts.json +++ b/backend/schema/endpoints/proxy-hosts.json @@ -50,6 +50,15 @@ "block_exploits": { "$ref": "../definitions.json#/definitions/block_exploits" }, + "use_openappsec": { + "$ref": "../definitions.json#/definitions/use_openappsec" + }, + "openappsec_mode": { + "$ref": "../definitions.json#/definitions/openappsec_mode" + }, + "minimum_confidence": { + "$ref": "../definitions.json#/definitions/minimum_confidence" + }, "caching_enabled": { "$ref": "../definitions.json#/definitions/caching_enabled" }, @@ -104,6 +113,15 @@ }, "advanced_config": { "type": "string" + }, + "use_openappsec": { + "type": "boolean" + }, + "openappsec_mode": { + "type": "string" + }, + "minimum_confidence": { + "type": "string" } } } @@ -149,6 +167,15 @@ "block_exploits": { "$ref": "#/definitions/block_exploits" }, + "use_openappsec": { + "$ref": "#/definitions/use_openappsec" + }, + "openappsec_mode": { + "$ref": "#/definitions/openappsec_mode" + }, + "minimum_confidence": { + "$ref": "#/definitions/minimum_confidence" + }, "caching_enabled": { "$ref": "#/definitions/caching_enabled" }, @@ -239,6 +266,15 @@ "block_exploits": { "$ref": "#/definitions/block_exploits" }, + "use_openappsec": { + "$ref": "#/definitions/use_openappsec" + }, + "openappsec_mode": { + "$ref": "#/definitions/openappsec_mode" + }, + "minimum_confidence": { + "$ref": "#/definitions/minimum_confidence" + }, "caching_enabled": { "$ref": "#/definitions/caching_enabled" }, @@ -312,6 +348,15 @@ "block_exploits": { "$ref": "#/definitions/block_exploits" }, + "use_openappsec": { + "$ref": "#/definitions/use_openappsec" + }, + "openappsec_mode": { + "$ref": "#/definitions/openappsec_mode" + }, + "minimum_confidence": { + "$ref": "#/definitions/minimum_confidence" + }, "caching_enabled": { "$ref": "#/definitions/caching_enabled" }, diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6d8cf87c..a8c68e4d 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -33,6 +33,7 @@ services: volumes: - npm_data:/data - le_data:/etc/letsencrypt + - ../localconfig:/ext/appsec - ../backend:/app - ../frontend:/app/frontend - ../global:/app/global diff --git a/frontend/html/partials/header.ejs b/frontend/html/partials/header.ejs index cabb9df2..c2e67a44 100644 --- a/frontend/html/partials/header.ejs +++ b/frontend/html/partials/header.ejs @@ -21,6 +21,7 @@ + diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index 6e33a6dc..2c198bf5 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -716,6 +716,17 @@ module.exports = { } }, + OpenappsecLog: { + /** + * @param {Array} [expand] + * @param {String} [query] + * @returns {Promise} + */ + getAll: function (expand, query) { + return getAllObjects('openappsec-log', expand, query); + } + }, + Reports: { /** @@ -753,5 +764,22 @@ module.exports = { delete data.id; return fetch('put', 'settings/' + id, data); } + }, + + OpenAppsecSettings: { + /** + * @returns {Promise} + */ + get: function () { + return fetch('get', 'openappsec-settings'); + }, + + /** + * @param {Object} data + * @returns {Promise} + */ + save: function (data) { + return fetch('put', 'openappsec-settings', data); } + }, }; diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js index ccb2978a..eb9677bb 100644 --- a/frontend/js/app/controller.js +++ b/frontend/js/app/controller.js @@ -407,6 +407,34 @@ module.exports = { } }, + /** + * openappsec Log + */ + showOpenappsecLog: function () { + let controller = this; + if (Cache.User.isAdmin()) { + require(['./main', './openappsec-log/main'], (App, View) => { + controller.navigate('/openappsec-log'); + App.UI.showAppContent(new View()); + }); + } else { + this.showDashboard(); + } + }, + + /** + * openappsec Log Metadata + * + * @param model + */ + showOpenappsecMeta: function (model) { + if (Cache.User.isAdmin()) { + require(['./main', './openappsec-log/meta'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Settings */ diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 56868f55..8d024623 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -72,6 +72,7 @@ +
+ +
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index 1dfb5c18..50b0dd30 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -33,6 +33,9 @@ module.exports = Mn.View.extend({ certificate_select: 'select[name="certificate_id"]', access_list_select: 'select[name="access_list_id"]', ssl_forced: 'input[name="ssl_forced"]', + use_openappsec: 'input[name="use_openappsec"]', + openappsec_mode: 'select[name="openappsec_mode"]', + minimum_confidence: 'select[name="minimum_confidence"]', hsts_enabled: 'input[name="hsts_enabled"]', hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', @@ -75,6 +78,24 @@ module.exports = Mn.View.extend({ inputs.trigger('change'); }, + 'change @ui.use_openappsec': function () { + let checked = this.ui.use_openappsec.prop('checked'); + this.ui.openappsec_mode + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + this.ui.minimum_confidence + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + /*** check this */ + if (!checked) { + this.ui.openappsec_mode.prop('checked', false); + } + }, + 'change @ui.ssl_forced': function () { let checked = this.ui.ssl_forced.prop('checked'); this.ui.hsts_enabled @@ -146,14 +167,32 @@ module.exports = Mn.View.extend({ } let view = this; - let data = this.ui.form.serializeJSON(); - // Add locations + // enable openappsec inputs for serialization. if we don't do this, serialization of custom locations will be added to the root object instead of the missing root properties with the same name. + + let topHostDisabledElements = this.ui.form.find('#details [name*="openappsec"]:disabled, [name="minimum_confidence"]:disabled'); + topHostDisabledElements.prop('disabled', false); + let data = this.ui.form.serializeJSON(); + topHostDisabledElements.prop('disabled', true); + + // Check if openappsec is enabled. serializeJSON() will only return its value attribute, not its checked attribute. + let use_openappsec = this.ui.use_openappsec.prop('checked'); + data.use_openappsec = use_openappsec; + + // Add locations using the model defined in file frontend/js/app/nginx/proxy/location.js and the template frontend/js/app/nginx/proxy/location-item.ejs. + // input fields with the class 'model' will automatically update the model. data.locations = []; this.locationsCollection.models.forEach((location) => { data.locations.push(location.toJSON()); }); + // convert all "false" strings to false booleans in data. + Object.keys(data).forEach(key => { + if (typeof data[key] === 'string' && data[key].toLowerCase() === 'false') { + data[key] = false; + } + }); + // Serialize collects path from custom locations // This field must be removed from root object delete data.path; @@ -161,6 +200,7 @@ module.exports = Mn.View.extend({ // Manipulate data.forward_port = parseInt(data.forward_port, 10); data.block_exploits = !!data.block_exploits; + data.use_openappsec = !!data.use_openappsec; data.caching_enabled = !!data.caching_enabled; data.allow_websocket_upgrade = !!data.allow_websocket_upgrade; data.http2_support = !!data.http2_support; @@ -266,6 +306,7 @@ module.exports = Mn.View.extend({ this.ui.ssl_forced.trigger('change'); this.ui.hsts_enabled.trigger('change'); + this.ui.use_openappsec.trigger('change'); // Domain names this.ui.domain_names.selectize({ diff --git a/frontend/js/app/nginx/proxy/location-item.ejs b/frontend/js/app/nginx/proxy/location-item.ejs index 466cb9ba..c0866740 100644 --- a/frontend/js/app/nginx/proxy/location-item.ejs +++ b/frontend/js/app/nginx/proxy/location-item.ejs @@ -16,7 +16,7 @@
+
+
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+
<%- i18n('locations', 'delete') %> diff --git a/frontend/js/app/nginx/proxy/location.js b/frontend/js/app/nginx/proxy/location.js index e9513a48..6e1fe9ae 100644 --- a/frontend/js/app/nginx/proxy/location.js +++ b/frontend/js/app/nginx/proxy/location.js @@ -7,7 +7,10 @@ const LocationView = Mn.View.extend({ className: 'location_block', ui: { - toggle: 'input[type="checkbox"]', + use_openappsec: 'input[name="use_openappsec"]', + openappsec_mode: 'select[name="openappsec_mode"]', + minimum_confidence: 'select[name="minimum_confidence"]', + toggle: 'input[type="checkbox"]#advanced_config_toggle', config: '.config', delete: '.location-delete' }, @@ -21,6 +24,27 @@ const LocationView = Mn.View.extend({ } }, + 'change @ui.use_openappsec': function () { + let checked = this.ui.use_openappsec.prop('checked'); + this.model.set('use_openappsec', checked); + + this.ui.openappsec_mode + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + this.ui.minimum_confidence + .prop('disabled', !checked) + .parents('.form-group') + .css('opacity', checked ? 1 : 0.5); + + /*** check this */ + if (!checked) { + this.ui.openappsec_mode.prop('checked', false); + } + }, + + // input fields with the class 'model' will automatically update the model. 'change .model': function (e) { const map = {}; map[e.target.name] = e.target.value; diff --git a/frontend/js/app/nginx/redirection/form.js b/frontend/js/app/nginx/redirection/form.js index 1f81feeb..d14bfd3d 100644 --- a/frontend/js/app/nginx/redirection/form.js +++ b/frontend/js/app/nginx/redirection/form.js @@ -129,6 +129,7 @@ module.exports = Mn.View.extend({ // Manipulate data.block_exploits = !!data.block_exploits; + data.use_openappsec = !!data.use_openappsec; data.preserve_path = !!data.preserve_path; data.http2_support = !!data.http2_support; data.hsts_enabled = !!data.hsts_enabled; diff --git a/frontend/js/app/router.js b/frontend/js/app/router.js index a036bfc5..d005e582 100644 --- a/frontend/js/app/router.js +++ b/frontend/js/app/router.js @@ -13,6 +13,7 @@ module.exports = AppRouter.default.extend({ 'nginx/access': 'showNginxAccess', 'nginx/certificates': 'showNginxCertificates', 'audit-log': 'showAuditLog', + 'openappsec-log': 'showOpenappsecLog', 'settings': 'showSettings', '*default': 'showDashboard' } diff --git a/frontend/js/app/settings/main.ejs b/frontend/js/app/settings/main.ejs index 2b02769f..8dc37c9f 100644 --- a/frontend/js/app/settings/main.ejs +++ b/frontend/js/app/settings/main.ejs @@ -3,12 +3,55 @@

<%- i18n('settings', 'title') %>

-
+
+ + + +
+ + +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ + +
+ + + +
+
+ +
+ +
+
+ +
+
diff --git a/frontend/js/app/settings/main.js b/frontend/js/app/settings/main.js index 96b2941f..ea7b41c2 100644 --- a/frontend/js/app/settings/main.js +++ b/frontend/js/app/settings/main.js @@ -5,11 +5,18 @@ const ListView = require('./list/main'); const ErrorView = require('../error/main'); const template = require('./main.ejs'); +require('jquery-serializejson'); + module.exports = Mn.View.extend({ id: 'settings', template: template, ui: { + local_policy_field: '#open-appsec form #local_policy', + lp_success_info: '#open-appsec form #lp_success_info', + lp_error_info: '#open-appsec form #lp_error_info', + form: '#open-appsec form', + save: 'button.save', list_region: '.list-region', add: '.add-item', dimmer: '.dimmer' @@ -19,9 +26,57 @@ module.exports = Mn.View.extend({ list_region: '@ui.list_region' }, + events: { + 'click @ui.save': function (e) { + e.preventDefault(); + + this.ui.lp_success_info.hide(); + this.ui.lp_error_info.hide(); + + let data = this.ui.form.serializeJSON(); + console.log(data); + App.Api.OpenAppsecSettings.save(data) + .then(response => { + this.showSuccess(); + }) + .catch(err => { + console.error(err); + this.showError(err); + }); + } + }, + + showSuccess: function () { + this.ui.lp_success_info.show(); + setTimeout(() => { + this.ui.lp_success_info.fadeOut(); + }, 1000); + }, + + showError: function (err) { + this.ui.lp_error_info.show(); + this.ui.lp_error_info.html(err.message); + setTimeout(() => { + this.ui.lp_error_info.fadeOut(); + }, 3000); + }, + onRender: function () { let view = this; + this.ui.lp_success_info.hide(); + this.ui.lp_error_info.hide(); + + App.Api.OpenAppsecSettings.get() + .then(response => { + if (!view.isDestroyed() && response) { + view.ui.local_policy_field.val(response); + } + }) + .catch(err => { + console.error(err); + }); + App.Api.Settings.getAll() .then(response => { if (!view.isDestroyed() && response && response.length) { diff --git a/frontend/js/app/ui/header/main.ejs b/frontend/js/app/ui/header/main.ejs index 18ed2b6a..7467ba1c 100644 --- a/frontend/js/app/ui/header/main.ejs +++ b/frontend/js/app/ui/header/main.ejs @@ -6,6 +6,8 @@   <%- i18n('main', 'app') %> +
+ Secured by: open-appsec