Viewing File: /usr/local/cpanel/whostmgr/docroot/templates/mod_security/services/reportService.js

/*
# mod_security/services/reportService.js          Copyright(c) 2020 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/

/* global define: false */

define(
    [

        // Libraries
        "angular",

        // CJT
        "cjt/util/locale",
        "cjt/io/whm-v1-request",
        "cjt/io/whm-v1",
        "cjt/services/APIService",

        // Feature-specific
        "app/services/hitlistService",
        "app/services/ruleService"
    ],
    function(angular, LOCALE, APIREQUEST) {

        var NO_MODULE = "";

        // Fetch the current application
        var app;

        try {
            app = angular.module("App"); // For runtime
        } catch (e) {
            app = angular.module("App", ["cjt2.services.api"]); // Fall-back for unit testing
        }

        /**
         * This service uses the ruleService and hitListService to allow for better front-end
         * visualization of the relationships between hits and rules. Using setHit or setRule
         * will return a promise that will resolve with a report object, which is just a
         * conglomerate object of related rules and hits. The two methods differ slightly in
         * their output and more information is provided in their documentation blocks.
         */
        app.factory("reportService", [
            "$q",
            "APIService",
            "ruleService",
            "hitListService",
            function(
                $q,
                APIService,
                ruleService,
                hitListService
            ) {

                var currentReport; // Will be a promise

                /**
                 * Extracts the vendor id from a config file path.
                 *
                 * @method _getVendorFromFile
                 * @private
                 * @param  {String} file   The full file path to the config file.
                 *
                 * @return {String}        The vendor id if it's a vendor config or undefined if we
                 *                         can't parse the file path properly.
                 */
                function _getVendorFromFile(file) {
                    var VENDOR_REGEX = /\/modsec_vendor_configs\/(\w+)/;

                    var match = file && file.match(VENDOR_REGEX);
                    return match ? match[1] : void 0;
                }

                /**
                 * Given a unique hit ID (the id column from modsec.hits) or an actual hit object,
                 * this method will kick off a promise chain that will package together the full
                 * hit object along with its associated rule. The resolved report object from this
                 * method will differ from the report object provided by the setRule promise in
                 * that it will ONLY include the hit given as an argument.
                 *
                 * @method setHit
                 * @param  {String|Number|Object} hit   Either a bare hit ID or a hit object
                 *
                 * @return {Promise}                    This promise will resolve with a report object,
                 *                                      which essentially just packages a rule object
                 *                                      with an array of associated hits. For this
                 *                                      method, there will only be one hit in the array.
                 */
                function fetchByHit(hit) {
                    var fetched = {}; // This will house the eventual response
                    var hitPromise;

                    if (!angular.isObject(hit)) {

                        // This is a bare hitId so we need to fetch the actual hit object first
                        hitPromise = hitListService.fetchById(hit)
                            .then(function(response) {
                                fetched.hits = response.items;
                                return response.items[0]; // This length is guaranteed by the hitListService
                            });
                    } else {

                        // We already have the hit object, so just wrap it in an array and a promise
                        fetched.hits = [hit];

                        var deferred = $q.defer();
                        deferred.resolve(hit);
                        hitPromise = deferred.promise;
                    }

                    currentReport = hitPromise.then(function(hit) {

                        // Reports only work with vendors right now, so check that this is a vendor rule
                        var vendor = _getVendorFromFile(hit.meta_file);
                        if (!vendor) {
                            return $q.reject( LOCALE.maketext("You can only report [asis,ModSecurity] rules that a vendor provided.") );
                        }

                        // Fetch the rule
                        return ruleService.fetchRulesById(hit.meta_id, vendor);
                    }).then(function(response) {
                        fetched.rule = response.items[0]; // The length is guaranteed by the ruleService
                        return fetched;
                    });

                    return currentReport;
                }


                /**
                 * Given a unique rule ID or an actual rule object, this method will kick off a promise
                 * chain that will package together the full rule object along with any associated hits.
                 * The resolved report object from this method will differ from the report object
                 * provided by the setHit promise in that it will include ALL hits associated with the
                 * rule argument.
                 *
                 * @method setRule
                 * @param  {String|Number|Object} rule     Either a rule ID or a rule object
                 * @param  {String}               vendor   A vendor ID string
                 *
                 * @return {Promise}                       This promise will resolve with a report object,
                 *                                         which essentially just packages a rule object
                 *                                         with an array of associated hits.
                 */
                function fetchByRule(rule, vendor) {
                    var fetched = {};
                    var rulePromise;

                    if (!angular.isObject(rule)) { // This is a bare ruleId so we need to fetch the actual rule object first
                        // Reports only work with vendors right now, so check that one was provided
                        if (!vendor) {
                            return $q.reject( LOCALE.maketext("You can only report [asis,ModSecurity] rules that a vendor provided.") );
                        }

                        rulePromise = ruleService.fetchRulesById(rule, vendor).then(function(response) {
                            fetched.rule = response.items[0]; // The length is guaranteed by the ruleService
                            return fetched.rule;
                        });
                    } else { // We already have the rule object, so just wrap it in a promise

                        // Reports only work with vendors, so check that one was provided
                        if (!rule.vendor_id) {
                            return $q.reject( LOCALE.maketext("Only [asis,ModSecurity] rules provided by vendors may be reported.") );
                        }

                        fetched.rule = rule;

                        var deferred = $q.defer();
                        deferred.resolve(rule);
                        rulePromise = deferred.promise;
                    }

                    currentReport = rulePromise.then(function(rule) {
                        return hitListService.fetchList({
                            filterBy: "meta_id",
                            filterValue: rule.id,
                            filterCompare: "eq"
                        });
                    }).then(function(response) {
                        fetched.hits = response.items;
                        return fetched;
                    });

                    return currentReport;
                }

                /**
                 * Returns the current report promise. This is useful when changing views/controllers.
                 *
                 * @method getCurrent
                 * @return {Promise}   Either undefined if there is no current report promise,
                 *                     or a promise that will resolve with a report object,
                 *                     which essentially just packages a rule object with an
                 *                     array of associated hits.
                 */
                function getCurrent() {
                    return currentReport;
                }

                /**
                 * Unsets the current report so that it doesn't become stale.
                 * @method clearCurrent
                 */
                function clearCurrent() {
                    currentReport = void 0;
                }

                /**
                 * Generates a report but doesn't send it.
                 *
                 * @method viewReport
                 * @param  {Object} reportParams   See _generateReport documentation
                 *
                 * @return {Promise}
                 */
                function viewReport(reportParams) {
                    reportParams.send = false;
                    return _generateReport.call(this, reportParams);
                }

                /**
                 * Generates a report and sends it. Optionally disables the rule as well.
                 *
                 * @method sendReport
                 * @param  {Object} reportParams      See _generateReport documentation
                 * @param  {Object} [disableParams]   A set of params required for disabling the rule.
                 *     @param {Number}  disableParams.ruleId        The id of the rule to be disabled.
                 *     @param {Boolean} disableParams.deployRule    Should the disable change be deployed?
                 *     @param {String}  disableParams.ruleConfig    The path of the config file housing the rule.
                 *
                 * @return {Promise}                  Resolves when both operations are complete (or just the report, if no disableParams were given)
                 */
                function sendReport(reportParams, disableParams) {
                    var promises = {};

                    reportParams.send = true;
                    promises.report = _generateReport.call(this, reportParams);

                    if (disableParams) {
                        promises.disable = ruleService.disableRule(disableParams.ruleConfig, disableParams.ruleId, disableParams.deployRule);
                    }

                    return $q.all(promises);
                }

                /**
                 * Uses the modsec_report_rule API to either send a report or only perform a dry
                 * run and generate what would be sent without actually sending the payload.
                 *
                 * @method _generateReport
                 * @param  {Object}  params           Contains the key/value pairs for the parameters that will be passed with the API call.
                 * @param  {Array}   params.hits      An array of hit IDs that correspond to the id column in the modsec.hits table.
                 * @param  {String}  params.message   A short message to accompany the report.
                 * @param  {String}  params.email     The sender's email address.
                 * @param  {String}  params.reason    The reason for which the report is being submitted.
                 * @param  {Boolean} params.send      If true, the generated report will be sent by the API.
                 *
                 * @return {Promise}                  Resolves with the raw JSON generated by the API.
                 */
                function _generateReport(params) {

                    var apiCall = new APIREQUEST.Class();
                    apiCall.initialize(NO_MODULE, "modsec_report_rule");

                    angular.forEach({
                        row_ids: params.hits.join(","),
                        message: params.message,
                        email: params.email,
                        type: params.reason,
                        send: params.send ? 1 : 0
                    }, function(val, key) {
                        apiCall.addArgument(key, val);
                    });

                    return this.deferred(apiCall, {
                        transformAPISuccess: _extractReport
                    }).promise;
                }

                /**
                 * Extracts the report object from the response.
                 * @param  {Object} response   The response from the API.
                 * @return {Object}            The report object.
                 */
                function _extractReport(response) {
                    return response.data.report;
                }


                // Set up the service's constructor and parent
                var ReportService = function() {};
                ReportService.prototype = new APIService();

                // Extend the prototype with any class-specific functionality
                angular.extend(ReportService.prototype, {
                    fetchByHit: fetchByHit,
                    fetchByRule: fetchByRule,
                    getCurrent: getCurrent,
                    clearCurrent: clearCurrent,
                    viewReport: viewReport,
                    sendReport: sendReport
                });

                return new ReportService();
            }
        ]);
    }
);
Back to Directory File Manager