diff --git a/backend/internal/nginx-openappsec.js b/backend/internal/nginx-openappsec.js new file mode 100755 index 00000000..a8806156 --- /dev/null +++ b/backend/internal/nginx-openappsec.js @@ -0,0 +1,255 @@ +const _ = require('lodash'); +const fs = require('fs'); +const logger = require('../logger').nginx; +const config = require('../lib/config'); +const yaml = require('js-yaml'); +const path = require('path'); +const constants = require('../lib/constants'); + +const internalNginxOpenappsec = { + + // module constants + CONFIG_TEMPLATE_FILE_NAME: 'local-policy-open-appsec-enabled-for-proxy-host.yaml', + CONFIG_TEMPLATE_DIR: '/app/templates', + + // module variables + config: null, + configTemplate: null, + + /** + * Generate an open-appsec config file for a proxy host. + * + * @param {Object} access + * @param {Object} row + * @param {Object} data + * @returns {Promise} + */ + generateConfig: (access, row, data) => { + return access.can('settings:update', row.id) + .then(() => { + if (config.debug()) { + logger.info('Generating openappsec config:', JSON.stringify(data, null, 2)); + } + + const openappsecMode = data.use_openappsec == false ? 'inactive' : data.openappsec_mode; + + const configTemplateFilePath = path.join(internalNginxOpenappsec.CONFIG_TEMPLATE_DIR, internalNginxOpenappsec.CONFIG_TEMPLATE_FILE_NAME) + const configFilePath = path.join(constants.APPSEC_EXT_DIR, constants.APPSEC_CONFIG_FILE_NAME); + + let openappsecConfig = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + let openappsecConfigTemplate = yaml.load(fs.readFileSync(configTemplateFilePath, 'utf8')); + + internalNginxOpenappsec.config = openappsecConfig; + internalNginxOpenappsec.configTemplate = openappsecConfigTemplate; + + const specificRuleName = 'npm-managed-specific-rule-proxyhost-' + row.id; + const logTriggerName = 'npm-managed-log-trigger-proxyhost-' + row.id; + const practiceName = 'npm-managed-practice-proxyhost-' + row.id; + + _.remove(openappsecConfig.policies['specific-rules'], rule => rule.name === specificRuleName || rule.name.startsWith(`${specificRuleName}.`)); + + data.domain_names.forEach((domain, index) => { + let ruleName = index > 0 ? `${specificRuleName}.${index}` : specificRuleName; + let specificRuleNode = { + host: domain, + name: ruleName, + triggers: [logTriggerName], + mode: openappsecMode, + practices: [practiceName] + }; + internalNginxOpenappsec.updateNode('policies', 'specific-rules', specificRuleNode, openappsecMode); + }); + + internalNginxOpenappsec.updateNode('', 'practices', { name: practiceName, 'web-attacks.override-mode': openappsecMode, 'web-attacks.minimum-confidence': data.minimum_confidence }, openappsecMode); + internalNginxOpenappsec.updateNode('', 'log-triggers', { name: logTriggerName }, openappsecMode); + + // remove all openappsec managed location config nodes for a proxy host. + let pattern = new RegExp(`^npm-managed.*-${row.id}-.*`); + internalNginxOpenappsec.removeMatchingNodes(openappsecConfig, pattern); + + // for each data.location, create location config nodes + data.locations.forEach((location, index) => { + let locationSpecificRuleName = 'npm-managed-specific-rule-proxyhost-' + row.id + '-' + index; + let locationLogTriggerName = 'npm-managed-log-trigger-proxyhost-' + row.id + '-' + index; + let locationPracticeName = 'npm-managed-practice-proxyhost-' + row.id + '-' + index; + + let locationOpenappsecMode = location.use_openappsec == false ? 'inactive' : location.openappsec_mode; + + _.remove(openappsecConfig.policies['specific-rules'], rule => rule.name === locationSpecificRuleName || rule.name.startsWith(`${locationSpecificRuleName}.`)); + + data.domain_names.forEach((domain, index) => { + let locationUrl = domain + location.path; + let ruleName = index > 0 ? `${locationSpecificRuleName}.${index}` : locationSpecificRuleName; + + let domainSpecificRuleNode = { + host: locationUrl, + name: ruleName, + triggers: [locationLogTriggerName], + mode: locationOpenappsecMode, + practices: [locationPracticeName] + }; + internalNginxOpenappsec.updateNode('policies', 'specific-rules', domainSpecificRuleNode, locationOpenappsecMode, 'location', openappsecMode); + }); + + internalNginxOpenappsec.updateNode('', 'practices', { name: locationPracticeName, 'web-attacks.override-mode': locationOpenappsecMode, 'web-attacks.minimum-confidence': location.minimum_confidence }, locationOpenappsecMode, 'location', openappsecMode); + internalNginxOpenappsec.updateNode('', 'log-triggers', { name: locationLogTriggerName }, locationOpenappsecMode, 'location', openappsecMode); + }); + + fs.writeFileSync(configFilePath, yaml.dump(openappsecConfig)); + }, + (err) => { + logger.error('Error generating openappsec config:', err); + return Promise.reject(err); + }); + }, + + /** + * Remove all openappsec managed config nodes for a proxy host. + * + * @param {Object} access + * @param {Object} row + * @returns {Promise} + * + */ + deleteConfig: (access, row) => { + return access.can('settings:update', row.id) + .then(() => { + const configFilePath = path.join(constants.APPSEC_EXT_DIR, constants.APPSEC_CONFIG_FILE_NAME); + let openappsecConfig = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + + // remove all openappsec managed location config nodes for a proxy host. + let pattern = new RegExp(`^npm-managed.*-${row.id}`); + internalNginxOpenappsec.removeMatchingNodes(openappsecConfig, pattern); + fs.writeFileSync(configFilePath, yaml.dump(openappsecConfig)); + }) + .catch(err => { + logger.error('Error deleting openappsec config:', err); + return Promise.reject(err); + }); + }, + + /** + * Update a node in the openappsec config. + * - if the node does not exist, create it. + * - if the node exists, update it. + * - if openappsecMode is 'inactive', delete the node. + * + * @param {String} parentNodePath - path to the parent node. e.g. 'policies'. + * @param {String} nodeName - name of the node. e.g. 'specific-rules', 'practices', 'log-triggers'. + * @param {Object} nodeItemProperties + * @param {String} openappsecMode + * @param {String} nodeType - 'host' or 'location' + * @param {String} hostAppsecMode - to check if the host of a location is inactive. + */ + updateNode: function (parentNodePath, nodeName, nodeItemProperties, openappsecMode, nodeType = 'host', hostAppsecMode = '') { + // if no parent node path is specified, use the root of the config object. + const parent = parentNodePath ? _.get(this.config, parentNodePath, this.config) : this.config; + + if (!parent) { + console.log('parent is not defined'); + return; + } + + let nodeItems = _.find(parent[nodeName], { name: nodeItemProperties.name }); + if (openappsecMode == 'inactive' && nodeItems) { + _.remove(parent[nodeName], { name: nodeItemProperties.name }); + } + + if (openappsecMode !== 'inactive' || nodeType === 'location' && hostAppsecMode !== 'inactive') { + if (!nodeItems) { + // create the node from the template if it does not exist. + let templateSearchPath = parentNodePath ? `${parentNodePath}.${nodeName}[0]` : `${nodeName}[0]`; + nodeItems = _.cloneDeep(_.get(this.configTemplate, templateSearchPath)); + + // update the node with the nodeItemProperties. if the nodeType is 'location' and the openappsecMode is 'inactive', only update the name, host, and the (inactive) mode. + if (nodeType === 'location' && openappsecMode === 'inactive') { + nodeItemProperties = _.pick(nodeItemProperties, ['name', 'host', 'triggers', 'practices', 'mode', 'web-attacks.override-mode']); + } + + Object.keys(nodeItemProperties).forEach(key => { + _.set(nodeItems, key, nodeItemProperties[key]); + }); + parent[nodeName] = parent[nodeName] || []; + parent[nodeName].push(nodeItems); + } else { + // update the node if it exists. + Object.keys(nodeItemProperties).forEach(key => { + _.set(nodeItems, key, nodeItemProperties[key]); + }); + } + } + }, + + /** + * Recursively removes nodes from a JavaScript object based on a pattern. + * + * @param {Object|Array} obj - The object or array to remove nodes from. + * @param {RegExp} pattern - The pattern to match against node names. + */ + removeMatchingNodes: function (obj, pattern) { + _.forEach(obj, (value, key) => { + if (_.isPlainObject(value)) { + if (pattern.test(key)) { + delete obj[key]; + } else { + this.removeMatchingNodes(value, pattern); + } + } else if (_.isArray(value)) { + _.remove(value, function (item) { + return _.isPlainObject(item) && pattern.test(item.name); + }); + value.forEach(item => { + if (_.isPlainObject(item)) { + this.removeMatchingNodes(item, pattern); + } + }); + } + }); + }, + + /** + * Get the openappsec mode, use_openappsec and minimum_confidence for a proxy host. + * + * @param {Object} openappsecConfig - openappsec config object + * @param {Number} rowId - proxy host id + * @returns {Object} { mode, use_openappsec, minimum_confidence } + */ + getOpenappsecFields: (openappsecConfig, rowId) => { + const specificRuleName = 'npm-managed-specific-rule-proxyhost-' + rowId; + + const specificRule = _.find(openappsecConfig?.policies['specific-rules'], { name: specificRuleName }); + const mode = specificRule?.mode || 'inactive'; + const use_openappsec = mode !== 'inactive' && mode !== undefined; + + const practiceName = 'npm-managed-practice-proxyhost-' + rowId; + const practice = _.find(openappsecConfig?.practices, { name: practiceName }); + const minimum_confidence = practice?.['web-attacks']['minimum-confidence'] || 'high'; + + return { mode, use_openappsec, minimum_confidence }; + }, + + /** + * get the openappsec config file path. + */ + getConfigFilePath: () => { + const configFilePath = path.join(constants.APPSEC_EXT_DIR, constants.APPSEC_CONFIG_FILE_NAME); + return configFilePath; + }, + + /** + * A simple wrapper around unlinkSync that writes to the logger + * + * @param {String} filename + */ + deleteFile: (filename) => { + logger.debug('Deleting file: ' + filename); + try { + fs.unlinkSync(filename); + } catch (err) { + logger.debug('Could not delete file:', JSON.stringify(err, null, 2)); + } + } + +}; + +module.exports = internalNginxOpenappsec; \ No newline at end of file diff --git a/backend/internal/openappsec-log.js b/backend/internal/openappsec-log.js new file mode 100755 index 00000000..a695826a --- /dev/null +++ b/backend/internal/openappsec-log.js @@ -0,0 +1,89 @@ +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const error = require('../lib/error'); +const { APPSEC_LOG_DIR } = require('../lib/constants'); + +const internalOpenappsecLog = { + + /** + * All logs + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access.can('auditlog:list') + .then(() => { + + const directoryPath = APPSEC_LOG_DIR; + + const readdir = util.promisify(fs.readdir); + const readFile = util.promisify(fs.readFile); + + async function listLogFiles(dir) { + const files = await readdir(dir); + const logFiles = files.filter(file => path.extname(file).startsWith('.log')); + + const sortedLogFiles = logFiles.sort((a, b) => { + const baseA = path.basename(a, path.extname(a)); + const baseB = path.basename(b, path.extname(b)); + + if (baseA < baseB) return -1; + if (baseA > baseB) return 1; + + return path.extname(a).localeCompare(path.extname(b)); + }); + + const groupedFiles = sortedLogFiles.reduce((groups, file) => { + const fileName = path.basename(file, path.extname(file)); + if (!groups[fileName]) { + groups[fileName] = []; + } + groups[fileName].push(file); + return groups; + }, {}); + + const wrappedObjects = []; + + for (const [groupName, files] of Object.entries(groupedFiles)) { + for (const file of files) { + try { + const content = await readFile(path.join(dir, file), 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + try { + const json = JSON.parse(line); + const wrappedObject = { + source: groupName, + meta: json, + serviceName: json.eventSource.serviceName, + eventPriority: json.eventPriority, + eventSeverity: json.eventSeverity, + eventLevel: json.eventLevel, + eventTime: json.eventTime, + eventName: json.eventName + }; + wrappedObjects.push(wrappedObject); + } catch (err) { + // Ignore lines that don't contain JSON data + } + } + } catch (err) { + console.error(`Failed to read file ${file}: ${err.message}`); + } + } + } + wrappedObjects.sort((a, b) => new Date(b.eventTime) - new Date(a.eventTime)); + return wrappedObjects; + } + + let groupedFiles = listLogFiles(directoryPath).catch(console.error); + return groupedFiles; + }); + } +}; + +module.exports = internalOpenappsecLog; diff --git a/backend/internal/setting-openappsec.js b/backend/internal/setting-openappsec.js new file mode 100755 index 00000000..e009bc9f --- /dev/null +++ b/backend/internal/setting-openappsec.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const error = require('../lib/error'); +const path = require('path'); + +const constants = require('../lib/constants'); + +const internalOpenappsecSetting = { + configFilePath: path.join(constants.APPSEC_EXT_DIR, constants.APPSEC_CONFIG_FILE_NAME), + + /** + * @param {Access} access + * @return {Promise} + */ + getLocalPolicy: (access) => { + return access.can('settings:list') + .then(() => { + try { + const filePath = internalOpenappsecSetting.configFilePath + if (!fs.existsSync(filePath)) { + return; + } + const fileContent = fs.readFileSync(filePath, 'utf8'); + const jsonStr = JSON.stringify(fileContent); + return jsonStr; + + } catch (err) { + console.error(err); + } + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + updateLocalPolicy: (access, data) => { + return access.can('settings:list') + .then(() => { + const filePath = internalOpenappsecSetting.configFilePath + const yamlStr = data.local_policy; + fs.writeFileSync(filePath, yamlStr, {encoding: 'utf8'}); + return true; + }) + .catch((err) => { + console.error(err); + throw new error.ConfigurationError(err.message); + }); + } +}; + +module.exports = internalOpenappsecSetting; diff --git a/backend/lib/constants.js b/backend/lib/constants.js new file mode 100755 index 00000000..d41a6343 --- /dev/null +++ b/backend/lib/constants.js @@ -0,0 +1,5 @@ +module.exports = { + APPSEC_CONFIG_FILE_NAME: 'local_policy.yaml', + APPSEC_EXT_DIR: '/ext/appsec', + APPSEC_LOG_DIR: '/ext/appsec-logs', +}; \ No newline at end of file diff --git a/backend/routes/api/openappsec-log.js b/backend/routes/api/openappsec-log.js new file mode 100755 index 00000000..aa70d48d --- /dev/null +++ b/backend/routes/api/openappsec-log.js @@ -0,0 +1,35 @@ +const express = require('express'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const internalOpenappsecLog = require('../../internal/openappsec-log'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/openappsec-log + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/openappsec-log + * + * Retrieve all logs + */ + .get((req, res, next) => { + return internalOpenappsecLog.getAll(res.locals.access) + .then((policy) => { + res.status(200) + .send(policy); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/routes/api/openappsec-settings.js b/backend/routes/api/openappsec-settings.js new file mode 100755 index 00000000..dcda2333 --- /dev/null +++ b/backend/routes/api/openappsec-settings.js @@ -0,0 +1,49 @@ +const express = require('express'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const internalOpenappsecSetting = require('../../internal/setting-openappsec'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/openappsec-settings + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/openappsec-settings + * + * Retrieve the open-appsec local policy. + */ + .get((req, res, next) => { + return internalOpenappsecSetting.getLocalPolicy(res.locals.access) + .then((policy) => { + res.status(200) + .send(policy); + }) + .catch(next); + }) + + /** + * PUT /api/openappsec-settings + * + * Update the open-appsec local policy. + */ + .put((req, res, next) => { + return internalOpenappsecSetting.updateLocalPolicy(res.locals.access, req.body) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/templates/local-policy-open-appsec-enabled-for-proxy-host.yaml b/backend/templates/local-policy-open-appsec-enabled-for-proxy-host.yaml new file mode 100755 index 00000000..f4e1ef19 --- /dev/null +++ b/backend/templates/local-policy-open-appsec-enabled-for-proxy-host.yaml @@ -0,0 +1,121 @@ +# This example is for NPM Proxy Host with open-appsec enabled, +# Enforcement Mode set to Prevent/Learn, hostname web.server.com/example + +policies: + default: + triggers: + - appsec-default-log-trigger + mode: inactive + practices: + - webapp-default-practice + custom-response: appsec-default-web-user-response + specific-rules: + - host: web.server.com/example + # as set in "Edit Proxy Host" in "Domain Names" field + # IMPORTANT LIMITATION: Currently open-appsec declarative with CRD version 1.0 only supports single host entry per specific rule + # This will be resolved with new CRDs 2.0 + name: npm-managed-specific-rule-proxyhost-1 + # This “name” key will be the actual reference to a specific Reverse Proxy object defined in NPM + triggers: + - npm-managed-log-trigger-proxyhost-1 + mode: prevent-learn + practices: + - npm-managed-practice-proxyhost-1 + +practices: + - name: webapp-default-practice + web-attacks: + max-body-size-kb: 1000000 + max-header-size-bytes: 102400 + max-object-depth: 40 + max-url-size-bytes: 32768 + minimum-confidence: high + override-mode: inactive + protections: + csrf-protection: inactive + error-disclosure: inactive + non-valid-http-methods: false + open-redirect: inactive + anti-bot: + injected-URIs: [] + validated-URIs: [] + override-mode: inactive + snort-signatures: + configmap: [] + override-mode: inactive + openapi-schema-validation: + configmap: [] + override-mode: inactive + + - name: npm-managed-practice-proxyhost-1 + web-attacks: + max-body-size-kb: 1000000 + max-header-size-bytes: 102400 + max-object-depth: 40 + max-url-size-bytes: 32768 + minimum-confidence: high + override-mode: inactive + protections: + csrf-protection: inactive + error-disclosure: inactive + non-valid-http-methods: false + open-redirect: inactive + anti-bot: + injected-URIs: [] + validated-URIs: [] + override-mode: inactive + snort-signatures: + configmap: [] + override-mode: inactive + openapi-schema-validation: + configmap: [] + override-mode: inactive + +log-triggers: + - name: appsec-default-log-trigger + access-control-logging: + allow-events: false + drop-events: true + additional-suspicious-events-logging: + enabled: true + minimum-severity: high + response-body: false + appsec-logging: + all-web-requests: false + detect-events: true + prevent-events: true + extended-logging: + http-headers: false + request-body: false + url-path: false + url-query: false + log-destination: + cloud: false + stdout: + format: json + - name: npm-managed-log-trigger-proxyhost-1 + access-control-logging: + allow-events: false + drop-events: true + additional-suspicious-events-logging: + enabled: true + minimum-severity: high + response-body: false + appsec-logging: + all-web-requests: false + detect-events: true + prevent-events: true + extended-logging: + http-headers: false + request-body: false + url-path: false + url-query: false + log-destination: + cloud: false + stdout: + format: json + +custom-responses: + - name: appsec-default-web-user-response + mode: response-code-only + http-response-code: 403 \ No newline at end of file diff --git a/backend/templates/openappsec.conf b/backend/templates/openappsec.conf new file mode 100755 index 00000000..bc350cab --- /dev/null +++ b/backend/templates/openappsec.conf @@ -0,0 +1,16 @@ +policies: + default: + triggers: + - appsec-default-log-trigger + mode: inactive + practices: + - webapp-default-practice + custom-response: appsec-default-web-user-response + specific-rules: + - host: {{ domain_names.first }} + triggers: + - appsec-default-log-trigger + mode: {{openappsec_mode}} + practices: + - webapp-default-practice + custom-response: appsec-default-web-user-response \ No newline at end of file diff --git a/frontend/app-images/open-appsec-logo.svg b/frontend/app-images/open-appsec-logo.svg new file mode 100755 index 00000000..57b41551 --- /dev/null +++ b/frontend/app-images/open-appsec-logo.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + diff --git a/frontend/js/app/openappsec-log/list/item.ejs b/frontend/js/app/openappsec-log/list/item.ejs new file mode 100755 index 00000000..57c4feab --- /dev/null +++ b/frontend/js/app/openappsec-log/list/item.ejs @@ -0,0 +1,77 @@ + + +
+ <% var sevirityClass = 'bg-success'; + switch (eventSeverity) { + case 'Critical': + sevirityClass = 'bg-danger'; + break; + case 'Warning': + sevirityClass = 'bg-warning'; + break; + case 'Info': + sevirityClass = 'bg-success'; + //sevirityClass = 'bg-info'; + break; + case 'Debug': + sevirityClass = 'bg-success'; + break; + } + %> + <%- eventSeverity %> +
+ + +
+ <% + var items = []; + object_type = "test"; + switch (object_type) { + case 'proxy-host': + %> <% + items = meta.domain_names; + break; + case 'redirection-host': + %> <% + items = meta.domain_names; + break; + case 'stream': + %> <% + items.push(meta.incoming_port); + break; + case 'dead-host': + %> <% + items = meta.domain_names; + break; + case 'access-list': + %> <% + items.push(meta.name); + break; + case 'user': + %> <% + items.push(meta.name); + break; + case 'certificate': + %> <% + if (meta.provider === 'letsencrypt') { + items = meta.domain_names; + } else { + //items.push(meta.nice_name); + items.push("test"); + } + break; + } + %><%- eventName %> +
+
+ <%- formatDbDate(eventTime, 'Do MMMM YYYY, h:mm a') %> +
+ + +
+ <%- serviceName %> +
+ + + open + diff --git a/frontend/js/app/openappsec-log/list/item.js b/frontend/js/app/openappsec-log/list/item.js new file mode 100755 index 00000000..009e3a3e --- /dev/null +++ b/frontend/js/app/openappsec-log/list/item.js @@ -0,0 +1,32 @@ +const Mn = require('backbone.marionette'); +const Controller = require('../../controller'); +const template = require('./item.ejs'); + +module.exports = Mn.View.extend({ + template: template, + tagName: 'tr', + + ui: { + meta: 'a.meta' + }, + + events: { + 'click @ui.meta': function (e) { + e.preventDefault(); + Controller.showOpenappsecMeta(this.model); + } + }, + + templateContext: { + more: function() { + switch (this.object_type) { + case 'redirection-host': + case 'stream': + case 'proxy-host': + return this.meta.domain_names.join(', '); + } + + return '#' + (this.object_id || '?'); + } + } +}); diff --git a/frontend/js/app/openappsec-log/list/main.ejs b/frontend/js/app/openappsec-log/list/main.ejs new file mode 100755 index 00000000..0bb40fa1 --- /dev/null +++ b/frontend/js/app/openappsec-log/list/main.ejs @@ -0,0 +1,9 @@ + + Severity + Event + Handler +   + + + + diff --git a/frontend/js/app/openappsec-log/list/main.js b/frontend/js/app/openappsec-log/list/main.js new file mode 100755 index 00000000..9d3e26fb --- /dev/null +++ b/frontend/js/app/openappsec-log/list/main.js @@ -0,0 +1,27 @@ +const Mn = require('backbone.marionette'); +const ItemView = require('./item'); +const template = require('./main.ejs'); + +const TableBody = Mn.CollectionView.extend({ + tagName: 'tbody', + childView: ItemView +}); + +module.exports = Mn.View.extend({ + tagName: 'table', + className: 'table table-hover table-outline table-vcenter card-table', + template: template, + + regions: { + body: { + el: 'tbody', + replaceElement: true + } + }, + + onRender: function () { + this.showChildView('body', new TableBody({ + collection: this.collection + })); + } +}); diff --git a/frontend/js/app/openappsec-log/main.ejs b/frontend/js/app/openappsec-log/main.ejs new file mode 100755 index 00000000..ed36305a --- /dev/null +++ b/frontend/js/app/openappsec-log/main.ejs @@ -0,0 +1,25 @@ +
+
+
+

Security Log

+
+ +
+
+
+
+
+
+ +
+
+ +
+
diff --git a/frontend/js/app/openappsec-log/main.js b/frontend/js/app/openappsec-log/main.js new file mode 100755 index 00000000..356a8646 --- /dev/null +++ b/frontend/js/app/openappsec-log/main.js @@ -0,0 +1,82 @@ +const Mn = require('backbone.marionette'); +const App = require('../main'); +const OpenappsecLogModel = require('../../models/openappsec-log'); +const ListView = require('./list/main'); +const template = require('./main.ejs'); +const ErrorView = require('../error/main'); +const EmptyView = require('../empty/main'); + +module.exports = Mn.View.extend({ + id: 'openappsec-log', + template: template, + + ui: { + list_region: '.list-region', + dimmer: '.dimmer', + search: '.search-form', + query: 'input[name="source-query"]' + }, + + fetch: App.Api.OpenappsecLog.getAll, + + showData: function(response) { + this.showChildView('list_region', new ListView({ + collection: new OpenappsecLogModel.Collection(response) + })); + }, + + showError: function(err) { + this.showChildView('list_region', new ErrorView({ + code: err.code, + message: err.message, + retry: function () { + App.Controller.showOpenappsecLog(); + } + })); + + console.error(err); + }, + + showEmpty: function() { + this.showChildView('list_region', new EmptyView({ + title: App.i18n('audit-log', 'empty'), + subtitle: App.i18n('audit-log', 'empty-subtitle') + })); + }, + + regions: { + list_region: '@ui.list_region' + }, + + events: { + 'submit @ui.search': function (e) { + e.preventDefault(); + let query = this.ui.query.val(); + + this.fetch(['user'], query) + .then(response => this.showData(response)) + .catch(err => { + this.showError(err); + }); + } + }, + + onRender: function () { + let view = this; + + view.fetch(['user']) + .then(response => { + if (!view.isDestroyed() && response && response.length) { + view.showData(response); + } else { + view.showEmpty(); + } + }) + .catch(err => { + view.showError(err); + }) + .then(() => { + view.ui.dimmer.removeClass('active'); + }); + } +}); diff --git a/frontend/js/app/openappsec-log/meta.ejs b/frontend/js/app/openappsec-log/meta.ejs new file mode 100755 index 00000000..fe583117 --- /dev/null +++ b/frontend/js/app/openappsec-log/meta.ejs @@ -0,0 +1,27 @@ + diff --git a/frontend/js/app/openappsec-log/meta.js b/frontend/js/app/openappsec-log/meta.js new file mode 100755 index 00000000..815cdfac --- /dev/null +++ b/frontend/js/app/openappsec-log/meta.js @@ -0,0 +1,7 @@ +const Mn = require('backbone.marionette'); +const template = require('./meta.ejs'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog wide' +}); diff --git a/frontend/js/models/openappsec-log.js b/frontend/js/models/openappsec-log.js new file mode 100755 index 00000000..c929a0bd --- /dev/null +++ b/frontend/js/models/openappsec-log.js @@ -0,0 +1,18 @@ +const Backbone = require('backbone'); + +const model = Backbone.Model.extend({ + idAttribute: 'id', + + defaults: function () { + return { + name: '' + }; + } +}); + +module.exports = { + Model: model, + Collection: Backbone.Collection.extend({ + model: model + }) +};