diff --git a/CHANGES.md b/CHANGES.md
index f996f6f6..a526085a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,17 @@ Cobra Changelog
Here you can see the full list of changes between each Cobra release.
+Version 2.0.0-alpha.5
+---------------------
+
+Released on Sep 15 2017
+
+- 增加漏洞搜索在报告页 #475
+- 优化Log输出 #570
+- 增加yacc依赖帮助 #569
+- 更改默认使用方法及参数配置
+- 其它细节优化和Bug修复
+
Version 2.0.0-alpha.4
---------------------
diff --git a/cobra/__version__.py b/cobra/__version__.py
index e5bb4536..bb1534b4 100644
--- a/cobra/__version__.py
+++ b/cobra/__version__.py
@@ -7,7 +7,7 @@
__issue_page__ = 'https://github.com/wufeifei/cobra/issues/new'
__python_version__ = sys.version.split()[0]
__platform__ = platform.platform()
-__version__ = '2.0.0-alpha.4'
+__version__ = '2.0.0-alpha.5'
__author__ = 'Feei'
__author_email__ = 'feei@feei.cn'
__license__ = 'MIT License'
@@ -27,5 +27,5 @@
python {m} -t {td} -f json -o /tmp/report.json
python {m} -t {tg} -f json -o feei@feei.cn
python {m} -t {tg} -f json -o http://push.to.com/api
- sudo python {m} -H 127.0.0.1 -P 80
+ python {m} -H 127.0.0.1 -P 8888
""".format(m='cobra.py', td='tests/vulnerabilities', tg='https://github.com/ethicalhack3r/DVWA')
diff --git a/cobra/api.py b/cobra/api.py
index e97e91b0..86ec29a0 100644
--- a/cobra/api.py
+++ b/cobra/api.py
@@ -31,7 +31,7 @@
from .config import Config, running_path, package_path
from .engine import Running
from .log import logger
-from .utils import allowed_file, secure_filename, PY2
+from .utils import allowed_file, secure_filename, PY2, split_branch
try:
# Python 3
@@ -299,8 +299,57 @@ def post():
else:
return {'code': 1002, 'msg': 'No such file.'}
- return {'code': 1001, 'result': {'file_content': file_content,
- 'extension': extension}}
+ return {'code': 1001, 'result': {'file_content': file_content, 'extension': extension}}
+
+
+class Search(Resource):
+ @staticmethod
+ def post():
+ """
+ Search specific rule.
+ :return:
+ """
+ data = request.json
+ if not data or data == "":
+ return {'code': 1003, 'msg': 'Only support json, please post json data.'}
+
+ sid = data.get('sid')
+ if not sid or sid == '':
+ return {'code': 1002, 'msg': 'sid is required.'}
+
+ rule_id = data.get('rule_id')
+ if not rule_id or rule_id == '':
+ return {'code': 1002, 'msg': 'rule_id is required.'}
+
+ scan_list_file = os.path.join(running_path, '{sid}_list'.format(sid=sid))
+ if not os.path.exists(scan_list_file):
+ return {'code': 1002, 'msg': 'No such sid.'}
+
+ with open(scan_list_file, 'r') as f:
+ scan_list = json.load(f)
+
+ if not isinstance(rule_id, list):
+ rule_id = [rule_id]
+
+ search_data = list()
+ for s_sid in scan_list.get('sids').keys():
+ target, branch = split_branch(scan_list.get('sids').get(s_sid))
+ search_result = search_rule(s_sid, rule_id)
+ cvi_count = list(search_result.values())
+ if int(cvi_count[0]) > 0:
+ search_data.append({
+ 'target_info': {
+ 'sid': s_sid,
+ 'target': target,
+ 'branch': branch,
+ },
+ 'search_result': search_result,
+ })
+
+ return {
+ 'code': 1001,
+ 'result': search_data,
+ }
@app.route('/', methods=['GET', 'POST'])
@@ -333,13 +382,7 @@ def summary():
if scan_status.get('result').get('status') == 'running':
still_running = scan_status.get('result').get('still_running')
for s_sid, target_str in still_running.items():
- split_target = target_str.split(':')
- if len(split_target) == 3:
- target, branch = '{p}:{u}'.format(p=split_target[0], u=split_target[1]), split_target[-1]
- elif len(split_target) == 2:
- target, branch = target_str, 'master'
- else:
- target, branch = target_str, 'master'
+ target, branch = split_branch(target_str)
still_running[s_sid] = {'target': target,
'branch': branch}
else:
@@ -357,7 +400,8 @@ def summary():
not_finished_number = scan_status.get('result').get('not_finished')
total_vul_number, critical_vul_number, high_vul_number, medium_vul_number, low_vul_number = 0, 0, 0, 0, 0
- rule_filter = dict()
+ rule_num = dict()
+ rules = dict()
targets = list()
for s_sid, target_str in scan_list.get('sids').items():
@@ -365,13 +409,7 @@ def summary():
target_info = dict()
# 分割项目地址与分支,默认 master
- split_target = target_str.split(':')
- if len(split_target) == 3:
- target, branch = '{p}:{u}'.format(p=split_target[0], u=split_target[1]), split_target[-1]
- elif len(split_target) == 2:
- target, branch = target_str, 'master'
- else:
- target, branch = target_str, 'master'
+ target, branch = split_branch(target_str)
target_info.update({
'sid': s_sid,
@@ -403,9 +441,11 @@ def summary():
low_vul_number += 1
try:
- rule_filter[vul.get('rule_name')] += 1
+ rule_num[vul.get('rule_name')] += 1
except KeyError:
- rule_filter[vul.get('rule_name')] = 1
+ rule_num[vul.get('rule_name')] = 1
+
+ rules[vul.get('id')] = vul.get('rule_name')
return render_template(template_name_or_list='summary.html',
total_targets_number=total_targets_number,
@@ -418,7 +458,8 @@ def summary():
high_vul_number=high_vul_number,
medium_vul_number=medium_vul_number,
low_vul_number=low_vul_number,
- vuls=rule_filter,
+ rule_num=rule_num,
+ rules=rules,
running=still_running,)
@@ -461,6 +502,30 @@ def guess_type(fn):
return extension.lower()
+def search_rule(sid, rule_id):
+ """
+ Search specific rule name in scan data.
+ :param sid: scan data id
+ :param rule_id: a list of rule name
+ :return: {rule_name1: num1, rule_name2: num2}
+ """
+ scan_data_file = os.path.join(running_path, '{sid}_data'.format(sid=sid))
+ search_result = dict.fromkeys(rule_id, 0)
+ if not os.path.exists(scan_data_file):
+ return search_result
+
+ with open(scan_data_file, 'r') as f:
+ scan_data = json.load(f)
+
+ if scan_data.get('code') == 1001 and len(scan_data.get('result').get('vulnerabilities')) > 0:
+ for vul in scan_data.get('result').get('vulnerabilities'):
+ if vul.get('id') in rule_id:
+ search_result[vul.get('id')] += 1
+ return search_result
+ else:
+ return search_result
+
+
def start(host, port, debug):
logger.info('Start {host}:{port}'.format(host=host, port=port))
api = Api(app)
@@ -470,6 +535,7 @@ def start(host, port, debug):
api.add_resource(FileUpload, '/api/upload')
api.add_resource(ResultData, '/api/list')
api.add_resource(ResultDetail, '/api/detail')
+ api.add_resource(Search, '/api/search')
# consumer
threads = []
diff --git a/cobra/cast.py b/cobra/cast.py
index 13897195..e907df30 100644
--- a/cobra/cast.py
+++ b/cobra/cast.py
@@ -286,7 +286,7 @@ def is_controllable_param(self):
logger.debug("[AST] Not Java/PHP, can't parse ({l})".format(l=self.language))
return False, self.data
else:
- logger.warning("[AST] Can't get `param`, check built-in rule")
+ logger.debug("[AST] Can't get `param`, check built-in rule")
return False, self.data
def match(self, rule, block_id):
diff --git a/cobra/cve.py b/cobra/cve.py
index 6aa2bfbd..70a61868 100644
--- a/cobra/cve.py
+++ b/cobra/cve.py
@@ -406,7 +406,7 @@ def parse_math(cve_path, cve_id, cve_level, module_, target_directory):
mr.file_path = 'unkown'
mr.language = '*'
mr.id = cvi
- mr.rule_name = cve_id
+ mr.rule_name = '引用了存在漏洞的三方组件'
mr.level = cve_level
mr.line_number = 1
mr.analysis = 'Dependencies Matched(依赖匹配)'
diff --git a/cobra/engine.py b/cobra/engine.py
index 8efa2bbf..8cac1fa0 100644
--- a/cobra/engine.py
+++ b/cobra/engine.py
@@ -175,9 +175,11 @@ def store(result):
return False
logger.info('[PUSH] {rc} Rules'.format(rc=len(rules)))
push_rules = []
+ off_rules = 0
for idx, single_rule in enumerate(rules):
if single_rule['status'] is False:
- logger.info('[CVI-{cvi}] [STATUS] OFF, CONTINUE...'.format(cvi=single_rule['id']))
+ off_rules += 1
+ logger.debug('[CVI-{cvi}] [STATUS] OFF, CONTINUE...'.format(cvi=single_rule['id']))
continue
# SR(Single Rule)
logger.debug("""[PUSH] [CVI-{cvi}] {idx}.{name}({language})""".format(
@@ -227,7 +229,7 @@ def store(result):
if vn == 0:
logger.info('[SCAN] Not found vulnerability!')
else:
- logger.info("[SCAN] Trigger Rules: {tr} Vulnerabilities ({vn})\r\n{table}".format(tr=len(trigger_rules), vn=len(find_vulnerabilities), table=table))
+ logger.info("[SCAN] Trigger Rules/Not Trigger Rules/Off Rules: {tr}/{ntr}/{fr} Vulnerabilities ({vn})\r\n{table}".format(tr=len(trigger_rules), ntr=len(diff_rules), fr=off_rules, vn=len(find_vulnerabilities), table=table))
if len(diff_rules) > 0:
logger.info('[SCAN] Not Trigger Rules ({l}): {r}'.format(l=len(diff_rules), r=','.join(diff_rules)))
diff --git a/cobra/export.py b/cobra/export.py
index 56850d91..56e3019f 100644
--- a/cobra/export.py
+++ b/cobra/export.py
@@ -130,7 +130,7 @@ def write_to_file(target, sid, output_format='', filename=None):
:return:
"""
if not filename:
- logger.info('[EXPORT] No filename given, nothing exported.')
+ logger.debug('[EXPORT] No filename given, nothing exported.')
return False
scan_data_file = os.path.join(running_path, '{sid}_data'.format(sid=sid))
diff --git a/cobra/templates/asset/css/bootstrap-multiselect.css b/cobra/templates/asset/css/bootstrap-multiselect.css
new file mode 100755
index 00000000..13de57bb
--- /dev/null
+++ b/cobra/templates/asset/css/bootstrap-multiselect.css
@@ -0,0 +1 @@
+.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0}
\ No newline at end of file
diff --git a/cobra/templates/asset/css/report.css b/cobra/templates/asset/css/report.css
index bf2b7801..1b092ede 100644
--- a/cobra/templates/asset/css/report.css
+++ b/cobra/templates/asset/css/report.css
@@ -52,31 +52,43 @@
/* Let's get this party started */
/*::-webkit-scrollbar {*/
- /*width: 10px;*/
+/*width: 10px;*/
/*}*/
/*!* Track *!*/
/*::-webkit-scrollbar-track {*/
- /*background: #343f44;*/
+/*background: #343f44;*/
/*}*/
/*!* Handle *!*/
/*::-webkit-scrollbar-thumb {*/
- /*background: #525b5f !important;*/
- /*border-radius: 5px !important;*/
- /*border: 1px solid #383e40 !important;*/
+/*background: #525b5f !important;*/
+/*border-radius: 5px !important;*/
+/*border: 1px solid #383e40 !important;*/
/*}*/
/*::-webkit-scrollbar-thumb:window-inactive {*/
- /*background: rgba(255, 0, 0, 0.4);*/
+/*background: rgba(255, 0, 0, 0.4);*/
/*}*/
+#target_table {
+ overflow: auto;
+ max-height: 600px;
+}
+
+#search_table {
+ margin-top: 20px;
+}
-#table {
- overflow-y: auto;
+#search_table_div {
+ overflow: auto;
max-height: 600px;
}
+#submit_search {
+ margin-left: 20px;
+}
+
.vulnerabilities_list > li {
border-top: 1px solid #282828;
padding: 5px;
@@ -104,20 +116,6 @@
background: #1c2427;
}
-.vulnerabilities_list > li.fixed {
- background-image: url("/asset/img/fixed.png");
- background-position: right center;
- background-repeat: no-repeat;
- background-size: 50px 50px;
-}
-
-.vulnerabilities_list > li.not_fixed {
- background-image: url("/asset/img/not_fixed.png");
- background-position: right center;
- background-repeat: no-repeat;
- background-size: 50px 50px;
-}
-
.congratulations {
display: block;
max-width: 100%;
@@ -158,19 +156,19 @@ ul.v_detail li {
border-radius: 6px;
}
-.n-o-v tr:nth-child(1) td:nth-child(2){
+.n-o-v tr:nth-child(1) td:nth-child(2) {
color: red;
}
-.n-o-v tr:nth-child(2) td:nth-child(2){
- color: gold;
+.n-o-v tr:nth-child(2) td:nth-child(2) {
+ color: gold;
}
-.n-o-v tr:nth-child(3) td:nth-child(2){
+.n-o-v tr:nth-child(3) td:nth-child(2) {
color: green;
}
-.n-o-v tr:nth-child(4) td:nth-child(2){
+.n-o-v tr:nth-child(4) td:nth-child(2) {
color: #357abd;
}
@@ -281,10 +279,8 @@ ul.v_detail li {
margin: 225px auto;
text-align: center;
position: relative;
- width: 100%;
display: block;
height: 500px;
- position: relative;
width: 32px;
}
diff --git a/cobra/templates/asset/js/bootstrap-multiselect.js b/cobra/templates/asset/js/bootstrap-multiselect.js
new file mode 100755
index 00000000..5fb4c18c
--- /dev/null
+++ b/cobra/templates/asset/js/bootstrap-multiselect.js
@@ -0,0 +1,1416 @@
+/**
+ * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect)
+ *
+ * Apache License, Version 2.0:
+ * Copyright (c) 2012 - 2015 David Stutz
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * BSD 3-Clause License:
+ * Copyright (c) 2012 - 2015 David Stutz
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of David Stutz nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+!function ($) {
+ "use strict";// jshint ;_;
+
+ if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
+ ko.bindingHandlers.multiselect = {
+ after: ['options', 'value', 'selectedOptions'],
+
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var $element = $(element);
+ var config = ko.toJS(valueAccessor());
+
+ $element.multiselect(config);
+
+ if (allBindings.has('options')) {
+ var options = allBindings.get('options');
+ if (ko.isObservable(options)) {
+ ko.computed({
+ read: function() {
+ options();
+ setTimeout(function() {
+ var ms = $element.data('multiselect');
+ if (ms)
+ ms.updateOriginalOptions();//Not sure how beneficial this is.
+ $element.multiselect('rebuild');
+ }, 1);
+ },
+ disposeWhenNodeIsRemoved: element
+ });
+ }
+ }
+
+ //value and selectedOptions are two-way, so these will be triggered even by our own actions.
+ //It needs some way to tell if they are triggered because of us or because of outside change.
+ //It doesn't loop but it's a waste of processing.
+ if (allBindings.has('value')) {
+ var value = allBindings.get('value');
+ if (ko.isObservable(value)) {
+ ko.computed({
+ read: function() {
+ value();
+ setTimeout(function() {
+ $element.multiselect('refresh');
+ }, 1);
+ },
+ disposeWhenNodeIsRemoved: element
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
+ }
+ }
+
+ //Switched from arrayChange subscription to general subscription using 'refresh'.
+ //Not sure performance is any better using 'select' and 'deselect'.
+ if (allBindings.has('selectedOptions')) {
+ var selectedOptions = allBindings.get('selectedOptions');
+ if (ko.isObservable(selectedOptions)) {
+ ko.computed({
+ read: function() {
+ selectedOptions();
+ setTimeout(function() {
+ $element.multiselect('refresh');
+ }, 1);
+ },
+ disposeWhenNodeIsRemoved: element
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
+ }
+ }
+
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+ $element.multiselect('destroy');
+ });
+ },
+
+ update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var $element = $(element);
+ var config = ko.toJS(valueAccessor());
+
+ $element.multiselect('setOptions', config);
+ $element.multiselect('rebuild');
+ }
+ };
+ }
+
+ function forEach(array, callback) {
+ for (var index = 0; index < array.length; ++index) {
+ callback(array[index], index);
+ }
+ }
+
+ /**
+ * Constructor to create a new multiselect using the given select.
+ *
+ * @param {jQuery} select
+ * @param {Object} options
+ * @returns {Multiselect}
+ */
+ function Multiselect(select, options) {
+
+ this.$select = $(select);
+
+ // Placeholder via data attributes
+ if (this.$select.attr("data-placeholder")) {
+ options.nonSelectedText = this.$select.data("placeholder");
+ }
+
+ this.options = this.mergeOptions($.extend({}, options, this.$select.data()));
+
+ // Initialization.
+ // We have to clone to create a new reference.
+ this.originalOptions = this.$select.clone()[0].options;
+ this.query = '';
+ this.searchTimeout = null;
+ this.lastToggledInput = null
+
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
+ this.options.onChange = $.proxy(this.options.onChange, this);
+ this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
+ this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
+ this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this);
+ this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this);
+
+ // Build select all if enabled.
+ this.buildContainer();
+ this.buildButton();
+ this.buildDropdown();
+ this.buildSelectAll();
+ this.buildDropdownOptions();
+ this.buildFilter();
+
+ this.updateButtonText();
+ this.updateSelectAll();
+
+ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
+ this.disable();
+ }
+
+ this.$select.hide().after(this.$container);
+ };
+
+ Multiselect.prototype = {
+
+ defaults: {
+ /**
+ * Default text function will either print 'None selected' in case no
+ * option is selected or a list of the selected options up to a length
+ * of 3 selected options.
+ *
+ * @param {jQuery} options
+ * @param {jQuery} select
+ * @returns {String}
+ */
+ buttonText: function(options, select) {
+ if (options.length === 0) {
+ return this.nonSelectedText;
+ }
+ else if (this.allSelectedText
+ && options.length === $('option', $(select)).length
+ && $('option', $(select)).length !== 1
+ && this.multiple) {
+
+ if (this.selectAllNumber) {
+ return this.allSelectedText + ' (' + options.length + ')';
+ }
+ else {
+ return this.allSelectedText;
+ }
+ }
+ else if (options.length > this.numberDisplayed) {
+ return options.length + ' ' + this.nSelectedText;
+ }
+ else {
+ var selected = '';
+ var delimiter = this.delimiterText;
+
+ options.each(function() {
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
+ selected += label + delimiter;
+ });
+
+ return selected.substr(0, selected.length - 2);
+ }
+ },
+ /**
+ * Updates the title of the button similar to the buttonText function.
+ *
+ * @param {jQuery} options
+ * @param {jQuery} select
+ * @returns {@exp;selected@call;substr}
+ */
+ buttonTitle: function(options, select) {
+ if (options.length === 0) {
+ return this.nonSelectedText;
+ }
+ else {
+ var selected = '';
+ var delimiter = this.delimiterText;
+
+ options.each(function () {
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
+ selected += label + delimiter;
+ });
+ return selected.substr(0, selected.length - 2);
+ }
+ },
+ /**
+ * Create a label.
+ *
+ * @param {jQuery} element
+ * @returns {String}
+ */
+ optionLabel: function(element){
+ return $(element).attr('label') || $(element).text();
+ },
+ /**
+ * Triggered on change of the multiselect.
+ *
+ * Not triggered when selecting/deselecting options manually.
+ *
+ * @param {jQuery} option
+ * @param {Boolean} checked
+ */
+ onChange : function(option, checked) {
+
+ },
+ /**
+ * Triggered when the dropdown is shown.
+ *
+ * @param {jQuery} event
+ */
+ onDropdownShow: function(event) {
+
+ },
+ /**
+ * Triggered when the dropdown is hidden.
+ *
+ * @param {jQuery} event
+ */
+ onDropdownHide: function(event) {
+
+ },
+ /**
+ * Triggered after the dropdown is shown.
+ *
+ * @param {jQuery} event
+ */
+ onDropdownShown: function(event) {
+
+ },
+ /**
+ * Triggered after the dropdown is hidden.
+ *
+ * @param {jQuery} event
+ */
+ onDropdownHidden: function(event) {
+
+ },
+ /**
+ * Triggered on select all.
+ */
+ onSelectAll: function() {
+
+ },
+ enableHTML: false,
+ buttonClass: 'btn btn-default',
+ inheritClass: false,
+ buttonWidth: 'auto',
+ buttonContainer: '
',
+ dropRight: false,
+ selectedClass: 'active',
+ // Maximum height of the dropdown menu.
+ // If maximum height is exceeded a scrollbar will be displayed.
+ maxHeight: false,
+ checkboxName: false,
+ includeSelectAllOption: false,
+ includeSelectAllIfMoreThan: 0,
+ selectAllText: ' Select all',
+ selectAllValue: 'multiselect-all',
+ selectAllName: false,
+ selectAllNumber: true,
+ enableFiltering: false,
+ enableCaseInsensitiveFiltering: false,
+ enableClickableOptGroups: false,
+ filterPlaceholder: 'Search',
+ // possible options: 'text', 'value', 'both'
+ filterBehavior: 'text',
+ includeFilterClearBtn: true,
+ preventInputChangeEvent: false,
+ nonSelectedText: 'None selected',
+ nSelectedText: 'selected',
+ allSelectedText: 'All selected',
+ numberDisplayed: 3,
+ disableIfEmpty: false,
+ delimiterText: ', ',
+ templates: {
+ button: '',
+ ul: '',
+ filter: '
',
+ filterClearBtn: '',
+ li: '',
+ divider: '',
+ liGroup: ''
+ }
+ },
+
+ constructor: Multiselect,
+
+ /**
+ * Builds the container of the multiselect.
+ */
+ buildContainer: function() {
+ this.$container = $(this.options.buttonContainer);
+ this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
+ this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
+ this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
+ this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
+ },
+
+ /**
+ * Builds the button of the multiselect.
+ */
+ buildButton: function() {
+ this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
+ if (this.$select.attr('class') && this.options.inheritClass) {
+ this.$button.addClass(this.$select.attr('class'));
+ }
+ // Adopt active state.
+ if (this.$select.prop('disabled')) {
+ this.disable();
+ }
+ else {
+ this.enable();
+ }
+
+ // Manually add button width if set.
+ if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
+ this.$button.css({
+ 'width' : this.options.buttonWidth,
+ 'overflow' : 'hidden',
+ 'text-overflow' : 'ellipsis'
+ });
+ this.$container.css({
+ 'width': this.options.buttonWidth
+ });
+ }
+
+ // Keep the tab index from the select.
+ var tabindex = this.$select.attr('tabindex');
+ if (tabindex) {
+ this.$button.attr('tabindex', tabindex);
+ }
+
+ this.$container.prepend(this.$button);
+ },
+
+ /**
+ * Builds the ul representing the dropdown menu.
+ */
+ buildDropdown: function() {
+
+ // Build ul.
+ this.$ul = $(this.options.templates.ul);
+
+ if (this.options.dropRight) {
+ this.$ul.addClass('pull-right');
+ }
+
+ // Set max height of dropdown menu to activate auto scrollbar.
+ if (this.options.maxHeight) {
+ // TODO: Add a class for this option to move the css declarations.
+ this.$ul.css({
+ 'max-height': this.options.maxHeight + 'px',
+ 'overflow-y': 'auto',
+ 'overflow-x': 'hidden'
+ });
+ }
+
+ this.$container.append(this.$ul);
+ },
+
+ /**
+ * Build the dropdown options and binds all nessecary events.
+ *
+ * Uses createDivider and createOptionValue to create the necessary options.
+ */
+ buildDropdownOptions: function() {
+
+ this.$select.children().each($.proxy(function(index, element) {
+
+ var $element = $(element);
+ // Support optgroups and options without a group simultaneously.
+ var tag = $element.prop('tagName')
+ .toLowerCase();
+
+ if ($element.prop('value') === this.options.selectAllValue) {
+ return;
+ }
+
+ if (tag === 'optgroup') {
+ this.createOptgroup(element);
+ }
+ else if (tag === 'option') {
+
+ if ($element.data('role') === 'divider') {
+ this.createDivider();
+ }
+ else {
+ this.createOptionValue(element);
+ }
+
+ }
+
+ // Other illegal tags will be ignored.
+ }, this));
+
+ // Bind the change event on the dropdown elements.
+ $('li input', this.$ul).on('change', $.proxy(function(event) {
+ var $target = $(event.target);
+
+ var checked = $target.prop('checked') || false;
+ var isSelectAllOption = $target.val() === this.options.selectAllValue;
+
+ // Apply or unapply the configured selected class.
+ if (this.options.selectedClass) {
+ if (checked) {
+ $target.closest('li')
+ .addClass(this.options.selectedClass);
+ }
+ else {
+ $target.closest('li')
+ .removeClass(this.options.selectedClass);
+ }
+ }
+
+ // Get the corresponding option.
+ var value = $target.val();
+ var $option = this.getOptionByValue(value);
+
+ var $optionsNotThis = $('option', this.$select).not($option);
+ var $checkboxesNotThis = $('input', this.$container).not($target);
+
+ if (isSelectAllOption) {
+ if (checked) {
+ this.selectAll();
+ }
+ else {
+ this.deselectAll();
+ }
+ }
+
+ if(!isSelectAllOption){
+ if (checked) {
+ $option.prop('selected', true);
+
+ if (this.options.multiple) {
+ // Simply select additional option.
+ $option.prop('selected', true);
+ }
+ else {
+ // Unselect all other options and corresponding checkboxes.
+ if (this.options.selectedClass) {
+ $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass);
+ }
+
+ $($checkboxesNotThis).prop('checked', false);
+ $optionsNotThis.prop('selected', false);
+
+ // It's a single selection, so close.
+ this.$button.click();
+ }
+
+ if (this.options.selectedClass === "active") {
+ $optionsNotThis.closest("a").css("outline", "");
+ }
+ }
+ else {
+ // Unselect option.
+ $option.prop('selected', false);
+ }
+ }
+
+ this.$select.change();
+
+ this.updateButtonText();
+ this.updateSelectAll();
+
+ this.options.onChange($option, checked);
+
+ if(this.options.preventInputChangeEvent) {
+ return false;
+ }
+ }, this));
+
+ $('li a', this.$ul).on('mousedown', function(e) {
+ if (e.shiftKey) {
+ // Prevent selecting text by Shift+click
+ return false;
+ }
+ });
+
+ $('li a', this.$ul).on('touchstart click', $.proxy(function(event) {
+ event.stopPropagation();
+
+ var $target = $(event.target);
+
+ if (event.shiftKey && this.options.multiple) {
+ if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
+ event.preventDefault();
+ $target = $target.find("input");
+ $target.prop("checked", !$target.prop("checked"));
+ }
+ var checked = $target.prop('checked') || false;
+
+ if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
+ var from = $target.closest("li").index();
+ var to = this.lastToggledInput.closest("li").index();
+
+ if (from > to) { // Swap the indices
+ var tmp = to;
+ to = from;
+ from = tmp;
+ }
+
+ // Make sure we grab all elements since slice excludes the last index
+ ++to;
+
+ // Change the checkboxes and underlying options
+ var range = this.$ul.find("li").slice(from, to).find("input");
+
+ range.prop('checked', checked);
+
+ if (this.options.selectedClass) {
+ range.closest('li')
+ .toggleClass(this.options.selectedClass, checked);
+ }
+
+ for (var i = 0, j = range.length; i < j; i++) {
+ var $checkbox = $(range[i]);
+
+ var $option = this.getOptionByValue($checkbox.val());
+
+ $option.prop('selected', checked);
+ }
+ }
+
+ // Trigger the select "change" event
+ $target.trigger("change");
+ }
+
+ // Remembers last clicked option
+ if($target.is("input") && !$target.closest("li").is(".multiselect-item")){
+ this.lastToggledInput = $target;
+ }
+
+ $target.blur();
+ }, this));
+
+ // Keyboard support.
+ this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) {
+ if ($('input[type="text"]', this.$container).is(':focus')) {
+ return;
+ }
+
+ if (event.keyCode === 9 && this.$container.hasClass('open')) {
+ this.$button.click();
+ }
+ else {
+ var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible");
+
+ if (!$items.length) {
+ return;
+ }
+
+ var index = $items.index($items.filter(':focus'));
+
+ // Navigation up.
+ if (event.keyCode === 38 && index > 0) {
+ index--;
+ }
+ // Navigate down.
+ else if (event.keyCode === 40 && index < $items.length - 1) {
+ index++;
+ }
+ else if (!~index) {
+ index = 0;
+ }
+
+ var $current = $items.eq(index);
+ $current.focus();
+
+ if (event.keyCode === 32 || event.keyCode === 13) {
+ var $checkbox = $current.find('input');
+
+ $checkbox.prop("checked", !$checkbox.prop("checked"));
+ $checkbox.change();
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }, this));
+
+ if(this.options.enableClickableOptGroups && this.options.multiple) {
+ $('li.multiselect-group', this.$ul).on('click', $.proxy(function(event) {
+ event.stopPropagation();
+
+ var group = $(event.target).parent();
+
+ // Search all option in optgroup
+ var $options = group.nextUntil('li.multiselect-group');
+ var $visibleOptions = $options.filter(":visible:not(.disabled)");
+
+ // check or uncheck items
+ var allChecked = true;
+ var optionInputs = $visibleOptions.find('input');
+ optionInputs.each(function() {
+ allChecked = allChecked && $(this).prop('checked');
+ });
+
+ optionInputs.prop('checked', !allChecked).trigger('change');
+ }, this));
+ }
+ },
+
+ /**
+ * Create an option using the given select option.
+ *
+ * @param {jQuery} element
+ */
+ createOptionValue: function(element) {
+ var $element = $(element);
+ if ($element.is(':selected')) {
+ $element.prop('selected', true);
+ }
+
+ // Support the label attribute on options.
+ var label = this.options.optionLabel(element);
+ var value = $element.val();
+ var inputType = this.options.multiple ? "checkbox" : "radio";
+
+ var $li = $(this.options.templates.li);
+ var $label = $('label', $li);
+ $label.addClass(inputType);
+
+ if (this.options.enableHTML) {
+ $label.html(" " + label);
+ }
+ else {
+ $label.text(" " + label);
+ }
+
+ var $checkbox = $('').attr('type', inputType);
+
+ if (this.options.checkboxName) {
+ $checkbox.attr('name', this.options.checkboxName);
+ }
+ $label.prepend($checkbox);
+
+ var selected = $element.prop('selected') || false;
+ $checkbox.val(value);
+
+ if (value === this.options.selectAllValue) {
+ $li.addClass("multiselect-item multiselect-all");
+ $checkbox.parent().parent()
+ .addClass('multiselect-all');
+ }
+
+ $label.attr('title', $element.attr('title'));
+
+ this.$ul.append($li);
+
+ if ($element.is(':disabled')) {
+ $checkbox.attr('disabled', 'disabled')
+ .prop('disabled', true)
+ .closest('a')
+ .attr("tabindex", "-1")
+ .closest('li')
+ .addClass('disabled');
+ }
+
+ $checkbox.prop('checked', selected);
+
+ if (selected && this.options.selectedClass) {
+ $checkbox.closest('li')
+ .addClass(this.options.selectedClass);
+ }
+ },
+
+ /**
+ * Creates a divider using the given select option.
+ *
+ * @param {jQuery} element
+ */
+ createDivider: function(element) {
+ var $divider = $(this.options.templates.divider);
+ this.$ul.append($divider);
+ },
+
+ /**
+ * Creates an optgroup.
+ *
+ * @param {jQuery} group
+ */
+ createOptgroup: function(group) {
+ var groupName = $(group).prop('label');
+
+ // Add a header for the group.
+ var $li = $(this.options.templates.liGroup);
+
+ if (this.options.enableHTML) {
+ $('label', $li).html(groupName);
+ }
+ else {
+ $('label', $li).text(groupName);
+ }
+
+ if (this.options.enableClickableOptGroups) {
+ $li.addClass('multiselect-group-clickable');
+ }
+
+ this.$ul.append($li);
+
+ if ($(group).is(':disabled')) {
+ $li.addClass('disabled');
+ }
+
+ // Add the options of the group.
+ $('option', group).each($.proxy(function(index, element) {
+ this.createOptionValue(element);
+ }, this));
+ },
+
+ /**
+ * Build the selct all.
+ *
+ * Checks if a select all has already been created.
+ */
+ buildSelectAll: function() {
+ if (typeof this.options.selectAllValue === 'number') {
+ this.options.selectAllValue = this.options.selectAllValue.toString();
+ }
+
+ var alreadyHasSelectAll = this.hasSelectAll();
+
+ if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
+ && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
+
+ // Check whether to add a divider after the select all.
+ if (this.options.includeSelectAllDivider) {
+ this.$ul.prepend($(this.options.templates.divider));
+ }
+
+ var $li = $(this.options.templates.li);
+ $('label', $li).addClass("checkbox");
+
+ if (this.options.enableHTML) {
+ $('label', $li).html(" " + this.options.selectAllText);
+ }
+ else {
+ $('label', $li).text(" " + this.options.selectAllText);
+ }
+
+ if (this.options.selectAllName) {
+ $('label', $li).prepend('');
+ }
+ else {
+ $('label', $li).prepend('');
+ }
+
+ var $checkbox = $('input', $li);
+ $checkbox.val(this.options.selectAllValue);
+
+ $li.addClass("multiselect-item multiselect-all");
+ $checkbox.parent().parent()
+ .addClass('multiselect-all');
+
+ this.$ul.prepend($li);
+
+ $checkbox.prop('checked', false);
+ }
+ },
+
+ /**
+ * Builds the filter.
+ */
+ buildFilter: function() {
+
+ // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
+ var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
+
+ if (this.$select.find('option').length >= enableFilterLength) {
+
+ this.$filter = $(this.options.templates.filter);
+ $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
+
+ // Adds optional filter clear button
+ if(this.options.includeFilterClearBtn){
+ var clearBtn = $(this.options.templates.filterClearBtn);
+ clearBtn.on('click', $.proxy(function(event){
+ clearTimeout(this.searchTimeout);
+ this.$filter.find('.multiselect-search').val('');
+ $('li', this.$ul).show().removeClass("filter-hidden");
+ this.updateSelectAll();
+ }, this));
+ this.$filter.find('.input-group').append(clearBtn);
+ }
+
+ this.$ul.prepend(this.$filter);
+
+ this.$filter.val(this.query).on('click', function(event) {
+ event.stopPropagation();
+ }).on('input keydown', $.proxy(function(event) {
+ // Cancel enter key default behaviour
+ if (event.which === 13) {
+ event.preventDefault();
+ }
+
+ // This is useful to catch "keydown" events after the browser has updated the control.
+ clearTimeout(this.searchTimeout);
+
+ this.searchTimeout = this.asyncFunction($.proxy(function() {
+
+ if (this.query !== event.target.value) {
+ this.query = event.target.value;
+
+ var currentGroup, currentGroupVisible;
+ $.each($('li', this.$ul), $.proxy(function(index, element) {
+ var value = $('input', element).length > 0 ? $('input', element).val() : "";
+ var text = $('label', element).text();
+
+ var filterCandidate = '';
+ if ((this.options.filterBehavior === 'text')) {
+ filterCandidate = text;
+ }
+ else if ((this.options.filterBehavior === 'value')) {
+ filterCandidate = value;
+ }
+ else if (this.options.filterBehavior === 'both') {
+ filterCandidate = text + '\n' + value;
+ }
+
+ if (value !== this.options.selectAllValue && text) {
+ // By default lets assume that element is not
+ // interesting for this search.
+ var showElement = false;
+
+ if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
+ showElement = true;
+ }
+ else if (filterCandidate.indexOf(this.query) > -1) {
+ showElement = true;
+ }
+
+ // Toggle current element (group or group item) according to showElement boolean.
+ $(element).toggle(showElement).toggleClass('filter-hidden', !showElement);
+
+ // Differentiate groups and group items.
+ if ($(element).hasClass('multiselect-group')) {
+ // Remember group status.
+ currentGroup = element;
+ currentGroupVisible = showElement;
+ }
+ else {
+ // Show group name when at least one of its items is visible.
+ if (showElement) {
+ $(currentGroup).show().removeClass('filter-hidden');
+ }
+
+ // Show all group items when group name satisfies filter.
+ if (!showElement && currentGroupVisible) {
+ $(element).show().removeClass('filter-hidden');
+ }
+ }
+ }
+ }, this));
+ }
+
+ this.updateSelectAll();
+ }, this), 300, this);
+ }, this));
+ }
+ }
+ },
+
+ /**
+ * Unbinds the whole plugin.
+ */
+ destroy: function() {
+ this.$container.remove();
+ this.$select.show();
+ this.$select.data('multiselect', null);
+ },
+
+ /**
+ * Refreshs the multiselect based on the selected options of the select.
+ */
+ refresh: function() {
+ $('option', this.$select).each($.proxy(function(index, element) {
+ var $input = $('li input', this.$ul).filter(function() {
+ return $(this).val() === $(element).val();
+ });
+
+ if ($(element).is(':selected')) {
+ $input.prop('checked', true);
+
+ if (this.options.selectedClass) {
+ $input.closest('li')
+ .addClass(this.options.selectedClass);
+ }
+ }
+ else {
+ $input.prop('checked', false);
+
+ if (this.options.selectedClass) {
+ $input.closest('li')
+ .removeClass(this.options.selectedClass);
+ }
+ }
+
+ if ($(element).is(":disabled")) {
+ $input.attr('disabled', 'disabled')
+ .prop('disabled', true)
+ .closest('li')
+ .addClass('disabled');
+ }
+ else {
+ $input.prop('disabled', false)
+ .closest('li')
+ .removeClass('disabled');
+ }
+ }, this));
+
+ this.updateButtonText();
+ this.updateSelectAll();
+ },
+
+ /**
+ * Select all options of the given values.
+ *
+ * If triggerOnChange is set to true, the on change event is triggered if
+ * and only if one value is passed.
+ *
+ * @param {Array} selectValues
+ * @param {Boolean} triggerOnChange
+ */
+ select: function(selectValues, triggerOnChange) {
+ if(!$.isArray(selectValues)) {
+ selectValues = [selectValues];
+ }
+
+ for (var i = 0; i < selectValues.length; i++) {
+ var value = selectValues[i];
+
+ if (value === null || value === undefined) {
+ continue;
+ }
+
+ var $option = this.getOptionByValue(value);
+ var $checkbox = this.getInputByValue(value);
+
+ if($option === undefined || $checkbox === undefined) {
+ continue;
+ }
+
+ if (!this.options.multiple) {
+ this.deselectAll(false);
+ }
+
+ if (this.options.selectedClass) {
+ $checkbox.closest('li')
+ .addClass(this.options.selectedClass);
+ }
+
+ $checkbox.prop('checked', true);
+ $option.prop('selected', true);
+
+ if (triggerOnChange) {
+ this.options.onChange($option, true);
+ }
+ }
+
+ this.updateButtonText();
+ this.updateSelectAll();
+ },
+
+ /**
+ * Clears all selected items.
+ */
+ clearSelection: function () {
+ this.deselectAll(false);
+ this.updateButtonText();
+ this.updateSelectAll();
+ },
+
+ /**
+ * Deselects all options of the given values.
+ *
+ * If triggerOnChange is set to true, the on change event is triggered, if
+ * and only if one value is passed.
+ *
+ * @param {Array} deselectValues
+ * @param {Boolean} triggerOnChange
+ */
+ deselect: function(deselectValues, triggerOnChange) {
+ if(!$.isArray(deselectValues)) {
+ deselectValues = [deselectValues];
+ }
+
+ for (var i = 0; i < deselectValues.length; i++) {
+ var value = deselectValues[i];
+
+ if (value === null || value === undefined) {
+ continue;
+ }
+
+ var $option = this.getOptionByValue(value);
+ var $checkbox = this.getInputByValue(value);
+
+ if($option === undefined || $checkbox === undefined) {
+ continue;
+ }
+
+ if (this.options.selectedClass) {
+ $checkbox.closest('li')
+ .removeClass(this.options.selectedClass);
+ }
+
+ $checkbox.prop('checked', false);
+ $option.prop('selected', false);
+
+ if (triggerOnChange) {
+ this.options.onChange($option, false);
+ }
+ }
+
+ this.updateButtonText();
+ this.updateSelectAll();
+ },
+
+ /**
+ * Selects all enabled & visible options.
+ *
+ * If justVisible is true or not specified, only visible options are selected.
+ *
+ * @param {Boolean} justVisible
+ * @param {Boolean} triggerOnSelectAll
+ */
+ selectAll: function (justVisible, triggerOnSelectAll) {
+ var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
+ var allCheckboxes = $("li input[type='checkbox']:enabled", this.$ul);
+ var visibleCheckboxes = allCheckboxes.filter(":visible");
+ var allCheckboxesCount = allCheckboxes.length;
+ var visibleCheckboxesCount = visibleCheckboxes.length;
+
+ if(justVisible) {
+ visibleCheckboxes.prop('checked', true);
+ $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").addClass(this.options.selectedClass);
+ }
+ else {
+ allCheckboxes.prop('checked', true);
+ $("li:not(.divider):not(.disabled)", this.$ul).addClass(this.options.selectedClass);
+ }
+
+ if (allCheckboxesCount === visibleCheckboxesCount || justVisible === false) {
+ $("option:enabled", this.$select).prop('selected', true);
+ }
+ else {
+ var values = visibleCheckboxes.map(function() {
+ return $(this).val();
+ }).get();
+
+ $("option:enabled", this.$select).filter(function(index) {
+ return $.inArray($(this).val(), values) !== -1;
+ }).prop('selected', true);
+ }
+
+ if (triggerOnSelectAll) {
+ this.options.onSelectAll();
+ }
+ },
+
+ /**
+ * Deselects all options.
+ *
+ * If justVisible is true or not specified, only visible options are deselected.
+ *
+ * @param {Boolean} justVisible
+ */
+ deselectAll: function (justVisible) {
+ var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
+
+ if(justVisible) {
+ var visibleCheckboxes = $("li input[type='checkbox']:not(:disabled)", this.$ul).filter(":visible");
+ visibleCheckboxes.prop('checked', false);
+
+ var values = visibleCheckboxes.map(function() {
+ return $(this).val();
+ }).get();
+
+ $("option:enabled", this.$select).filter(function(index) {
+ return $.inArray($(this).val(), values) !== -1;
+ }).prop('selected', false);
+
+ if (this.options.selectedClass) {
+ $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").removeClass(this.options.selectedClass);
+ }
+ }
+ else {
+ $("li input[type='checkbox']:enabled", this.$ul).prop('checked', false);
+ $("option:enabled", this.$select).prop('selected', false);
+
+ if (this.options.selectedClass) {
+ $("li:not(.divider):not(.disabled)", this.$ul).removeClass(this.options.selectedClass);
+ }
+ }
+ },
+
+ /**
+ * Rebuild the plugin.
+ *
+ * Rebuilds the dropdown, the filter and the select all option.
+ */
+ rebuild: function() {
+ this.$ul.html('');
+
+ // Important to distinguish between radios and checkboxes.
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
+
+ this.buildSelectAll();
+ this.buildDropdownOptions();
+ this.buildFilter();
+
+ this.updateButtonText();
+ this.updateSelectAll();
+
+ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
+ this.disable();
+ }
+ else {
+ this.enable();
+ }
+
+ if (this.options.dropRight) {
+ this.$ul.addClass('pull-right');
+ }
+ },
+
+ /**
+ * The provided data will be used to build the dropdown.
+ */
+ dataprovider: function(dataprovider) {
+
+ var groupCounter = 0;
+ var $select = this.$select.empty();
+
+ $.each(dataprovider, function (index, option) {
+ var $tag;
+
+ if ($.isArray(option.children)) { // create optiongroup tag
+ groupCounter++;
+
+ $tag = $('').attr({
+ label: option.label || 'Group ' + groupCounter,
+ disabled: !!option.disabled
+ });
+
+ forEach(option.children, function(subOption) { // add children option tags
+ $tag.append($('').attr({
+ value: subOption.value,
+ label: subOption.label || subOption.value,
+ title: subOption.title,
+ selected: !!subOption.selected,
+ disabled: !!subOption.disabled
+ }));
+ });
+ }
+ else {
+ $tag = $('').attr({
+ value: option.value,
+ label: option.label || option.value,
+ title: option.title,
+ selected: !!option.selected,
+ disabled: !!option.disabled
+ });
+ }
+
+ $select.append($tag);
+ });
+
+ this.rebuild();
+ },
+
+ /**
+ * Enable the multiselect.
+ */
+ enable: function() {
+ this.$select.prop('disabled', false);
+ this.$button.prop('disabled', false)
+ .removeClass('disabled');
+ },
+
+ /**
+ * Disable the multiselect.
+ */
+ disable: function() {
+ this.$select.prop('disabled', true);
+ this.$button.prop('disabled', true)
+ .addClass('disabled');
+ },
+
+ /**
+ * Set the options.
+ *
+ * @param {Array} options
+ */
+ setOptions: function(options) {
+ this.options = this.mergeOptions(options);
+ },
+
+ /**
+ * Merges the given options with the default options.
+ *
+ * @param {Array} options
+ * @returns {Array}
+ */
+ mergeOptions: function(options) {
+ return $.extend(true, {}, this.defaults, this.options, options);
+ },
+
+ /**
+ * Checks whether a select all checkbox is present.
+ *
+ * @returns {Boolean}
+ */
+ hasSelectAll: function() {
+ return $('li.multiselect-all', this.$ul).length > 0;
+ },
+
+ /**
+ * Updates the select all checkbox based on the currently displayed and selected checkboxes.
+ */
+ updateSelectAll: function() {
+ if (this.hasSelectAll()) {
+ var allBoxes = $("li:not(.multiselect-item):not(.filter-hidden) input:enabled", this.$ul);
+ var allBoxesLength = allBoxes.length;
+ var checkedBoxesLength = allBoxes.filter(":checked").length;
+ var selectAllLi = $("li.multiselect-all", this.$ul);
+ var selectAllInput = selectAllLi.find("input");
+
+ if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
+ selectAllInput.prop("checked", true);
+ selectAllLi.addClass(this.options.selectedClass);
+ this.options.onSelectAll();
+ }
+ else {
+ selectAllInput.prop("checked", false);
+ selectAllLi.removeClass(this.options.selectedClass);
+ }
+ }
+ },
+
+ /**
+ * Update the button text and its title based on the currently selected options.
+ */
+ updateButtonText: function() {
+ var options = this.getSelected();
+
+ // First update the displayed button text.
+ if (this.options.enableHTML) {
+ $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select));
+ }
+ else {
+ $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select));
+ }
+
+ // Now update the title attribute of the button.
+ $('.multiselect', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
+ },
+
+ /**
+ * Get all selected options.
+ *
+ * @returns {jQUery}
+ */
+ getSelected: function() {
+ return $('option', this.$select).filter(":selected");
+ },
+
+ /**
+ * Gets a select option by its value.
+ *
+ * @param {String} value
+ * @returns {jQuery}
+ */
+ getOptionByValue: function (value) {
+
+ var options = $('option', this.$select);
+ var valueToCompare = value.toString();
+
+ for (var i = 0; i < options.length; i = i + 1) {
+ var option = options[i];
+ if (option.value === valueToCompare) {
+ return $(option);
+ }
+ }
+ },
+
+ /**
+ * Get the input (radio/checkbox) by its value.
+ *
+ * @param {String} value
+ * @returns {jQuery}
+ */
+ getInputByValue: function (value) {
+
+ var checkboxes = $('li input', this.$ul);
+ var valueToCompare = value.toString();
+
+ for (var i = 0; i < checkboxes.length; i = i + 1) {
+ var checkbox = checkboxes[i];
+ if (checkbox.value === valueToCompare) {
+ return $(checkbox);
+ }
+ }
+ },
+
+ /**
+ * Used for knockout integration.
+ */
+ updateOriginalOptions: function() {
+ this.originalOptions = this.$select.clone()[0].options;
+ },
+
+ asyncFunction: function(callback, timeout, self) {
+ var args = Array.prototype.slice.call(arguments, 3);
+ return setTimeout(function() {
+ callback.apply(self || window, args);
+ }, timeout);
+ },
+
+ setAllSelectedText: function(allSelectedText) {
+ this.options.allSelectedText = allSelectedText;
+ this.updateButtonText();
+ }
+ };
+
+ $.fn.multiselect = function(option, parameter, extraOptions) {
+ return this.each(function() {
+ var data = $(this).data('multiselect');
+ var options = typeof option === 'object' && option;
+
+ // Initialize the multiselect.
+ if (!data) {
+ data = new Multiselect(this, options);
+ $(this).data('multiselect', data);
+ }
+
+ // Call multiselect method.
+ if (typeof option === 'string') {
+ data[option](parameter, extraOptions);
+
+ if (option === 'destroy') {
+ $(this).data('multiselect', false);
+ }
+ }
+ });
+ };
+
+ $.fn.multiselect.Constructor = Multiselect;
+
+ $(function() {
+ $("select[data-role=multiselect]").multiselect();
+ });
+
+}(window.jQuery);
diff --git a/cobra/templates/asset/js/report.js b/cobra/templates/asset/js/report.js
index a05f9718..21508533 100644
--- a/cobra/templates/asset/js/report.js
+++ b/cobra/templates/asset/js/report.js
@@ -27,7 +27,7 @@ var score2level = {
$(function () {
var current_tab = '';
var c_tab = getParameterByName('t');
- if (c_tab !== null && c_tab !== '' && ['inf', 'tar', 'vul'].indexOf(c_tab) >= 0) {
+ if (c_tab !== null && c_tab !== '' && ['inf', 'tar', 'fil','vul'].indexOf(c_tab) >= 0) {
current_tab = c_tab;
$(".nav-tabs li").removeClass('active');
$("a[data-id=" + c_tab + "]").parent('li').addClass('active');
@@ -191,10 +191,17 @@ $(function () {
if (s_sid !== null) {
s_sid = '&s_sid=' + $('#search_target').val();
}
- if (current_tab === '') {
- current_tab = 'inf';
+
+ var url = '';
+ if (current_tab === '' | current_tab === 'inf') {
+ url = '?t=' + current_tab + sid + s_sid;
+ } else if (current_tab === 'vul') {
+ url = '?t=' + current_tab + sid + s_sid + vulnerabilities_list.filter_url() + v;
+ } else if (current_tab === 'fil') {
+ url = '?t=' + current_tab + sid + s_sid;
+ } else if (current_tab === 'tar') {
+ url = '?t=' + current_tab + sid + s_sid;
}
- url = '?t=' + current_tab + sid + s_sid + vulnerabilities_list.filter_url() + v;
window.history.pushState("CobraState", "Cobra", url);
},
get: function (on_filter) {
@@ -275,10 +282,11 @@ $(function () {
vul_list_origin = result.result.scan_data;
rule_filter = result.result.rule_filter;
// rule filter
- $('#search_rule').empty();
- $('#search_rule').append('');
+ $search_rule = $('#search_rule');
+ $search_rule.empty();
+ $search_rule.append('');
for (var key in rule_filter) {
- $('#search_rule').append('');
+ $search_rule.append('');
}
// Search vulnerability type
diff --git a/cobra/templates/asset/js/search.js b/cobra/templates/asset/js/search.js
new file mode 100644
index 00000000..f8c964e9
--- /dev/null
+++ b/cobra/templates/asset/js/search.js
@@ -0,0 +1,59 @@
+$(document).ready(function () {
+ $('#rule_filter').multiselect({
+ enableClickableOptGroups: false
+ });
+});
+
+function createTable(table, data) {
+ // 清空数据
+ table.empty();
+
+ // 表头
+ var thead = $('');
+ var trs = $('
');
+ trs.append($('Target | '));
+ trs.append($('Branch / Tag | '));
+ var rules = Object.keys(data[0].search_result).sort();
+ for (var i = 0; i < rules.length; i++) {
+ trs.append($('' + rule_ids[rules[i]] + ' | '));
+ }
+ thead.append(trs);
+ table.append(thead);
+
+ // 填充内容
+ var tbody = $('');
+ for (i = 0; i < data.length; i++) {
+ // 每一行
+ var row_data = data[i];
+ trs = $('
');
+ // target
+ var s_sid = row_data.target_info.sid;
+ var target = row_data.target_info.target;
+ var branch = row_data.target_info.branch;
+ trs.append($('' + target + ' | '));
+ trs.append($('' + branch + ' | '));
+ // 漏洞数量
+ for (var j = 0; j < rules.length; j++) {
+ trs.append($('' + row_data.search_result[rules[j]] + ' | '));
+ }
+ tbody.append(trs);
+ }
+ table.append(tbody);
+}
+
+$('#submit_search').click(function () {
+ $.ajax({
+ type: 'POST',
+ url: '/api/search',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({sid: getParameterByName('sid'), rule_id: $('#rule_filter').val()}),
+ dataType: 'json',
+ success: function (result) {
+ if (result.code === 1001) {
+ createTable($('#search_table'), result.result);
+ } else {
+ alert(result.msg);
+ }
+ }
+ })
+});
\ No newline at end of file
diff --git a/cobra/templates/summary.html b/cobra/templates/summary.html
index f799b016..23c7b04f 100644
--- a/cobra/templates/summary.html
+++ b/cobra/templates/summary.html
@@ -12,9 +12,11 @@
+
+
Task Information
@@ -395,6 +482,7 @@ Running Targets
+