mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-07 09:53:38 +00:00
Supporting open-appsec
This commit is contained in:
255
backend/internal/nginx-openappsec.js
Executable file
255
backend/internal/nginx-openappsec.js
Executable file
@@ -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;
|
89
backend/internal/openappsec-log.js
Executable file
89
backend/internal/openappsec-log.js
Executable file
@@ -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;
|
52
backend/internal/setting-openappsec.js
Executable file
52
backend/internal/setting-openappsec.js
Executable file
@@ -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;
|
5
backend/lib/constants.js
Executable file
5
backend/lib/constants.js
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
APPSEC_CONFIG_FILE_NAME: 'local_policy.yaml',
|
||||||
|
APPSEC_EXT_DIR: '/ext/appsec',
|
||||||
|
APPSEC_LOG_DIR: '/ext/appsec-logs',
|
||||||
|
};
|
35
backend/routes/api/openappsec-log.js
Executable file
35
backend/routes/api/openappsec-log.js
Executable file
@@ -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;
|
49
backend/routes/api/openappsec-settings.js
Executable file
49
backend/routes/api/openappsec-settings.js
Executable file
@@ -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;
|
121
backend/templates/local-policy-open-appsec-enabled-for-proxy-host.yaml
Executable file
121
backend/templates/local-policy-open-appsec-enabled-for-proxy-host.yaml
Executable file
@@ -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
|
16
backend/templates/openappsec.conf
Executable file
16
backend/templates/openappsec.conf
Executable file
@@ -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
|
68
frontend/app-images/open-appsec-logo.svg
Executable file
68
frontend/app-images/open-appsec-logo.svg
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="873.41901"
|
||||||
|
height="838.49463"
|
||||||
|
viewBox="0 0 873.41901 838.49463"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
sodipodi:docname="open-appsec-logo.svg"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.27439024"
|
||||||
|
inkscape:cx="1568.9333"
|
||||||
|
inkscape:cy="351.68889"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="979"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg2" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<clipPath
|
||||||
|
id="clip-Without_CP">
|
||||||
|
<rect
|
||||||
|
width="3444"
|
||||||
|
height="979"
|
||||||
|
id="rect1"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="Group_1_copy-2"
|
||||||
|
data-name="Group 1 copy"
|
||||||
|
transform="translate(-16.001,-12)">
|
||||||
|
<path
|
||||||
|
id="Shape_3"
|
||||||
|
data-name="Shape 3"
|
||||||
|
d="m 943.882,813.9 c 0,0 -76.84,12.621 -141.535,-1.855 -63.8,-14.276 -115.462,-55.652 -115.462,-55.652 0,0 -87.913,63.539 -176.918,63.072 C 422.06,819 333.048,754.536 333.048,754.536 c 0,0 -41.188,37.694 -102.426,51.942 -66.14,15.389 -152.708,7.42 -152.708,7.42 v 77.913 c 0,0 83.521,12.22 154.57,0 56.763,-9.763 104.289,-44.522 104.289,-44.522 0,0 68.9,50.51 176.918,50.087 95.062,-0.372 171.332,-51.942 171.332,-51.942 0,0 54.106,35.641 113.6,46.377 68.383,12.34 143.4,0 143.4,0 z"
|
||||||
|
transform="translate(-61.913,-46.884)"
|
||||||
|
fill="#ff0b05" />
|
||||||
|
<path
|
||||||
|
id="Shape_2"
|
||||||
|
data-name="Shape 2"
|
||||||
|
d="m 727.624,274.073 c 0,0 21.518,103.827 16.76,192.927 -4.549,85.193 -37.246,157.681 -37.246,157.681 0,0 76.431,26.129 121.05,61.219 43.834,34.471 57.731,76.058 57.731,76.058 0,0 -53.385,9.1 -102.427,-9.275 -51.955,-19.463 -98.7,-55.652 -98.7,-55.652 0,0 -79.692,64.725 -175.057,63.069 C 409,758.354 342.128,697.029 342.128,697.029 c 0,0 -54.86,40.153 -111.738,57.507 -45.1,13.761 -93.115,7.42 -93.115,7.42 0,0 38.01,-96.083 169.469,-133.565 64.982,-18.528 154.034,-27.082 225.338,-25.971 66.292,1.033 117.325,11.13 117.325,11.13 0,0 44.715,-82.589 57.731,-176.232 12.091,-86.981 -14.9,-181.8 -14.9,-181.8 z"
|
||||||
|
transform="translate(-61.682,-46.884)"
|
||||||
|
fill="#8b1548" />
|
||||||
|
<path
|
||||||
|
id="Shape_1"
|
||||||
|
data-name="Shape 1"
|
||||||
|
d="m 730.221,146.072 c 0,0 -28.435,-37.192 -67.042,-59.362 -40.495,-23.253 -89.39,-27.826 -89.39,-27.826 0,0 43.638,22.244 67.043,44.522 17.685,16.834 14.9,31.536 14.9,31.536 0,0 -56.372,-17.173 -109.876,7.42 -103.573,47.607 -119.189,118.725 -119.189,118.725 0,0 48.89,-43.93 85.666,-61.217 45.218,-21.256 76.354,-14.841 76.354,-14.841 a 9.365,9.365 0 0 1 9.311,3.71 c 5.734,7.85 -1.862,16.7 -1.862,16.7 0,0 -61.447,49.466 -72.63,105.739 -16.1,81.044 26.072,170.667 26.072,170.667 0,0 -19.194,-84.193 20.485,-153.971 16.627,-29.239 47.676,-63.819 74.492,-83.478 30.46,-22.331 55.869,-27.826 55.869,-27.826 0,0 36.166,6.686 68.905,55.652 12.261,18.339 26.846,42.126 31.659,66.783 11.488,58.848 1.862,133.565 1.862,133.565 0,0 41.487,-47.813 44.7,-128 2.718,-67.941 -37.246,-113.159 -37.246,-113.159 0,0 -10.414,-12.34 -9.581,-17.736 1.012,-6.55 13.305,-6.38 13.305,-6.38 a 226.339,226.339 0 0 1 59.593,25.971 c 37.294,23.409 76.354,59.362 76.354,59.362 0,0 -31.733,-74.906 -96.839,-116.87 -38.15,-24.592 -122.915,-29.686 -122.915,-29.686 z"
|
||||||
|
transform="translate(-60.555,-46.884)"
|
||||||
|
fill="#ff2885" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
77
frontend/js/app/openappsec-log/list/item.ejs
Executable file
77
frontend/js/app/openappsec-log/list/item.ejs
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
<td>
|
||||||
|
<div <div class="text-nowrap">
|
||||||
|
<% 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;
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
<span class="status-icon <%- sevirityClass %>"></span> <%- eventSeverity %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<%
|
||||||
|
var items = [];
|
||||||
|
object_type = "test";
|
||||||
|
switch (object_type) {
|
||||||
|
case 'proxy-host':
|
||||||
|
%> <span class="text-success"><i class="fe fe-zap"></i></span> <%
|
||||||
|
items = meta.domain_names;
|
||||||
|
break;
|
||||||
|
case 'redirection-host':
|
||||||
|
%> <span class="text-yellow"><i class="fe fe-shuffle"></i></span> <%
|
||||||
|
items = meta.domain_names;
|
||||||
|
break;
|
||||||
|
case 'stream':
|
||||||
|
%> <span class="text-blue"><i class="fe fe-radio"></i></span> <%
|
||||||
|
items.push(meta.incoming_port);
|
||||||
|
break;
|
||||||
|
case 'dead-host':
|
||||||
|
%> <span class="text-danger"><i class="fe fe-zap-off"></i></span> <%
|
||||||
|
items = meta.domain_names;
|
||||||
|
break;
|
||||||
|
case 'access-list':
|
||||||
|
%> <span class="text-teal"><i class="fe fe-lock"></i></span> <%
|
||||||
|
items.push(meta.name);
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
%> <span class="text-teal"><i class="fe fe-user"></i></span> <%
|
||||||
|
items.push(meta.name);
|
||||||
|
break;
|
||||||
|
case 'certificate':
|
||||||
|
%> <span class="text-pink"><i class="fe fe-shield"></i></span> <%
|
||||||
|
if (meta.provider === 'letsencrypt') {
|
||||||
|
items = meta.domain_names;
|
||||||
|
} else {
|
||||||
|
//items.push(meta.nice_name);
|
||||||
|
items.push("test");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
%><%- eventName %>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<%- formatDbDate(eventTime, 'Do MMMM YYYY, h:mm a') %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<%- serviceName %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a href="#" class="meta btn btn-secondary btn-sm">open</a>
|
||||||
|
</td>
|
32
frontend/js/app/openappsec-log/list/item.js
Executable file
32
frontend/js/app/openappsec-log/list/item.js
Executable file
@@ -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 || '?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
9
frontend/js/app/openappsec-log/list/main.ejs
Executable file
9
frontend/js/app/openappsec-log/list/main.ejs
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
<thead>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Handler</th>
|
||||||
|
<th> </th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- items -->
|
||||||
|
</tbody>
|
27
frontend/js/app/openappsec-log/list/main.js
Executable file
27
frontend/js/app/openappsec-log/list/main.js
Executable file
@@ -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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
25
frontend/js/app/openappsec-log/main.ejs
Executable file
25
frontend/js/app/openappsec-log/main.ejs
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="card">
|
||||||
|
<div class="card-status bg-teal"></div>
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Security Log</h3>
|
||||||
|
<div class="card-options">
|
||||||
|
<form class="search-form" role="search">
|
||||||
|
<div class="input-icon">
|
||||||
|
<span class="input-icon-addon">
|
||||||
|
<i class="fe fe-search"></i>
|
||||||
|
</span>
|
||||||
|
<input name="source-query" type="text" value="" class="form-control form-control-sm" placeholder="<%- i18n('audit-log', 'search') %>" aria-label="<%- i18n('audit-log', 'search') %>">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body no-padding min-100">
|
||||||
|
<div class="dimmer active">
|
||||||
|
<div class="loader"></div>
|
||||||
|
<div class="dimmer-content list-region">
|
||||||
|
<!-- List Region -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
82
frontend/js/app/openappsec-log/main.js
Executable file
82
frontend/js/app/openappsec-log/main.js
Executable file
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
27
frontend/js/app/openappsec-log/meta.ejs
Executable file
27
frontend/js/app/openappsec-log/meta.ejs
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><%- eventName %></h5>
|
||||||
|
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="tag tag-dark">
|
||||||
|
Handler
|
||||||
|
<span class="tag-addon tag-orange"><%- serviceName %></span>
|
||||||
|
</div>
|
||||||
|
<div class="tag tag-dark">
|
||||||
|
Severity
|
||||||
|
<span class="tag-addon tag-teal"><%- eventSeverity %></span>
|
||||||
|
</div>
|
||||||
|
<div class="tag tag-dark">
|
||||||
|
<%- i18n('audit-log', 'date') %>
|
||||||
|
<span class="tag-addon tag-primary"><%- formatDbDate(eventTime, 'Do MMMM YYYY, h:mm a') %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><%- JSON.stringify(meta, null, 2) %></pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'close') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
7
frontend/js/app/openappsec-log/meta.js
Executable file
7
frontend/js/app/openappsec-log/meta.js
Executable file
@@ -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'
|
||||||
|
});
|
18
frontend/js/models/openappsec-log.js
Executable file
18
frontend/js/models/openappsec-log.js
Executable file
@@ -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
|
||||||
|
})
|
||||||
|
};
|
Reference in New Issue
Block a user