Skip to content

Centrally managed #3680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 58 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
81a3895
Update README.md
roybarda Dec 6, 2023
f9ae99e
first open-appsec support
roybarda Dec 6, 2023
41b579d
Supporting open-appsec
roybarda Dec 7, 2023
b66497a
open-appsec-addition
roybarda Dec 7, 2023
01b9edf
Create docker-compose.yaml
orianelou Dec 12, 2023
a856a0c
Create local_policy.yaml
orianelou Dec 13, 2023
3741c52
Update shared volumes config
ramiwi Dec 12, 2023
80deae9
Merge branch 'client-side-log-pagination' into open-appsec-integration
ramiwi Dec 12, 2023
dbd78e5
Filtered log views
ramiwi Dec 13, 2023
ce84a86
Notify policy updates to open-appsec container.
ramiwi Dec 13, 2023
1d81a55
Merge pull request #1 from openappsec/filtered-logs
roybarda Dec 14, 2023
c0171fe
Update log table, login screen UI and package versions
ramiwi Dec 14, 2023
f90f5ed
login screen package version fix
ramiwi Dec 15, 2023
3ae059e
log fields and logo fixes
ramiwi Dec 15, 2023
1c493ea
Update Dockerfile
clutat Dec 18, 2023
84ee15f
Update README.md
clutat Dec 18, 2023
f2564e9
Update README.md
clutat Dec 18, 2023
2a3348e
Update README.md
clutat Dec 18, 2023
256a750
Update docker-compose.yaml - work on initial version
clutat Dec 18, 2023
8631791
wide page with sticky table heads.
ramiwi Dec 18, 2023
69f0ceb
Update API endpoint for applying policy
ramiwi Dec 18, 2023
b105a2c
Add Docker Compose file and local policy configuration
ramiwi Dec 18, 2023
36a0a17
Update README.md - fix directory path in compilation instructions
clutat Dec 19, 2023
24d1e53
Update README.md - fix mkdir docker/lib
clutat Dec 19, 2023
a909cd1
Update local_policy.yaml - adjust default log-trigger
clutat Dec 19, 2023
bea44ab
Update README.md - various updates
clutat Dec 19, 2023
cbf282f
Update UI styles in header, main, and menu components
ramiwi Dec 19, 2023
b22d683
Merge branch 'open-appsec-addition' of https://github.com/openappsec/…
ramiwi Dec 19, 2023
a49e2bc
Sort tab data by timestamp in descending order
ramiwi Dec 19, 2023
1f96171
Update curl command for setting policy
ramiwi Dec 19, 2023
c6c7319
policy path moved to globals
ramiwi Dec 19, 2023
4a8f6b3
Update README.md - add detached -d to docker compose command
clutat Dec 20, 2023
f915c08
Update README.md - various text adjustments
clutat Dec 20, 2023
d34478c
Update README.md
orianelou Dec 20, 2023
b13d8e0
ignore appsec policy update notification command error
ramiwi Dec 20, 2023
ca10cc4
Merge branch 'open-appsec-addition' of https://github.com/openappsec/…
ramiwi Dec 20, 2023
8a0825f
Update README.md - added default admin login info
clutat Dec 20, 2023
9730109
Update README.md - minor formatting changes
clutat Dec 20, 2023
6a3092b
Update README.md
orianelou Dec 20, 2023
f43b024
Fixing version for first release
roybarda Dec 20, 2023
4c25b81
Update README.md
orianelou Dec 20, 2023
ab96034
fixing first release of open-appsec for nginx-proxy-manager
Dec 20, 2023
8ef4a82
Merge branch 'open-appsec-addition' of https://github.com/openappsec/…
Dec 20, 2023
6b94d74
Update README.md
orianelou Dec 20, 2023
26553cc
Update README.md
orianelou Dec 20, 2023
4415447
Merge branch 'develop' into open-appsec-addition
roybarda Dec 20, 2023
98b1e49
Merge pull request #2 from openappsec/open-appsec-addition
orianelou Dec 20, 2023
94df666
Merge pull request #3 from openappsec/develop
roybarda Dec 20, 2023
9099729
Update README.md
orianelou Dec 21, 2023
f79772d
Update README.md
orianelou Dec 27, 2023
bc4a8d6
Update README.md
orianelou Dec 27, 2023
fd2c6fd
fixing html sanitiation
Dec 27, 2023
ccb571a
Update docker-compose.yaml
orianelou Feb 25, 2024
47ed646
Update docker-compose.yaml
orianelou Mar 11, 2024
8086073
Create docker-compose-managed.yaml
orianelou Apr 4, 2024
a3bdf0d
Rename deployment/docker-compose-managed.yaml to deployment/managed-f…
orianelou Apr 4, 2024
a9c94f4
Create docker-compose.yaml
orianelou Apr 4, 2024
5f172fb
Update docker-compose.yaml
orianelou Apr 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 141 additions & 76 deletions README.md

Large diffs are not rendered by default.

314 changes: 314 additions & 0 deletions backend/internal/nginx-openappsec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
const util = require('util');
const execPromise = util.promisify(require('child_process').exec);
const { exec } = require('child_process');
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);
})
.then(() => {
// Return the notifyPolicyUpdate promise chain
// notify openappsec to apply the policy
return internalNginxOpenappsec.notifyPolicyUpdate().catch((errorMessage) => {
// console.error('Error:', errorMessage);
const errorMessageForUI = `Error: Policy couldn’t be applied, open-appsec-agent container is not responding.
Check if open-appec-agent container is running, then apply open-appsec Configuration
again by clicking here:
<br>Settings -> open-appsec Advanced -> Save Settings`;

return Promise.reject(new Error(errorMessageForUI));
});
});
},

/**
* 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));
})
.then(() => {
// Return the notifyPolicyUpdate promise chain
// notify openappsec to apply the policy
return internalNginxOpenappsec.notifyPolicyUpdate().catch((errorMessage) => {
console.error('---Error:', errorMessage);
const errorMessageForUI = `Error: Policy couldn’t be applied, open-appsec-agent container is not responding.
Check if open-appec-agent container is running, then apply open-appsec Configuration
again by clicking here:
<br>Settings -> open-appsec Advanced -> Save Settings`;

return Promise.reject(new Error(errorMessageForUI));
});
})
.catch((err) => {
logger.error('Error deleting openappsec config:', err);
// throw err; // Propagate the error to the caller
});
},

/**
* 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]);
});
}
}
},

notifyPolicyUpdate: async function() {
if (!constants.USE_NOTIFY_POLICY) {
console.log('USE_NOTIFY_POLICY is false');
return;
}
let ports = constants.PORTS;
let lastError = null;
for (let port of ports) {
try {
const data = `{"policy_path":"${constants.POLICY_PATH}"}`;
const command = `curl -s -o /dev/null -w "%{http_code}" --data '${data}' ${constants.HOSTURL}:${port}/set-apply-policy`;
let { stdout } = await execPromise(command);
if (stdout === '200') {
console.log(`Policy applied successfully on port ${port}`);
return;
} else {
console.log(`Policy Unexpected response code: ${stdout}`);
lastError = new Error(`Unexpected response code: ${stdout}`);
}
} catch (error) {
console.log(`Error notifying openappsec to apply the policy on port ${port}: ${error.message}`);
lastError = error;
}
}

// if (lastError) {
// throw lastError;
// }
},

/**
* 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;
Loading