├── inc └── __init__.py ├── .gitignore ├── .dockerignore ├── env.example ├── docker ├── start.sh ├── patch-crontab.py └── my_mem.cnf ├── doc └── refman.pdf ├── __init__.py ├── db ├── createdatabase.sql ├── clear.sql └── ddl.sql ├── static ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── css │ ├── jquery-cron.css │ ├── asprom.css │ ├── bootstrap-dialog.min.css │ ├── bootstrap-table.min.css │ ├── bootstrap-table.css │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.css │ └── bootstrap-editable.css └── js │ ├── prettycron.js │ ├── jquery-cron-min.js │ ├── asprom.js │ ├── bootstrap-dialog.min.js │ └── bootstrap-table.min.js ├── requirements.txt ├── etc └── asprom.cfg ├── Dockerfile ├── aspromMetrics.py ├── docker-compose.yml ├── supplemental ├── apache-definition └── init-script ├── views ├── posture.tpl ├── baseline.tpl ├── alerts-exposed.tpl ├── alerts-closed.tpl ├── schedule.tpl ├── addjob.tpl ├── editjob.tpl └── base.tpl ├── README.md ├── aspromNagiosCheck.py ├── aspromScan.py ├── LICENSE └── aspromGUI.py /inc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.swp 3 | *.bak 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | supplemental 3 | doc 4 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | DOCKER_SOCKET=/var/run/docker.sock 2 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cron 3 | python3 aspromGUI.py 4 | -------------------------------------------------------------------------------- /doc/refman.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimoniac/asprom/HEAD/doc/refman.pdf -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py file 2 | 3 | import pymysql 4 | pymysql.install_as_MySQLdb() 5 | -------------------------------------------------------------------------------- /db/createdatabase.sql: -------------------------------------------------------------------------------- 1 | create database asprom; 2 | grant all privileges on asprom.* to asprom@localhost identified by 'asprom'; 3 | 4 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimoniac/asprom/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimoniac/asprom/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimoniac/asprom/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /db/clear.sql: -------------------------------------------------------------------------------- 1 | truncate table criticality; 2 | truncate table servicelog; 3 | truncate table changelog; 4 | truncate table machinelog;; 5 | delete from services; 6 | delete from machines; 7 | truncate table scanlog; 8 | 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyascii==0.3.2 2 | mysqlclient==2.2.4 3 | python-crontab==3.2.0 4 | netaddr==1.3.0 5 | paste==3.10.1 6 | bottle==0.13.1 7 | config==0.4.2 8 | croniter==3.0.3 9 | prometheus-client==0.20.0 10 | python-nmap==0.7.1 11 | -------------------------------------------------------------------------------- /etc/asprom.cfg: -------------------------------------------------------------------------------- 1 | # database 2 | db: { 3 | 'host':"mysql" 4 | 'user':"asprom" 5 | 'passwd':"asprom" 6 | 'db':"asprom" 7 | } 8 | # webserver parameters 9 | server: { 10 | 'listen': '0.0.0.0' 11 | 'port': 8080 12 | 'debug': True 13 | } 14 | 15 | # miscellaneous 16 | misc: { 17 | 'url':"http://localhost:8080" 18 | } 19 | 20 | -------------------------------------------------------------------------------- /docker/patch-crontab.py: -------------------------------------------------------------------------------- 1 | --- crontab.py 2024-09-17 06:56:19.752610239 +0000 2 | +++ 2.py 2024-09-17 06:57:56.279127134 +0000 3 | @@ -215,7 +215,7 @@ 4 | return str(self) == other 5 | 6 | 7 | -class CronTab: 8 | +class CronTab(object): 9 | """ 10 | Crontab object which can access any time based cron using the standard. 11 | 12 | -------------------------------------------------------------------------------- /static/css/jquery-cron.css: -------------------------------------------------------------------------------- 1 | .cron-button { 2 | height: 16px; 3 | padding-left: 20px; 4 | margin-left: 5px; 5 | background-repeat: no-repeat; 6 | background-position: center center; 7 | cursor: pointer; 8 | } 9 | .cron-changed { 10 | padding-top: 5px; 11 | padding-bottom: 5px; 12 | background-color: #fdd; 13 | } 14 | .cron-controls { 15 | margin-left: 10px; 16 | color: #c77; 17 | font-size: 0.9em; 18 | } 19 | .cron-controls > span.cron-loading { 20 | background-image: url('img/loading.gif'); 21 | } -------------------------------------------------------------------------------- /docker/my_mem.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | performance_schema = off 3 | key_buffer_size = 16M 4 | tmp_table_size = 1M 5 | innodb_buffer_pool_size = 1M 6 | innodb_log_buffer_size = 1M 7 | max_connections = 20 8 | sort_buffer_size = 64M 9 | read_buffer_size = 256K 10 | read_rnd_buffer_size = 512K 11 | join_buffer_size = 128K 12 | thread_stack = 196K 13 | max-heap-table-size = 32M 14 | thread-cache-size = 50 15 | open-files-limit = 65535 16 | table-definition-cache = 1024 17 | table-open-cache = 2048 18 | 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | ENV TINI_VERSION=v0.19.0 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 5 | RUN chmod +x /tini 6 | COPY requirements.txt /tmp/ 7 | RUN apt-get update && \ 8 | apt-get -y install cron nmap patch libmariadb3 python3-minimal python3-pip \ 9 | default-libmysqlclient-dev build-essential pkg-config && \ 10 | pip install --break-system-packages -r /tmp/requirements.txt && \ 11 | apt-get -y autoremove python3-dev python3-pip default-libmysqlclient-dev \ 12 | build-essential pkg-config && \ 13 | rm -rf /var/lib/apt/lists/* 14 | WORKDIR /asprom 15 | COPY . . 16 | # fix old style class in python-crontab leading to exception 17 | RUN patch -p0 /usr/local/lib/python3.11/dist-packages/crontab.py < docker/patch-crontab.py 18 | RUN chmod 640 aspromNagiosCheck.py 19 | EXPOSE 8080 20 | ENTRYPOINT ["/tini", "--"] 21 | CMD ["docker/start.sh"] 22 | 23 | -------------------------------------------------------------------------------- /aspromMetrics.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Sep 05, 2024 3 | 4 | @author stefankn 5 | @namespace asprom.aspromNagiosCheck 6 | small and nice metrics server for asprom 7 | ''' 8 | from inc.asprom import initDB, closeDB, AspromModel, Cfg 9 | from time import sleep 10 | from prometheus_client import start_http_server, Gauge 11 | from pprint import pprint 12 | 13 | localconf = Cfg() 14 | 15 | alertsExposed = Gauge('alerts_exposed', 'These Ports are unintentionally open and therefore to be checked with the highest priority.') 16 | alertsClosed = Gauge('alerts_closed', 'These Ports are unintentionally open and therefore to be checked with the highest priority.') 17 | 18 | initDB(localconf) 19 | M = AspromModel() 20 | 21 | def refreshMetrics(): 22 | 23 | alertsExposed.set(len(M.getAlertsExposed())) 24 | alertsClosed.set(len(M.getAlertsClosed())) 25 | 26 | if __name__ == '__main__': 27 | 28 | pprint("starting asprom metrics server") 29 | # Start up the server to expose the metrics. 30 | start_http_server(5000) 31 | 32 | while True: 33 | sleep(5) 34 | refreshMetrics() 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | asprom: 4 | restart: always 5 | environment: 6 | PYTHONUNBUFFERED: 1 7 | image: asprom 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | depends_on: 12 | - mysql 13 | volumes: 14 | - crontabs:/var/spool/cron/crontabs 15 | ports: 16 | - 8080:8080 17 | asprom-metrics: 18 | restart: always 19 | environment: 20 | PYTHONUNBUFFERED: 1 21 | image: asprom 22 | command: 23 | - python3 24 | - aspromMetrics.py 25 | depends_on: 26 | - mysql 27 | ports: 28 | - 5000:5000 29 | mysql: 30 | restart: always 31 | image: mysql:8.0 32 | cap_add: 33 | - SYS_NICE 34 | environment: 35 | MYSQL_USER: 'asprom' 36 | MYSQL_PASSWORD: 'asprom' 37 | MYSQL_DATABASE: 'asprom' 38 | MYSQL_RANDOM_ROOT_PASSWORD: 'true' 39 | volumes: 40 | - "mysqldata:/var/lib/mysql" 41 | - "./db/ddl.sql:/docker-entrypoint-initdb.d/1.sql" 42 | - "./docker/my_mem.cnf:/etc/mysql/conf.d/my_mem.cnf" 43 | volumes: 44 | mysqldata: 45 | crontabs: 46 | 47 | -------------------------------------------------------------------------------- /supplemental/apache-definition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirect / /alerts-exposed 5 | 6 | # Proxy Config 7 | ProxyPreserveHost On 8 | ProxyRequests On 9 | ProxyVia On 10 | ProxyPassMatch /(.+) http://127.0.0.1:8080/$1 11 | ProxyPassReverse / http://127.0.0.1:8080/ 12 | 13 | # SSL Config 14 | SSLEngine on 15 | SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem 16 | SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key 17 | 18 | SSLOptions +StdEnvVars 19 | 20 | 21 | SSLOptions +StdEnvVars 22 | 23 | BrowserMatch "MSIE [2-6]" \ 24 | nokeepalive ssl-unclean-shutdown \ 25 | downgrade-1.0 force-response-1.0 26 | # MSIE 7 and newer should be able to use keepalive 27 | BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown 28 | 29 | # Auth Config 30 | 31 | AuthType Basic 32 | AuthName "asprom GUI Login" 33 | AuthBasicProvider file 34 | AuthUserFile /home/asprom/asprom/.htpasswd 35 | Require valid-user 36 | #Order Deny,Allow 37 | #Deny from all 38 | #Allow from 192.168.0 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /static/css/asprom.css: -------------------------------------------------------------------------------- 1 | .ml10 { 2 | margin-left: 10px; 3 | } 4 | 5 | .infobox { 6 | margin-top: 15px; 7 | margin-bottom: 10px !important; 8 | } 9 | 10 | .glyphicon { 11 | text-shadow: 0 0 1px black; 12 | } 13 | .glyphicon-ok { 14 | color: #00EE00; 15 | } 16 | 17 | .glyphicon-remove { 18 | color: #FF3300; 19 | } 20 | 21 | .glyphicon-star { 22 | color: #FFCC00; 23 | } 24 | 25 | .glyphicon-search { 26 | color: #0000EE; 27 | } 28 | .glyphicon-edit { 29 | color: #0000EE; 30 | } 31 | 32 | 33 | .row-critical, .row-critical input { 34 | background-color: #FFAAAA; 35 | } 36 | 37 | .row-warning, .row-warning input { 38 | background-color: #FFFFAA; 39 | } 40 | 41 | .nopad { 42 | vertical-align: middle; 43 | padding: 0 !important; 44 | } 45 | 46 | .nopad input { 47 | box-shadow: none !important; 48 | border-radius: none !important; 49 | border: 0; 50 | 51 | } 52 | 53 | toggle { 54 | cursor: pointer; 55 | } 56 | 57 | .danger-2 { 58 | background-image: linear-gradient(to bottom, #FFEEEE 0px, #F7D3D3 100%); 59 | } 60 | 61 | .warning-2 { 62 | background-image: linear-gradient(to bottom, #FAFAF3 0px, #FAFAD0 100%); 63 | } 64 | 65 | .aspinfo { 66 | background-image: linear-gradient(to bottom, #F9FAFF 0px, #D9E0F0 100%); 67 | } 68 | 69 | .aspinfo-2 { 70 | background-image: linear-gradient(to bottom, #FFFFFF 0px, #E9EEFF 100%); 71 | } 72 | 73 | .moretolearn { 74 | display: none; 75 | } 76 | 77 | .container-fluid { 78 | margin-left: 15%; 79 | margin-right: 15%; 80 | margin-top: 10px; 81 | } 82 | 83 | 84 | .xlarge { 85 | font-size: 50px; 86 | text-shadow: 0 0 5px #FF8800; 87 | } -------------------------------------------------------------------------------- /views/posture.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl', title='posture') 2 |
3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 |
IDIPHostnamePortServiceextra infofirst found
21 |
22 |
23 |
24 |

About this view

25 |

The security posture is the actual profile of your networks found by forensic means.

26 |

You may rescan single ports by clicking the "rescan" button .

27 |

Learn more

28 |
29 | -------------------------------------------------------------------------------- /static/css/bootstrap-dialog.min.css: -------------------------------------------------------------------------------- 1 | .bootstrap-dialog .modal-header{border-top-left-radius:4px;border-top-right-radius:4px}.bootstrap-dialog .bootstrap-dialog-title{color:#fff;display:inline-block}.bootstrap-dialog.type-default .bootstrap-dialog-title{color:#333}.bootstrap-dialog.size-normal .bootstrap-dialog-title{font-size:16px}.bootstrap-dialog.size-large .bootstrap-dialog-title{font-size:24px}.bootstrap-dialog .bootstrap-dialog-close-button{float:right;filter:alpha(opacity=90);-moz-opacity:.9;-khtml-opacity:.9;opacity:.9}.bootstrap-dialog.size-normal .bootstrap-dialog-close-button{font-size:20px}.bootstrap-dialog.size-large .bootstrap-dialog-close-button{font-size:30px}.bootstrap-dialog .bootstrap-dialog-close-button:hover{cursor:pointer;filter:alpha(opacity=100);-moz-opacity:1;-khtml-opacity:1;opacity:1}.bootstrap-dialog.size-normal .bootstrap-dialog-message{font-size:14px}.bootstrap-dialog.size-large .bootstrap-dialog-message{font-size:18px}.bootstrap-dialog.type-default .modal-header{background-color:#fff}.bootstrap-dialog.type-info .modal-header{background-color:#5bc0de}.bootstrap-dialog.type-primary .modal-header{background-color:#428bca}.bootstrap-dialog.type-success .modal-header{background-color:#5cb85c}.bootstrap-dialog.type-warning .modal-header{background-color:#f0ad4e}.bootstrap-dialog.type-danger .modal-header{background-color:#d9534f}.bootstrap-dialog .bootstrap-dialog-button-icon{margin-right:3px}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0{-moz-transform:rotate(0)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0{-o-transform:rotate(0)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0{-ms-transform:rotate(0)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0{transform:rotate(0)}100%{transform:rotate(359deg)}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asprom - Assault Profile Monitor 2 | 3 | asprom is a network security compliance scanner that monitors Layer 4 firewall configurations. It allows you to define service profiles for your networks and automatically scans them using nmap to detect any deviations from the established baseline. 4 | 5 | This tool helps ensure compliance with security standards such as PCI-DSS, BSI-Grundschutz, and ISO/IEC 27001. 6 | 7 | ## Quick Start with Docker 8 | 9 | The easiest way to get started is using Docker, which includes all dependencies: 10 | 11 | ```bash 12 | cp env.example .env 13 | docker-compose up -d 14 | ``` 15 | 16 | Once running, access the GUI at [http://localhost:8080](http://localhost:8080). 17 | 18 | ## Getting Started 19 | 20 | 1. Navigate to the "Schedule" tab 21 | 2. Configure a scan target: 22 | - Enter a hostname, IP address, or IP range 23 | - Leave "port range" and "extra parameters" empty initially 24 | 3. Click "Add Job" to create the scan 25 | 4. Click the magnifying glass icon to execute the scan immediately 26 | 5. Wait for the scan completion notification 27 | 28 | ## Managing Alerts 29 | 30 | After scanning, you'll find detected services under the "Alerts: Exposed" tab. For each alert, you can: 31 | 32 | - Click the "star" icon to mark for mitigation 33 | - Click the "approve" icon to accept it as part of your baseline (requires business justification) 34 | 35 | Once you've processed alerts for all your IP ranges, your initial configuration is complete. asprom will now monitor these ranges and alert you to any new services that appear. 36 | 37 | ## Monitoring Integration 38 | 39 | ### Prometheus Metrics 40 | Access metrics about open ports and baseline deviations at: 41 | [http://localhost:5000/metrics](http://localhost:5000/metrics) 42 | 43 | ### Nagios Integration 44 | Use `aspromNagiosCheck.py` as a standard Nagios plugin to receive active alerts. The plugin will return CRITICAL status when unauthorized services are detected. 45 | 46 | -------------------------------------------------------------------------------- /aspromNagiosCheck.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 23, 2014 3 | 4 | @author stefankn 5 | @namespace asprom.aspromNagiosCheck 6 | This file is invoked from the CLI and can be directly used as a nagios plugin. 7 | if any of the services on the alerts-exposed or alerts-closed views 8 | are marked as critical, 9 | this script terminates with a return value of 2. 10 | if none are marked as critical, but at least one is marked as warning, 11 | this script terminates with a return value of 1. 12 | Else, it terminates with a value of 0 signalling everything is alright. 13 | ''' 14 | from inc.asprom import initDB, closeDB, AspromModel, Cfg, genMessages 15 | import sys 16 | 17 | def main(): 18 | exitstate = 0 19 | msg = "" 20 | 21 | localconf = Cfg() 22 | initDB(localconf) 23 | M = AspromModel() 24 | 25 | #exposed services 26 | messageCritExposed, messageWarnExposed = genMessages(M.getAlertsExposed()) 27 | 28 | #closed services 29 | messageCritClosed, messageWarnClosed = genMessages(M.getAlertsClosed()) 30 | 31 | closeDB() 32 | 33 | # Start up the server to expose the metrics. 34 | start_http_server(5000) 35 | if len(messageCritExposed): 36 | msg += 'CRITICAL-EXPOSED: ' + " | ".join(messageCritExposed) + "\n" 37 | exitstate = 2 38 | if len(messageCritClosed): 39 | msg += 'CRITICAL-CLOSED: ' + " | ".join(messageCritClosed) + "\n" 40 | exitstate = 2 41 | if len(messageWarnExposed): 42 | msg += 'WARNING-EXPOSED: ' + " | ".join(messageWarnExposed) + "\n" 43 | exitstate = exitstate or 1 44 | if len(messageWarnClosed): 45 | msg += 'WARNING-CLOSED: ' + " | ".join(messageWarnClosed) + "\n" 46 | exitstate = exitstate or 1 47 | 48 | if not exitstate: 49 | msg = 'all Profiles nominal.' 50 | 51 | msg += 'Profiling URL: ' + localconf['misc']['url'] 52 | 53 | print(msg) 54 | sys.exit(exitstate) 55 | 56 | 57 | if __name__ == '__main__': 58 | main() 59 | -------------------------------------------------------------------------------- /views/baseline.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl', title='baseline') 2 |
3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 |
IDIPHostnamePortServiceextra infodate of approvalbusiness justification
22 |
23 |
24 |
25 |

About this view

26 |

The "baseline" speficies the attack profile your network presents as it should be.

27 |

It lists only the services that should be available including the reason why (business justification).
28 | You may rescan single ports by clicking "rescan" or delete them using "delete" .

29 |

Learn more

30 |
31 | -------------------------------------------------------------------------------- /views/alerts-exposed.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl', title='alerts-exposed') 2 |
3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 |
IDIPHostnamePortServiceextra infofirst found
21 |
22 |
23 |
24 |

About this view

25 |

These Ports are unintentionally open and therefore to be checked with the highest priority.

26 |

You can temporarily mark an alert as non-critical during your research by clicking the star button . 27 | To get completely rid of an alert, close the port and rescan using the magnifying glass - or add it to the "baseline" by 28 | clicking the green approval button and specifying a business justification (case id, ticket number etc.).

29 |

Learn more

30 |
31 | -------------------------------------------------------------------------------- /views/alerts-closed.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl', title='alerts-closed') 2 |
3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 |
IDIPHostnamePortServiceextra infodate of approvalbusiness justification
22 |
23 |
24 |
25 |

About this view

26 |

These Ports are closed, but the "baseline" specifies a business justification for them to be open.

27 |

Normally, this is not a security risk, but may hint at a service failure. These alerts are non-critical by default. 28 | You can mark an alert as critical by clicking the star button . 29 | To get completely rid of an alert, reopen the port and rescan using the magnifying glass - 30 | or remove it from the "baseline" by clicking the red delete button and entering a business justification. 31 |

Learn more

32 |
33 | -------------------------------------------------------------------------------- /aspromScan.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 23, 2014 3 | 4 | @author stefankn 5 | @namespace asprom.aspromScan 6 | this file is invoked on the CLI as a wrapper script to nmap. 7 | when invoked from the command line, the scan() method is called. 8 | ''' 9 | import argparse 10 | import re 11 | from inc.asprom import scan, initDB, closeDB, Cfg 12 | 13 | 14 | def main(): 15 | ''' 16 | parse arguments from command line. 17 | 18 | 19 | > usage: 20 | 21 | aspromScan.py [-h] [-o EXTRA_OPTIONS] [-s SENSOR] [-p PORT_RANGE] 22 | [-j JOB_ID] TARGET 23 | 24 | Scans an IP Range for asprom. Needs nmap installed on the sensor host. 25 | 26 | 27 | positional arguments: 28 | TARGET the hostname/ip/ip range to be scanned 29 | 30 | optional arguments: 31 | 32 | -h, --help show this help message and exit 33 | 34 | -o EXTRA_OPTIONS, --extra-options EXTRA_OPTIONS 35 | extra options to be passed to nmap 36 | 37 | -s SENSOR, --sensor SENSOR 38 | start scanning on another sensor 39 | 40 | -p PORT_RANGE, --port-range PORT_RANGE 41 | set custom port range to be scanned 42 | 43 | -j JOB_ID, --job-id JOB_ID 44 | set arbitrary job id (used by aspromGUI and cron) 45 | ''' 46 | parser = argparse.ArgumentParser(description='''Scans an IP Range for 47 | asprom. Needs nmap installed on the sensor host.''') 48 | parser.add_argument('target', metavar="TARGET", 49 | help='the hostname/ip/ip range to be scanned') 50 | parser.add_argument('-o', '--extra-options', default='', 51 | help='extra options to be passed to nmap') 52 | parser.add_argument('-s', '--sensor', default='localhost', 53 | help='start scanning on another sensor') 54 | parser.add_argument('-p', '--port-range', default=None, 55 | help='set custom port range to be scanned') 56 | parser.add_argument('-j', '--job-id', default=None, 57 | help='set arbitrary job id (used by aspromGUI and cron)') 58 | 59 | args = parser.parse_args() 60 | 61 | localconf = Cfg() 62 | initDB(localconf) 63 | scan(args.target, args.port_range, args.extra_options, args.job_id) 64 | closeDB() 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /views/schedule.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl', title='schedule') 2 | 3 | 4 | 5 |
6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 |
IDwhenIP RangeSensorLast RunNext RunLast Statusextra ParametersPort range
25 | 26 |
27 |
28 |
29 |

About this view

30 |

In the schedule, you define which network ranges should be checked, and when they should be checked.

31 |

You can change the settings of each job by clicking the edit button . 32 | You can also run each scan right now by clicking the magnifying glass . 33 | When you delete a job in this interface, asprom will comment it out in the users' crontab.

34 |

Learn more

35 |
36 | -------------------------------------------------------------------------------- /views/addjob.tpl: -------------------------------------------------------------------------------- 1 |

Add Schedule

2 | 3 | 49 | 50 |
51 | Schedule 52 | 53 |
54 |
55 |
56 |
57 | IP Range 58 | 59 |
60 |
61 |
62 | Port Range 63 | 64 |
65 |
66 |
67 | extra Parameters 68 | 69 |
70 |
71 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /views/editjob.tpl: -------------------------------------------------------------------------------- 1 |

Edit Schedule for {{iprange}}

2 | 3 | 49 | 50 |
51 | Schedule 52 | 53 |
54 |
55 |
56 |
57 | IP Range 58 | 59 |
60 |
61 |
62 | Port Range 63 | 64 |
65 |
66 |
67 | extra Parameters 68 | 69 |
70 |
71 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /views/base.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | asprom: {{title}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 |
30 |
31 |
32 |
33 |

asprom assault profile monitor

34 | 41 |
42 |
43 |
44 |
45 |

46 |
47 |
48 |
49 |
50 |
51 |
52 | {{!base}} 53 |

last changes

54 |

55 |
56 |
57 |
58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /static/css/bootstrap-table.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | * bootstrap-table - v1.3.0 - 2014-10-16 3 | * https://github.com/wenzhixin/bootstrap-table 4 | * Copyright (c) 2014 zhixin wen 5 | * Licensed MIT License 6 | */ 7 | 8 | .table{margin-bottom:0!important;border-bottom:1px solid #ddd;border-collapse:collapse!important;border-radius:1px}.fixed-table-container{position:relative;clear:both;border:1px solid #ddd;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px}.fixed-table-header{overflow:hidden;border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0}.fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.fixed-table-container table{width:100%}.fixed-table-container thead th{height:0;padding:0;margin:0;border-left:1px solid #ddd}.fixed-table-container thead th:first-child{border-left:none;border-top-left-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px}.fixed-table-container thead th .th-inner{padding:8px;line-height:24px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fixed-table-container thead th .sortable{cursor:pointer}.fixed-table-container tbody td{border-left:1px solid #ddd}.fixed-table-container tbody tr:first-child td{border-top:none}.fixed-table-container tbody td:first-child{border-left:none}.fixed-table-container tbody .selected td{background-color:#f5f5f5}.fixed-table-container .bs-checkbox{text-align:center}.fixed-table-container .bs-checkbox .th-inner{padding:8px 0}.fixed-table-container input[type=checkbox],.fixed-table-container input[type=radio]{margin:0 auto!important}.fixed-table-container .no-records-found{text-align:center}.fixed-table-pagination .pagination,.fixed-table-pagination .pagination-detail{margin-top:10px;margin-bottom:10px}.fixed-table-pagination .pagination a{padding:6px 12px;line-height:1.428571429}.fixed-table-pagination .pagination-info{line-height:34px;margin-right:5px}.fixed-table-pagination .btn-group{position:relative;display:inline-block;vertical-align:middle}.fixed-table-pagination .dropup .dropdown-menu{margin-bottom:0}.fixed-table-pagination .page-list{display:inline-block}.fixed-table-toolbar .columns{margin-left:5px}.fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.fixed-table-toolbar .bars,.fixed-table-toolbar .columns,.fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px;line-height:34px}.fixed-table-pagination li.disabled a{pointer-events:none;cursor:default}.fixed-table-loading{display:none;position:absolute;top:42px;right:0;bottom:0;left:0;z-index:99;background-color:#fff;text-align:center}.fixed-table-body .card-view .title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.fixed-table-body thead th .th-inner{box-sizing:border-box}.table td,.table th{vertical-align:middle;box-sizing:border-box}.fixed-table-toolbar .dropdown-menu{text-align:left;max-height:300px;overflow:auto}.fixed-table-toolbar .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.fixed-table-toolbar .btn-group>.btn-group>.btn{border-radius:0}.fixed-table-toolbar .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.fixed-table-toolbar .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table thead>tr>th{padding:0;margin:0}.pull-right .dropdown-menu{right:0;left:auto}p.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden} 9 | -------------------------------------------------------------------------------- /static/css/bootstrap-table.css: -------------------------------------------------------------------------------- 1 | .table { 2 | margin-bottom: 0 !important; 3 | border-bottom: 1px solid #dddddd; 4 | border-collapse: collapse !important; 5 | border-radius: 1px; 6 | } 7 | 8 | .fixed-table-container { 9 | position: relative; 10 | clear: both; 11 | border: 1px solid #dddddd; 12 | border-radius: 4px; 13 | -webkit-border-radius: 4px; 14 | -moz-border-radius: 4px; 15 | } 16 | 17 | .fixed-table-header { 18 | overflow: hidden; 19 | border-radius: 4px 4px 0 0; 20 | -webkit-border-radius: 4px 4px 0 0; 21 | -moz-border-radius: 4px 4px 0 0; 22 | } 23 | 24 | .fixed-table-body { 25 | overflow-x: auto; 26 | overflow-y: auto; 27 | height: 100%; 28 | } 29 | 30 | .fixed-table-container table { 31 | width: 100%; 32 | } 33 | 34 | .fixed-table-container thead th { 35 | height: 0; 36 | padding: 0; 37 | margin: 0; 38 | border-left: 1px solid #dddddd; 39 | } 40 | 41 | .fixed-table-container thead th:first-child { 42 | border-left: none; 43 | border-top-left-radius: 4px; 44 | -webkit-border-top-left-radius: 4px; 45 | -moz-border-radius-topleft: 4px; 46 | } 47 | 48 | .fixed-table-container thead th .th-inner { 49 | padding: 8px; 50 | line-height: 24px; 51 | vertical-align: top; 52 | overflow: hidden; 53 | text-overflow: ellipsis; 54 | white-space: nowrap; 55 | } 56 | 57 | .fixed-table-container thead th .sortable { 58 | cursor: pointer; 59 | } 60 | 61 | .fixed-table-container tbody td { 62 | border-left: 1px solid #dddddd; 63 | } 64 | 65 | .fixed-table-container tbody tr:first-child td { 66 | border-top: none; 67 | } 68 | 69 | .fixed-table-container tbody td:first-child { 70 | border-left: none; 71 | } 72 | 73 | /* the same color with .active */ 74 | .fixed-table-container tbody .selected td { 75 | background-color: #f5f5f5; 76 | } 77 | 78 | .fixed-table-container .bs-checkbox { 79 | text-align: center; 80 | } 81 | 82 | .fixed-table-container .bs-checkbox .th-inner { 83 | padding: 8px 0; 84 | } 85 | 86 | .fixed-table-container input[type="radio"], 87 | .fixed-table-container input[type="checkbox"] { 88 | margin: 0 auto !important; 89 | } 90 | 91 | .fixed-table-container .no-records-found { 92 | text-align: center; 93 | } 94 | 95 | 96 | .fixed-table-pagination .pagination, 97 | .fixed-table-pagination .pagination-detail { 98 | margin-top: 10px; 99 | margin-bottom: 10px; 100 | } 101 | 102 | .fixed-table-pagination .pagination a { 103 | padding: 6px 12px; 104 | line-height: 1.428571429; 105 | } 106 | 107 | .fixed-table-pagination .pagination-info { 108 | line-height: 34px; 109 | margin-right: 5px; 110 | } 111 | 112 | .fixed-table-pagination .btn-group { 113 | position: relative; 114 | display: inline-block; 115 | vertical-align: middle; 116 | } 117 | 118 | .fixed-table-pagination .dropup .dropdown-menu { 119 | margin-bottom: 0; 120 | } 121 | 122 | .fixed-table-pagination .page-list { 123 | display: inline-block; 124 | } 125 | 126 | .fixed-table-toolbar .columns { 127 | margin-left: 5px; 128 | } 129 | 130 | .fixed-table-toolbar .columns label { 131 | display: block; 132 | padding: 3px 20px; 133 | clear: both; 134 | font-weight: normal; 135 | line-height: 1.428571429; 136 | } 137 | 138 | .fixed-table-toolbar .bars, 139 | .fixed-table-toolbar .search, 140 | .fixed-table-toolbar .columns { 141 | position: relative; 142 | margin-top: 10px; 143 | margin-bottom: 10px; 144 | line-height: 34px; 145 | } 146 | 147 | .fixed-table-pagination li.disabled a { 148 | pointer-events: none; 149 | cursor: default; 150 | } 151 | 152 | .fixed-table-loading { 153 | display: none; 154 | position: absolute; 155 | top: 42px; 156 | right: 0; 157 | bottom: 0; 158 | left: 0; 159 | z-index: 99; 160 | background-color: #fff; 161 | text-align: center; 162 | } 163 | 164 | .fixed-table-body .card-view .title { 165 | font-weight: bold; 166 | display: inline-block; 167 | min-width: 30%; 168 | text-align: left !important; 169 | } 170 | 171 | /* support bootstrap 2 */ 172 | .fixed-table-body thead th .th-inner { 173 | box-sizing: border-box; 174 | } 175 | 176 | .table th, .table td { 177 | vertical-align: middle; 178 | box-sizing: border-box; 179 | } 180 | 181 | .fixed-table-toolbar .dropdown-menu { 182 | text-align: left; 183 | max-height: 300px; 184 | overflow: auto; 185 | } 186 | 187 | .fixed-table-toolbar .btn-group>.btn-group { 188 | display: inline-block; 189 | margin-left: -1px !important; 190 | } 191 | 192 | .fixed-table-toolbar .btn-group>.btn-group>.btn { 193 | border-radius: 0; 194 | } 195 | 196 | .fixed-table-toolbar .btn-group>.btn-group:first-child>.btn { 197 | border-top-left-radius: 4px; 198 | border-bottom-left-radius: 4px; 199 | } 200 | 201 | .fixed-table-toolbar .btn-group>.btn-group:last-child>.btn { 202 | border-top-right-radius: 4px; 203 | border-bottom-right-radius: 4px; 204 | } 205 | 206 | .table>thead>tr>th { 207 | vertical-align: bottom; 208 | border-bottom: 2px solid #ddd; 209 | } 210 | 211 | /* support bootstrap 3 */ 212 | .table thead>tr>th { 213 | padding: 0; 214 | margin: 0; 215 | } 216 | 217 | .pull-right .dropdown-menu { 218 | right: 0; 219 | left: auto; 220 | } 221 | 222 | /* calculate scrollbar width */ 223 | p.fixed-table-scroll-inner { 224 | width: 100%; 225 | height: 200px; 226 | } 227 | 228 | div.fixed-table-scroll-outer { 229 | top: 0; 230 | left: 0; 231 | visibility: hidden; 232 | width: 200px; 233 | height: 150px; 234 | overflow: hidden; 235 | } 236 | -------------------------------------------------------------------------------- /supplemental/init-script: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: asprom 4 | # Required-Start: $network 5 | # Required-Stop: $network 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: asprom assault profile monitor 9 | # Description: starts the asprom GUI. 10 | ### END INIT INFO 11 | 12 | # Author: stefan knott 13 | # 14 | # Please remove the "Author" lines above and replace them 15 | # with your own name if you copy and modify this script. 16 | 17 | # Do NOT "set -e" 18 | 19 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 20 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 21 | 22 | # change this 23 | MAINPATH=/home/asprom/asprom 24 | USER=asprom 25 | 26 | DESC="asprom assault profile monitor" 27 | NAME=aspromGUI.py 28 | DAEMON=/usr/bin/python 29 | DAEMON_ARGS="$MAINPATH/$NAME" 30 | PIDFILE=/var/run/$NAME.pid 31 | SCRIPTNAME=/etc/init.d/$NAME 32 | 33 | # Exit if the package is not installed 34 | [ -x "$DAEMON" ] || exit 0 35 | 36 | # Read configuration variable file if it is present 37 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 38 | 39 | # Load the VERBOSE setting and other rcS variables 40 | . /lib/init/vars.sh 41 | 42 | # Define LSB log_* functions. 43 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present 44 | # and status_of_proc is working. 45 | . /lib/lsb/init-functions 46 | 47 | # 48 | # Function that starts the daemon/service 49 | # 50 | do_start() 51 | { 52 | # Return 53 | # 0 if daemon has been started 54 | # 1 if daemon was already running 55 | # 2 if daemon could not be started 56 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -u $USER --test > /dev/null \ 57 | || return 1 58 | start-stop-daemon --start --quiet --background --pidfile $PIDFILE --exec $DAEMON --user $USER \ 59 | --chuid $USER --startas $DAEMON --make-pidfile --chdir "$MAINPATH" -- \ 60 | $DAEMON_ARGS \ 61 | || return 2 62 | # Add code here, if necessary, that waits for the process to be ready 63 | # to handle requests from services started subsequently which depend 64 | # on this one. As a last resort, sleep for some time. 65 | } 66 | 67 | # 68 | # Function that stops the daemon/service 69 | # 70 | do_stop() 71 | { 72 | # Return 73 | # 0 if daemon has been stopped 74 | # 1 if daemon was already stopped 75 | # 2 if daemon could not be stopped 76 | # other if a failure occurred 77 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME 78 | RETVAL="$?" 79 | [ "$RETVAL" = 2 ] && return 2 80 | # Wait for children to finish too if this is a daemon that forks 81 | # and if the daemon is only ever run from this initscript. 82 | # If the above conditions are not satisfied then add some other code 83 | # that waits for the process to drop all resources that could be 84 | # needed by services started subsequently. A last resort is to 85 | # sleep for some time. 86 | start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON 87 | [ "$?" = 2 ] && return 2 88 | # Many daemons don't delete their pidfiles when they exit. 89 | rm -f $PIDFILE 90 | return "$RETVAL" 91 | } 92 | 93 | # 94 | # Function that sends a SIGHUP to the daemon/service 95 | # 96 | do_reload() { 97 | # 98 | # If the daemon can reload its configuration without 99 | # restarting (for example, when it is sent a SIGHUP), 100 | # then implement that here. 101 | # 102 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 103 | return 0 104 | } 105 | 106 | case "$1" in 107 | start) 108 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 109 | do_start 110 | case "$?" in 111 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 112 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 113 | esac 114 | ;; 115 | stop) 116 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 117 | do_stop 118 | case "$?" in 119 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 120 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 121 | esac 122 | ;; 123 | status) 124 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 125 | ;; 126 | #reload|force-reload) 127 | # 128 | # If do_reload() is not implemented then leave this commented out 129 | # and leave 'force-reload' as an alias for 'restart'. 130 | # 131 | #log_daemon_msg "Reloading $DESC" "$NAME" 132 | #do_reload 133 | #log_end_msg $? 134 | #;; 135 | restart|force-reload) 136 | # 137 | # If the "reload" option is implemented then remove the 138 | # 'force-reload' alias 139 | # 140 | log_daemon_msg "Restarting $DESC" "$NAME" 141 | do_stop 142 | case "$?" in 143 | 0|1) 144 | do_start 145 | case "$?" in 146 | 0) log_end_msg 0 ;; 147 | 1) log_end_msg 1 ;; # Old process is still running 148 | *) log_end_msg 1 ;; # Failed to start 149 | esac 150 | ;; 151 | *) 152 | # Failed to stop 153 | log_end_msg 1 154 | ;; 155 | esac 156 | ;; 157 | *) 158 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 159 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 160 | exit 3 161 | ;; 162 | esac 163 | 164 | : 165 | 166 | -------------------------------------------------------------------------------- /static/js/prettycron.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // prettycron.js 4 | // Generates human-readable sentences from a schedule string in cron format 5 | // 6 | // Based on an earlier version by Pehr Johansson 7 | // http://dsysadm.blogspot.com.au/2012/09/human-readable-cron-expressions-using.html 8 | // 9 | //////////////////////////////////////////////////////////////////////////////////// 10 | // This program is free software: you can redistribute it and/or modify 11 | // it under the terms of the GNU Lesser General Public License as published 12 | // by the Free Software Foundation, either version 3 of the License, or 13 | // (at your option) any later version. 14 | // 15 | // This program is distributed in the hope that it will be useful, 16 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | // GNU Lesser General Public License for more details. 19 | // 20 | // You should have received a copy of the GNU Lesser General Public License 21 | // along with this program. If not, see . 22 | //////////////////////////////////////////////////////////////////////////////////// 23 | 24 | if ((!moment || !later) && (typeof require !== 'undefined')) { 25 | var moment = require('moment'); 26 | var later = require('later').later; 27 | var cronParser = require('later').parse.cron; 28 | } 29 | 30 | (function() { 31 | 32 | /* 33 | * For an array of numbers, e.g. a list of hours in a schedule, 34 | * return a string listing out all of the values (complete with 35 | * "and" plus ordinal text on the last item). 36 | */ 37 | var numberList = function(numbers) { 38 | if (numbers.length < 2) { 39 | //return numbers 40 | return moment.localeData().ordinal(numbers); 41 | } 42 | 43 | var last_val = numbers.pop(); 44 | return numbers.join(', ') + ' and ' + moment.localeData().ordinal(last_val); 45 | }; 46 | 47 | /* 48 | * Parse a number into day of week, or a month name; 49 | * used in dateList below. 50 | */ 51 | var numberToDateName = function(value, type) { 52 | if (type == 'dow') { 53 | return moment().day(value - 1).format('ddd'); 54 | } else if (type == 'mon') { 55 | return moment().month(value - 1).format('MMM'); 56 | } 57 | }; 58 | 59 | /* 60 | * From an array of numbers corresponding to dates (given in type: either 61 | * days of the week, or months), return a string listing all the values. 62 | */ 63 | var dateList = function(numbers, type) { 64 | if (numbers.length < 2) { 65 | return numberToDateName(''+numbers[0], type); 66 | } 67 | 68 | var last_val = '' + numbers.pop(); 69 | var output_text = ''; 70 | 71 | for (var i=0, value; value=numbers[i]; i++) { 72 | if (output_text.length > 0) { 73 | output_text += ', '; 74 | } 75 | output_text += numberToDateName(value, type); 76 | } 77 | return output_text + ' and ' + numberToDateName(last_val, type); 78 | }; 79 | 80 | /* 81 | * Pad to equivalent of sprintf('%02d'). Both moment.js and later.js 82 | * have zero-fill functions, but alas, they're private. 83 | */ 84 | var zeroPad = function(x) { 85 | return (x < 10) ? '0' + x : x; 86 | }; 87 | 88 | //---------------- 89 | 90 | /* 91 | * Given a schedule from later.js (i.e. after parsing the cronspec), 92 | * generate a friendly sentence description. 93 | */ 94 | var scheduleToSentence = function(schedule) { 95 | var output_text = 'Every '; 96 | 97 | if (schedule['h'] && schedule['m'] && schedule['h'].length <= 2 && schedule['m'].length <= 2) { 98 | // If there are only one or two specified values for 99 | // hour or minute, print them in HH:MM format 100 | 101 | var hm = []; 102 | for (var i=0; i < schedule['h'].length; i++) { 103 | for (var j=0; j < schedule['m'].length; j++) { 104 | hm.push(zeroPad(schedule['h'][i]) + ':' + zeroPad(schedule['m'][j])); 105 | } 106 | } 107 | if (hm.length < 2) { 108 | output_text = hm[0]; 109 | } else { 110 | var last_val = hm.pop(); 111 | output_text = hm.join(', ') + ' and ' + last_val; 112 | } 113 | if (!schedule['d'] && !schedule['D']) { 114 | output_text += ' every day'; 115 | } 116 | 117 | } else { 118 | // Otherwise, list out every specified hour/minute value. 119 | 120 | if(schedule['h']) { // runs only at specific hours 121 | if (schedule['m']) { // and only at specific minutes 122 | output_text += numberList(schedule['m']) + ' minute past the ' + numberList(schedule['h']) + ' hour'; 123 | } else { // specific hours, but every minute 124 | output_text += 'minute of ' + numberList(schedule['h']) + ' hour'; 125 | } 126 | } else if(schedule['m']) { // every hour, but specific minutes 127 | if (schedule['m'].length == 1 && schedule['m'][0] == 0) { 128 | output_text += 'hour, on the hour'; 129 | } else { 130 | output_text += numberList(schedule['m']) + ' minute past every hour'; 131 | } 132 | } else { // cronspec has "*" for both hour and minute 133 | output_text += 'minute'; 134 | } 135 | } 136 | 137 | if (schedule['D']) { // runs only on specific day(s) of month 138 | output_text += ' on the ' + numberList(schedule['D']); 139 | if (!schedule['M']) { 140 | output_text += ' of every month'; 141 | } 142 | } 143 | 144 | if (schedule['d']) { // runs only on specific day(s) of week 145 | if (schedule['D']) { 146 | // if both day fields are specified, cron uses both; superuser.com/a/348372 147 | output_text += ' and every '; 148 | } else { 149 | output_text += ' on '; 150 | } 151 | output_text += dateList(schedule['d'], 'dow'); 152 | } 153 | 154 | if (schedule['M']) { 155 | // runs only in specific months; put this output last 156 | output_text += ' in ' + dateList(schedule['M'], 'mon'); 157 | } 158 | 159 | return output_text; 160 | }; 161 | 162 | //---------------- 163 | 164 | /* 165 | * Given a cronspec, return the human-readable string. 166 | */ 167 | var toString = function(cronspec, sixth) { 168 | var schedule = later.parse.cron(cronspec, sixth); 169 | return scheduleToSentence(schedule['schedules'][0]); 170 | }; 171 | 172 | /* 173 | * Given a cronspec, return a friendly string for when it will next run. 174 | * (This is just a wrapper for later.js and moment.js) 175 | */ 176 | var getNext = function(cronspec, sixth) { 177 | var schedule = later.parse.cron(cronspec, sixth); 178 | return moment( 179 | later.schedule(schedule).next() 180 | ).calendar(); 181 | }; 182 | 183 | //---------------- 184 | 185 | // attach ourselves to window in the browser, and to exports in Node, 186 | // so our functions can always be called as prettyCron.toString() 187 | var global_obj = (typeof exports !== "undefined" && exports !== null) ? exports : window.prettyCron = {}; 188 | 189 | global_obj.toString = toString; 190 | global_obj.getNext = getNext; 191 | 192 | }).call(this); 193 | -------------------------------------------------------------------------------- /static/js/jquery-cron-min.js: -------------------------------------------------------------------------------- 1 | (function(e){var n={initial:"* * * * *",minuteOpts:{minWidth:100,itemWidth:30,columns:4,rows:undefined,title:"Minutes Past the Hour"},timeHourOpts:{minWidth:100,itemWidth:20,columns:2,rows:undefined,title:"Time: Hour"},domOpts:{minWidth:100,itemWidth:30,columns:undefined,rows:10,title:"Day of Month"},monthOpts:{minWidth:100,itemWidth:100,columns:2,rows:undefined,title:undefined},dowOpts:{minWidth:100,itemWidth:undefined,columns:undefined,rows:undefined,title:undefined},timeMinuteOpts:{minWidth:100,itemWidth:20,columns:4,rows:undefined,title:"Time: Minute"},effectOpts:{openSpeed:400,closeSpeed:400,openEffect:"slide",closeEffect:"slide",hideOnMouseOut:true},url_set:undefined,customValues:undefined,onChange:undefined,useGentleSelect:false};var y="";for(var u=0;u<60;u++){var t=(u<10)?"0":"";y+="\n"}var d="";for(var u=0;u<24;u++){var t=(u<10)?"0":"";d+="\n"}var v="";for(var u=1;u<32;u++){if(u==1||u==21||u==31){var c="st"}else{if(u==2||u==22){var c="nd"}else{if(u==3||u==23){var c="rd"}else{var c="th"}}}v+="\n"}var h="";var l=["January","February","March","April","May","June","July","August","September","October","November","December"];for(var u=0;u"+l[u]+"\n"}var s="";var g=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];for(var u=0;u"+g[u]+"\n"}var r="";var b=["minute","hour","day","week","month","year"];for(var u=0;u"+b[u]+"\n"}var p={minute:[],hour:["mins"],day:["time"],week:["dow","time"],month:["dom","time"],year:["dom","month","time"]};var w={minute:/^(\*\s){4}\*$/,hour:/^\d{1,2}\s(\*\s){3}\*$/,day:/^(\d{1,2}\s){2}(\*\s){2}\*$/,week:/^(\d{1,2}\s){2}(\*\s){2}\d{1,2}$/,month:/^(\d{1,2}\s){3}\*\s\*$/,year:/^(\d{1,2}\s){4}\*$/};function a(i){if(typeof i=="undefined"){return false}else{return true}}function q(i){return(!a(i)||typeof i=="object")}function z(A,j){if(a(j.customValues)){for(key in j.customValues){if(A==j.customValues[key]){return key}}}var E=/^((\d{1,2}|\*)\s){4}(\d{1,2}|\*)$/;if(typeof A!="string"||!E.test(A)){e.error("cron: invalid initial value");return undefined}var C=A.split(" ");var D=[0,0,1,1,0];var G=[59,23,31,12,6];for(var B=0;B=D[B]){continue}e.error("cron: invalid value found (col "+(B+1)+") in "+o.initial);return undefined}for(var H in w){if(w[H].test(A)){return H}}e.error("cron: valid but unsupported cron format. sorry.");return undefined}function f(j,i){if(!a(z(i.initial,i))){return true}if(!q(i.customValues)){return true}if(a(i.customValues)){for(key in i.customValues){if(w.hasOwnProperty(key)){e.error("cron: reserved keyword '"+key+"' should not be used as customValues key.");return true}}}return false}function k(B){var i=B.data("block");var j=hour=day=month=dow="*";var A=i.period.find("select").val();switch(A){case"minute":break;case"hour":j=i.mins.find("select").val();break;case"day":j=i.time.find("select.cron-time-min").val();hour=i.time.find("select.cron-time-hour").val();break;case"week":j=i.time.find("select.cron-time-min").val();hour=i.time.find("select.cron-time-hour").val();dow=i.dow.find("select").val();break;case"month":j=i.time.find("select.cron-time-min").val();hour=i.time.find("select.cron-time-hour").val();day=i.dom.find("select").val();break;case"year":j=i.time.find("select.cron-time-min").val();hour=i.time.find("select.cron-time-hour").val();day=i.dom.find("select").val();month=i.month.find("select").val();break;default:return A}return[j,hour,day,month,dow].join(" ")}var x={init:function(i){var G=i?i:{};var B=e.extend([],n,G);var j=e.extend({},n.effectOpts,G.effectOpts);e.extend(B,{minuteOpts:e.extend({},n.minuteOpts,j,G.minuteOpts),domOpts:e.extend({},n.domOpts,j,G.domOpts),monthOpts:e.extend({},n.monthOpts,j,G.monthOpts),dowOpts:e.extend({},n.dowOpts,j,G.dowOpts),timeHourOpts:e.extend({},n.timeHourOpts,j,G.timeHourOpts),timeMinuteOpts:e.extend({},n.timeMinuteOpts,j,G.timeMinuteOpts)});if(f(this,B)){return this}var C=[],A="",D=B.customValues;if(a(D)){for(var F in D){A+="\n"}}C.period=e("Every ").appendTo(this).data("root",this);var E=C.period.find("select");E.bind("change.cron",m.periodChanged).data("root",this);if(B.useGentleSelect){E.gentleSelect(j)}C.dom=e(" on the ").appendTo(this).data("root",this);E=C.dom.find("select").data("root",this);if(B.useGentleSelect){E.gentleSelect(B.domOpts)}C.month=e(" of ").appendTo(this).data("root",this);E=C.month.find("select").data("root",this);if(B.useGentleSelect){E.gentleSelect(B.monthOpts)}C.mins=e(" at minutes past the hour ").appendTo(this).data("root",this);E=C.mins.find("select").data("root",this);if(B.useGentleSelect){E.gentleSelect(B.minuteOpts)}C.dow=e(" on ").appendTo(this).data("root",this);E=C.dow.find("select").data("root",this);if(B.useGentleSelect){E.gentleSelect(B.dowOpts)}C.time=e(" at :'),c.header.stateField=f.field),f.radio&&(g="",c.header.stateField=f.field,c.options.singleSelect=!0),e.push(g),e.push(""),e.push('
'),e.push(""))}),this.$header.find("tr").html(e.join("")),this.$header.find("th").each(function(b){a(this).data(d[b])}),this.$container.off("click","th").on("click","th",function(b){c.options.sortable&&a(this).data().sortable&&c.onSort(b)}),!this.options.showHeader||this.options.cardView?(this.$header.hide(),this.$container.find(".fixed-table-header").hide(),this.$loading.css("top",0)):(this.$header.show(),this.$container.find(".fixed-table-header").show(),this.$loading.css("top","37px")),this.$selectAll=this.$header.find('[name="btSelectAll"]'),this.$container.off("click",'[name="btSelectAll"]').on("click",'[name="btSelectAll"]',function(){var b=a(this).prop("checked");c[b?"checkAll":"uncheckAll"]()})},g.prototype.initData=function(a,b){this.data=b?this.data.concat(a):a||this.options.data,this.options.data=this.data,"server"!==this.options.sidePagination&&this.initSort()},g.prototype.initSort=function(){var b=this,c=this.options.sortName,d="desc"===this.options.sortOrder?-1:1,e=a.inArray(this.options.sortName,this.header.fields);-1!==e&&this.data.sort(function(a,g){var h=f(b.header,b.header.sorters[e],[a[c],g[c]]);return void 0!==h?d*h:a[c]===g[c]?0:a[c]').appendTo(this.$toolbar).append(a(this.options.toolbar)),f=['
'],this.options.showRefresh&&f.push('"),this.options.showToggle&&f.push('"),this.options.showColumns&&(f.push(b('
',this.options.showRefresh||this.options.showToggle?"btn-group":""),'",'","
")),f.push("
"),f.length>2&&this.$toolbar.append(f.join("")),this.options.showRefresh&&this.$toolbar.find('button[name="refresh"]').off("click").on("click",a.proxy(this.refresh,this)),this.options.showToggle&&this.$toolbar.find('button[name="toggle"]').off("click").on("click",function(){e.options.cardView=!e.options.cardView,e.initHeader(),e.initBody()}),this.options.showColumns&&(c=this.$toolbar.find(".keep-open"),c.find("li").off("click").on("click",function(a){a.stopImmediatePropagation()}),c.find("input").off("click").on("click",function(){var b=a(this);e.toggleColumn(b.val(),b.prop("checked"),!1),e.trigger("column-switch",a(this).data("field"),b.prop("checked"))})),this.options.search&&(f=[],f.push('"),this.$toolbar.append(f.join("")),d=this.$toolbar.find(".search input"),d.off("keyup").on("keyup",function(a){clearTimeout(g),g=setTimeout(function(){e.onSearch(a)},500)}))},g.prototype.onSearch=function(b){var c=a.trim(a(b.currentTarget).val());a(b.currentTarget).val(c),c!==this.searchText&&(this.searchText=c,this.options.pageNumber=1,this.initSearch(),this.updatePagination())},g.prototype.initSearch=function(){var b=this;if("server"!==this.options.sidePagination){var c=this.searchText&&this.searchText.toLowerCase();this.data=c?a.grep(this.options.data,function(d,e){g=a.isNumeric(g)?parseInt(g,10):g;for(var g in d){var h=d[g];if(h=f(b.header,b.header.formatters[a.inArray(g,b.header.fields)],[h,d,e],h),-1!==a.inArray(g,b.header.fields)&&("string"==typeof h||"number"==typeof h)&&-1!==(h+"").toLowerCase().indexOf(c))return!0}return!1}):this.options.data}},g.prototype.initPagination=function(){if(this.$pagination=this.$container.find(".fixed-table-pagination"),this.options.pagination){var c,d,e,f,g,h,i,j,k,l=this,m=[],n=this.searchText?this.data:this.options.data;"server"!==this.options.sidePagination&&(this.options.totalRows=n.length),this.totalPages=0,this.options.totalRows&&(this.totalPages=~~((this.options.totalRows-1)/this.options.pageSize)+1),this.totalPages>0&&this.options.pageNumber>this.totalPages&&(this.options.pageNumber=this.totalPages),this.pageFrom=(this.options.pageNumber-1)*this.options.pageSize+1,this.pageTo=this.options.pageNumber*this.options.pageSize,this.pageTo>this.options.totalRows&&(this.pageTo=this.options.totalRows),m.push('
','',this.options.formatShowingRows(this.pageFrom,this.pageTo,this.options.totalRows),""),m.push('');var o=['','",'"),m.push(this.options.formatRecordsPerPage(o.join(""))),m.push(""),m.push("
",'"),this.$pagination.html(m.join("")),f=this.$pagination.find(".page-list a"),g=this.$pagination.find(".page-first"),h=this.$pagination.find(".page-pre"),i=this.$pagination.find(".page-next"),j=this.$pagination.find(".page-last"),k=this.$pagination.find(".page-number"),this.options.pageNumber<=1&&(g.addClass("disabled"),h.addClass("disabled")),this.options.pageNumber>=this.totalPages&&(i.addClass("disabled"),j.addClass("disabled")),f.off("click").on("click",a.proxy(this.onPageListChange,this)),g.off("click").on("click",a.proxy(this.onPageFirst,this)),h.off("click").on("click",a.proxy(this.onPagePre,this)),i.off("click").on("click",a.proxy(this.onPageNext,this)),j.off("click").on("click",a.proxy(this.onPageLast,this)),k.off("click").on("click",a.proxy(this.onPageNumber,this))}},g.prototype.updatePagination=function(){this.options.maintainSelected||this.resetRows(),this.initPagination(),"server"===this.options.sidePagination?this.initServer():this.initBody()},g.prototype.onPageListChange=function(b){var c=a(b.currentTarget);c.parent().addClass("active").siblings().removeClass("active"),this.options.pageSize=+c.text(),this.$toolbar.find(".page-size").text(this.options.pageSize),this.updatePagination()},g.prototype.onPageFirst=function(){this.options.pageNumber=1,this.updatePagination()},g.prototype.onPagePre=function(){this.options.pageNumber--,this.updatePagination()},g.prototype.onPageNext=function(){this.options.pageNumber++,this.updatePagination()},g.prototype.onPageLast=function(){this.options.pageNumber=this.totalPages,this.updatePagination()},g.prototype.onPageNumber=function(b){this.options.pageNumber!==+a(b.currentTarget).text()&&(this.options.pageNumber=+a(b.currentTarget).text(),this.updatePagination())},g.prototype.initBody=function(d){var e=this,g=[],h=this.getData();this.$body=this.$el.find("tbody"),this.$body.length||(this.$body=a("").appendTo(this.$el)),"server"===this.options.sidePagination&&(h=this.data),this.options.pagination&&"server"!==this.options.sidePagination||(this.pageFrom=1,this.pageTo=h.length);for(var i=this.pageFrom-1;i"),this.options.cardView&&g.push(b('',this.header.fields.length)),a.each(this.header.fields,function(a,d){var h="",m=j[d],n="",o={},p=e.header.classes[a];if(k=b('style="%s"',l.concat(e.header.styles[a]).join("; ")),m=f(e.header,e.header.formatters[a],[m,j,i],m),o=f(e.header,e.header.cellStyles[a],[m,j,i],o),o.classes&&(p=b(' class="%s"',o.classes)),o.css){l=[];for(var q in o.css)l.push(q+": "+o.css[q]);k=b('style="%s"',l.concat(e.header.styles[a]).join("; "))}if(e.options.columns[a].checkbox||e.options.columns[a].radio){if(e.options.cardView)return!0;n=e.options.columns[a].checkbox?"checkbox":n,n=e.options.columns[a].radio?"radio":n,h=['',"",""].join("")}else m="undefined"==typeof m?e.options.undefinedText:m,h=e.options.cardView?['
',e.options.showHeader?b('%s',k,c(e.options.columns,"field","title",d)):"",b('%s',m),"
"].join(""):[b("",p,k),m,""].join("");g.push(h)}),this.options.cardView&&g.push(""),g.push("")}g.length||g.push('',b('%s',this.header.fields.length,this.options.formatNoMatches()),""),this.$body.html(g.join("")),d||this.$container.find(".fixed-table-body").scrollTop(0),this.$body.find("> tr > td").off("click").on("click",function(){var c=a(this).parent();e.trigger("click-row",e.data[c.data("index")],c),e.options.clickToSelect&&e.header.clickToSelects[c.children().index(a(this))]&&c.find(b('[name="%s"]',e.options.selectItemName)).trigger("click")}),this.$body.find("tr").off("dblclick").on("dblclick",function(){e.trigger("dbl-click-row",e.data[a(this).data("index")],a(this))}),this.$selectItem=this.$body.find(b('[name="%s"]',this.options.selectItemName)),this.$selectItem.off("click").on("click",function(b){b.stopImmediatePropagation(),a(this).is(":radio")&&a(this).prop("checked",!0);var c=e.$selectItem.filter(":enabled").length===e.$selectItem.filter(":enabled").filter(":checked").length,d=a(this).prop("checked"),f=e.data[a(this).data("index")];e.$selectAll.add(e.$selectAll_).prop("checked",c),f[e.header.stateField]=d,e.trigger(d?"check":"uncheck",f),e.options.singleSelect&&(e.$selectItem.not(this).each(function(){e.data[a(this).data("index")][e.header.stateField]=!1}),e.$selectItem.filter(":checked").not(this).prop("checked",!1)),e.updateSelected()}),a.each(this.header.events,function(b,c){if(c){"string"==typeof c&&(c=f(null,c));for(var d in c)e.$body.find("tr").each(function(){var f=a(this),g=f.find("td").eq(b),h=d.indexOf(" "),i=d.substring(0,h),j=d.substring(h+1),k=c[d];g.find(j).off(i).on(i,function(a){var c=f.data("index"),d=e.data[c],g=d[e.header.fields[b]];k(a,g,d,c)})})}}),this.updateSelected(),this.resetView()},g.prototype.initServer=function(b){var c=this,d={},e={pageSize:this.options.pageSize,pageNumber:this.options.pageNumber,searchText:this.searchText,sortName:this.options.sortName,sortOrder:this.options.sortOrder};this.options.url&&("limit"===this.options.queryParamsType&&(e={limit:e.pageSize,offset:e.pageSize*(e.pageNumber-1),search:e.searchText,sort:e.sortName,order:e.sortOrder}),d=f(this.options,this.options.queryParams,[e],d),d!==!1&&(b||this.$loading.show(),a.ajax({type:this.options.method,url:this.options.url,data:d,cache:this.options.cache,contentType:this.options.contentType,dataType:"json",success:function(a){a=f(c.options,c.options.responseHandler,[a],a);var b=a;"server"===c.options.sidePagination&&(c.options.totalRows=a.total,b=a.rows),c.load(b),c.trigger("load-success",b)},error:function(a){c.trigger("load-error",a.status)},complete:function(){b||c.$loading.hide()}})))},g.prototype.getCaretHtml=function(){return['','',""].join("")},g.prototype.updateSelected=function(){this.$selectItem.each(function(){a(this).parents("tr")[a(this).prop("checked")?"addClass":"removeClass"]("selected")})},g.prototype.updateRows=function(b){var c=this;this.$selectItem.each(function(){c.data[a(this).data("index")][c.header.stateField]=b})},g.prototype.resetRows=function(){var b=this;a.each(this.data,function(a,c){b.$selectAll.prop("checked",!1),b.$selectItem.prop("checked",!1),c[b.header.stateField]=!1})},g.prototype.trigger=function(b){var c=Array.prototype.slice.call(arguments,1);b+=".bs.table",this.options[g.EVENTS[b]].apply(this.options,c),this.$el.trigger(a.Event(b),c),this.options.onAll(b,c),this.$el.trigger(a.Event("all.bs.table"),[b,c])},g.prototype.resetHeader=function(){var b=this,c=this.$container.find(".fixed-table-header"),d=this.$container.find(".fixed-table-body"),f=this.$el.width()>d.width()?e():0;return this.$el.is(":hidden")?(clearTimeout(this.timeoutId_),void(this.timeoutId_=setTimeout(a.proxy(this.resetHeader,this),100))):(this.$header_=this.$header.clone(!0,!0),this.$selectAll_=this.$header_.find('[name="btSelectAll"]'),void setTimeout(function(){c.css({height:"37px","border-bottom":"1px solid #dddddd","margin-right":f}).find("table").css("width",b.$el.css("width")).html("").attr("class",b.$el.attr("class")).append(b.$header_),b.$header.find("th").each(function(c){b.$header_.find("th").eq(c).data(a(this).data())}),b.$body.find("tr:first-child:not(.no-records-found) > *").each(function(c){b.$header_.find("div.fht-cell").eq(c).width(a(this).innerWidth())}),b.$el.css("margin-top",-b.$header.height()),d.off("scroll").on("scroll",function(){c.scrollLeft(a(this).scrollLeft())})}))},g.prototype.toggleColumn=function(a,c,d){if(-1!==a&&(this.options.columns[a].visible=c,this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns)){var e=this.$toolbar.find(".keep-open input").prop("disabled",!1);d&&e.filter(b('[value="%s"]',a)).prop("checked",c),e.filter(":checked").length<=this.options.minimumCountColumns&&e.filter(":checked").prop("disabled",!0)}},g.prototype.resetView=function(a){{var b=this;this.header}if(a&&a.height&&(this.options.height=a.height),this.$selectAll.prop("checked",this.$selectItem.length>0&&this.$selectItem.length===this.$selectItem.filter(":checked").length),this.options.height){var c=+this.$toolbar.children().outerHeight(!0),d=+this.$pagination.children().outerHeight(!0),e=this.options.height-c-d;this.$container.find(".fixed-table-container").css("height",e+"px")}return this.options.cardView?(b.$el.css("margin-top","0"),void b.$container.find(".fixed-table-container").css("padding-bottom","0")):(this.options.showHeader&&this.options.height&&this.resetHeader(),void(this.options.height&&this.options.showHeader&&this.$container.find(".fixed-table-container").css("padding-bottom","37px")))},g.prototype.getData=function(){return this.searchText?this.data:this.options.data},g.prototype.load=function(a){this.initData(a),this.initSearch(),this.initPagination(),this.initBody()},g.prototype.append=function(a){this.initData(a,!0),this.initSearch(),this.initPagination(),this.initBody(!0)},g.prototype.remove=function(b){var c,d,e=this.options.data.length;if(b.hasOwnProperty("field")&&b.hasOwnProperty("values")){for(c=e-1;c>=0;c--){if(d=this.options.data[c],!d.hasOwnProperty(b.field))return;-1!==a.inArray(d[b.field],b.values)&&this.options.data.splice(c,1)}e!==this.options.data.length&&(this.initSearch(),this.initPagination(),this.initBody(!0))}},g.prototype.updateRow=function(b){b.hasOwnProperty("index")&&b.hasOwnProperty("row")&&(a.extend(this.data[b.index],b.row),this.initBody())},g.prototype.mergeCells=function(b){var c,d,e=b.index,f=a.inArray(b.field,this.header.fields),g=b.rowspan||1,h=b.colspan||1,i=this.$body.find("tr"),j=i.eq(e).find("td").eq(f);if(!(0>e||0>f||e>=this.data.length)){for(c=e;e+g>c;c++)for(d=f;f+h>d;d++)i.eq(c).find("td").eq(d).hide();j.attr("rowspan",g).attr("colspan",h).show(10,a.proxy(this.resetView,this))}},g.prototype.getSelections=function(){var b=this;return a.grep(this.data,function(a){return a[b.header.stateField]})},g.prototype.checkAll=function(){this.$selectAll.add(this.$selectAll_).prop("checked",!0),this.$selectItem.filter(":enabled").prop("checked",!0),this.updateRows(!0),this.updateSelected(),this.trigger("check-all")},g.prototype.uncheckAll=function(){this.$selectAll.add(this.$selectAll_).prop("checked",!1),this.$selectItem.filter(":enabled").prop("checked",!1),this.updateRows(!1),this.updateSelected(),this.trigger("uncheck-all")},g.prototype.destroy=function(){this.$el.insertBefore(this.$container),a(this.options.toolbar).insertBefore(this.$el),this.$container.next().remove(),this.$container.remove(),this.$el.html(this.$el_.html()).attr("class",this.$el_.attr("class")||"")},g.prototype.showLoading=function(){this.$loading.show()},g.prototype.hideLoading=function(){this.$loading.hide()},g.prototype.refresh=function(a){a&&a.url&&(this.options.url=a.url),this.initServer(a&&a.silent)},g.prototype.showColumn=function(a){this.toggleColumn(d(this.options.columns,a),!0,!0)},g.prototype.hideColumn=function(a){this.toggleColumn(d(this.options.columns,a),!1,!0)},a.fn.bootstrapTable=function(b,c){var d,e=["getSelections","getData","load","append","remove","updateRow","mergeCells","checkAll","uncheckAll","refresh","resetView","destroy","showLoading","hideLoading","showColumn","hideColumn"];return this.each(function(){var f=a(this),h=f.data("bootstrap.table"),i=a.extend({},g.DEFAULTS,f.data(),"object"==typeof b&&b);if("string"==typeof b){if(a.inArray(b,e)<0)throw"Unknown method: "+b;if(!h)return;d=h[b](c),"destroy"===b&&f.removeData("bootstrap.table")}h||f.data("bootstrap.table",h=new g(this,i))}),"undefined"==typeof d?this:d},a.fn.bootstrapTable.Constructor=g,a.fn.bootstrapTable.defaults=g.DEFAULTS,a.fn.bootstrapTable.columnDefaults=g.COLUMN_DEFAULTS,a(function(){a('[data-toggle="table"]').bootstrapTable()})}(jQuery); 8 | 9 | --------------------------------------------------------------------------------