├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.TXT ├── README.md ├── attachments └── test.txt ├── config.json ├── db └── blank ├── helpers ├── asciidoc_exporter.rb ├── helper.rb ├── sinatra_ssl.rb ├── vuln_importer.rb └── xslt_generation.rb ├── model └── master.rb ├── public ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── docs.css │ ├── font-awesome.css │ └── signin.css ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── img │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ ├── logo.jpg │ ├── logo.png │ ├── logo_1.jpg │ └── logo_1.png └── js │ ├── bootstrap-affix.js │ ├── bootstrap-alert.js │ ├── bootstrap-button.js │ ├── bootstrap-carousel.js │ ├── bootstrap-collapse.js │ ├── bootstrap-dropdown.js │ ├── bootstrap-modal.js │ ├── bootstrap-popover.js │ ├── bootstrap-scrollspy.js │ ├── bootstrap-tab.js │ ├── bootstrap-tooltip.js │ ├── bootstrap-transition.js │ ├── bootstrap-typeahead.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── d3.js │ ├── jquery-2.0.3.js │ ├── jquery.fileupload.js │ ├── jquery.js │ └── script.js ├── scripts ├── alert_unapproved_findings.rb ├── create_user.rb ├── export_reports.rb ├── export_template_findings.rb ├── first_time.rb ├── lf.sed ├── make_export.sh ├── manage_users.rb ├── reset_pw.rb └── update_templates.rb ├── serpico.rb ├── templates ├── CVSS_Template.docx ├── Default Status.docx ├── Default Template.docx ├── Serpico - Finding.docx ├── Serpico - GenericRiskScoring.docx ├── Serpico - No DREAD.docx ├── Serpico - Report.docx ├── Serpico - Risk Finding.docx ├── Serpico - Status.docx └── template_findings.json ├── test └── main_test.rb ├── tmp └── tmp.txt └── views ├── add_template.haml ├── add_user.haml ├── add_user_report.haml ├── additional_features.haml ├── admin.haml ├── create_finding.haml ├── edit_template.haml ├── edit_user.haml ├── findings_add.haml ├── findings_edit.haml ├── findings_list.haml ├── footer.haml ├── import_burp.haml ├── import_nessus.haml ├── import_report.haml ├── import_templates.haml ├── index.haml ├── info.haml ├── layout.haml ├── list_attachments.haml ├── list_user.haml ├── new_report.haml ├── presentation.haml ├── report_edit.haml ├── reports_list.haml ├── template_list.haml ├── test.haml ├── text_status.haml ├── upload_attachments.haml └── user_defined_variable.haml /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | db/master.db 3 | cert.pem 4 | key.pem 5 | tmp/* 6 | templates/* 7 | attachments/* 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby "2.1.5" 4 | 5 | gem 'sinatra' 6 | gem 'haml' 7 | gem 'zipruby', '0.3.6' 8 | gem 'net-ldap', '~> 0.11' 9 | gem 'json' 10 | gem 'nokogiri' 11 | gem 'data_mapper', '1.2.0' 12 | gem 'dm-sqlite-adapter', '1.2.0' 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | bcrypt (3.1.10) 6 | bcrypt-ruby (3.1.5) 7 | bcrypt (>= 3.1.3) 8 | data_mapper (1.2.0) 9 | dm-aggregates (~> 1.2.0) 10 | dm-constraints (~> 1.2.0) 11 | dm-core (~> 1.2.0) 12 | dm-migrations (~> 1.2.0) 13 | dm-serializer (~> 1.2.0) 14 | dm-timestamps (~> 1.2.0) 15 | dm-transactions (~> 1.2.0) 16 | dm-types (~> 1.2.0) 17 | dm-validations (~> 1.2.0) 18 | data_objects (0.10.16) 19 | addressable (~> 2.1) 20 | dm-aggregates (1.2.0) 21 | dm-core (~> 1.2.0) 22 | dm-constraints (1.2.0) 23 | dm-core (~> 1.2.0) 24 | dm-core (1.2.1) 25 | addressable (~> 2.3) 26 | dm-do-adapter (1.2.0) 27 | data_objects (~> 0.10.6) 28 | dm-core (~> 1.2.0) 29 | dm-migrations (1.2.0) 30 | dm-core (~> 1.2.0) 31 | dm-serializer (1.2.2) 32 | dm-core (~> 1.2.0) 33 | fastercsv (~> 1.5) 34 | json (~> 1.6) 35 | json_pure (~> 1.6) 36 | multi_json (~> 1.0) 37 | dm-sqlite-adapter (1.2.0) 38 | dm-do-adapter (~> 1.2.0) 39 | do_sqlite3 (~> 0.10.6) 40 | dm-timestamps (1.2.0) 41 | dm-core (~> 1.2.0) 42 | dm-transactions (1.2.0) 43 | dm-core (~> 1.2.0) 44 | dm-types (1.2.2) 45 | bcrypt-ruby (~> 3.0) 46 | dm-core (~> 1.2.0) 47 | fastercsv (~> 1.5) 48 | json (~> 1.6) 49 | multi_json (~> 1.0) 50 | stringex (~> 1.4) 51 | uuidtools (~> 2.1) 52 | dm-validations (1.2.0) 53 | dm-core (~> 1.2.0) 54 | do_sqlite3 (0.10.16) 55 | data_objects (= 0.10.16) 56 | fastercsv (1.5.5) 57 | haml (4.0.6) 58 | tilt 59 | json (1.8.3) 60 | json_pure (1.8.2) 61 | mini_portile (0.6.2) 62 | multi_json (1.11.2) 63 | net-ldap (0.11) 64 | nokogiri (1.6.6.2) 65 | mini_portile (~> 0.6.0) 66 | rack (1.5.5) 67 | rack-protection (1.5.3) 68 | rack 69 | sinatra (1.4.6) 70 | rack (~> 1.4) 71 | rack-protection (~> 1.4) 72 | tilt (>= 1.3, < 3) 73 | stringex (1.5.1) 74 | tilt (2.0.1) 75 | uuidtools (2.1.5) 76 | zipruby (0.3.6) 77 | 78 | PLATFORMS 79 | ruby 80 | 81 | DEPENDENCIES 82 | data_mapper 83 | dm-sqlite-adapter 84 | haml 85 | json 86 | net-ldap 87 | nokogiri 88 | sinatra 89 | zipruby 90 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015, Willis Vandevanter 2 | All rights reserved. 3 | 4 | License: BSD-3-clause 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 2. 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. 9 | 3. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 中文版渗透测试报告生成系统 Serpico 2 | ## SimplE RePort wrIting and CollaboratiOn tool 3 | Serpico is a penetration testing report generation and collaboration tool. It was developed to cut down on the amount of time it takes to write a penetration testing report. 4 | 5 | Video Demo of Functionality: 6 | 7 | [Serpico - Demo 1](https://www.youtube.com/watch?v=G_qYcL4ynSc) 8 | 9 | [Additional Video Demos](https://github.com/MooseDojo/Serpico/wiki#online-demos) 10 | 11 | ## Installation 12 | 13 | ### Docker 14 | Serpico has a supported Docker image if you wanted to get started quickly: 15 | [Running Serpico From Docker](https://github.com/MooseDojo/Serpico/wiki/Running-Serpico-From-Docker) 16 | 17 | ### Building Serpico 18 | Serpico is written in Ruby using Sinatra, Bootstrap, and Haml. Installation should be easy: 19 | 20 | - You will need a copy of Ruby. RVM is suggested (https://rvm.io/rvm/install). ruby version 2.1.5 is supported. 21 | 22 | ``` 23 | rvm install 2.1.5 24 | rvm use 2.1.5 25 | ``` 26 | 27 | - If you are running Ubuntu (or also verified on Kali) you will need a couple of dependencies: 28 | ``` 29 | apt-get install libsqlite3-dev libxslt-dev libxml2-dev zlib1g-dev gcc 30 | ``` 31 | 32 | - Go into Serpico and install the proper gems: 33 | ``` 34 | cd Serpico 35 | gem install bundler 36 | bundle install 37 | ``` 38 | 39 | - 第一次使用使用以下命令进行初始化: 40 | ``` 41 | ruby scripts/first_time.rb 42 | ``` 43 | 44 | 使用以下命令启动: 45 | ``` 46 | ruby serpico.rb 47 | ``` 48 | 49 | Note: 第一次使用进行初始化时候回自动创建一个自签名证书,你也可以自己修改,只要放到程序目录里,若修改证书名称只要稍作配置就可以 50 | 51 | ``` 52 | openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 700 53 | ``` 54 | 55 | 启动以后使用浏览器访问: https://127.0.0.1:8443 (端口可以根据你设置的进行更改). 56 | 57 | 58 | ## About Serpico 59 | Serpico is at its core a report generation tool but targeted at creating information security reports. When building a report the user adds "findings" from the template database to the report. When there are enough findings, click 'Generate Report' to create the docx with your findings. The docx design comes from a Report Template which can be added through the UI; a default one is included. The Report Templates use a custom Markup Language to stub the data from the UI (i.e. findings, customer name, etc) and put them into the report. 60 | 61 | ## Features 62 | #### Report Template Editing is Easy 63 | **Philosophy: Editing a report template should be easy.** 64 | During peer review we would constantly ran into "little things" we were fixing from the report template; an extra space here, a misspelling there. But it adds up. With Serpico, "fix" the report template, upload it back through the UI, and generate a new report; the error should be fixed permanently. 65 | 66 | #### Template Database 67 | **Philosophy: We do not need to write most findings from scratch.** 68 | Most findings have been found in a previous assessment. In Serpico, all authors can pull findings from the template database and add to the report. A user can also 'Upload' a finding they made into the Template Database to share with everyone. 69 | 70 | #### Attachment Collaboration 71 | **Philosophy: It should be easy to share files with teammates.** 72 | Use the 'Add Attachment' functionality to store a file (e.g. screenshots, nmap scans) or share with teammates on a pen test. No thumb drive swapping or e-mailing, just log into the UI and download the files. At the end of the assessment everything traded or generated for that assessment is in one place. 73 | 74 | 75 | ## Microsoft Word Meta-Language 76 | The Meta language used for Microsoft Word was designed to be as simple as possible while still serving enough features to create a basic penetration test report. That being said it has a learning curve (and many bugs) and I _highly_ suggest looking at "Serpico - Report.docx" or "Serpico - No DREAD.docx" and editing these rather than working from scratch. 77 | 78 | Inserting Screenshots 79 | https://github.com/MooseDojo/Serpico/wiki/Inserting-Screenshots 80 | 81 | This is an area we know needs development so e-mail me with any ideas. 82 | 83 | See the Wiki for more information, [Serpico Meta-Language In Depth](https://github.com/MooseDojo/Serpico/wiki/Serpico-Meta-Language-In-Depth) 84 | 85 | ## Support 86 | - As questions come up we try to add them to the [Wiki](https://github.com/MooseDojo/Serpico/wiki) 87 | - IRC: [#therealserpico](http://webchat.freenode.net/?channels=%23therealserpico&uio=d4) on freenode 88 | - If all else fails create an issue and we will try to get it answered 89 | 90 | ## GOTCHAS 91 | - Microsoft has a really annoying habit of changing a character for you. Always beware of this when working with the meta language 92 | 93 | ## Huge Thanks 94 | * Wouldn't exist without testing, support, and feature suggestion of the rest of the [Moosedojo team!](https://github.com/MooseDojo). 95 | * @d4rkd0s for the great logo work. Thanks! 96 | 97 | 98 | 99 | ##中文版使用说明 100 | - 详细说明请参考wiki 101 | Meta language In-Depth 102 | 103 | 符号列表说明: 104 | 105 | - Ω - 一个简单的替换型变量. 106 | 107 | ``` 108 | ΩFULL_COMPANY_NAMEΩ 109 | 110 | 渲染为: 完整的公司名称 111 | ``` 112 | 113 | - § - 一个用户自定义变量. 用户自定义变量可以通过UI进行配置,可以在报告中引用,用户自定义变量在报告中特别好用 114 | 115 | ``` 116 | §my_executive_summary§ 117 | 118 | 渲染为: 用户在UI中定义的该变量的值 119 | ``` 120 | 121 | - ¬ - for each 122 | ``` 123 | ¬finding¬ 124 | 内容 125 | ∆ 126 | 127 | 渲染为: 每一个finding(漏洞)循环中都会打印'STUFF'. 128 | ``` 129 | 130 | - π - 循环中的替换变量. 在循环中就不要用 Ω 作为替换变量了. 131 | 132 | ``` 133 | ¬report/findings_list/findings¬ 134 | πtitleπ 135 | ∆ 136 | 137 | 在findings_list中的每一个findings中输出这个finding的标题. 138 | 139 | ``` 140 | 141 | - NOTE: 你可以在for循环中使用if来做判断: 142 | ``` 143 | ¬report/findings_list/findings:::DREAD_TOTAL<50:::DREAD_TOTAL>30¬ 144 | πtitleπ 145 | ∆ 146 | 147 | 上面的代码表示: 148 | for each finding 149 | if dread_total < 50 150 | if dread_total >30 151 | 152 | print title 153 | 154 | close for loop and both if's 155 | ``` 156 | 157 | - æ - 在表格中使用的循环变量,每一个表示一行 158 | ``` 159 | ::: - 在行中表示if的意思 160 | ``` 161 | æreport/findings_list/findings:::DREAD_TOTAL>35æ 162 | ``` 163 | 在finding(漏洞)列表中DREAD_TOTAL>35的渲染为一个表格的行 164 | - ∞ - 在一个表格中作为替换变量使用,注意仅在表格中使用 165 | 166 | ``` 167 | æreport/findings_list/findings:::DREAD_TOTAL>35æ ∞title∞ 168 | ``` 169 | 在finding(漏洞)列表中DREAD_TOTAL>35的渲染为一个表格的行并填入标题 170 | ``` 171 | - † - if 条件 172 | ``` 173 | † DREAD_SCORE > 1 † 174 | HELLO WORLD 175 | ¥ 176 | 177 | 如果DREAD_SCORE > 1那么输出一个HELLO WORLD 178 | ``` 179 | - µ - 初始化choose/when结构 Initiates choose/when structure 180 | 181 | ƒ - choose/when中when的value值 182 | 183 | å - 在for-each外结束一个choose/when 184 | 185 | ≠ - 在for-each里结束一个choose/when 186 | 187 | ¬overview/paragraph¬ 188 | µCONDITIONALµ π.π 189 | ƒcodeƒ π.π 190 | ƒitalicsƒ π.π 191 | ÷ π.π ≠ 192 | 193 | This will take each paragraph from the overview section of the finding. 194 | 这将会打印出overview部分所有的漏洞段落的内容 195 | If the paragraph is labelled as code then the paragraph will be formatted as code. 196 | 如果这个段落被标记为code,那么这里就会被格式化为code 197 | The "." above means the paragraph variable from the 'overview/paragraph' for loop. 198 | 上面的"." 意味着'overview/paragraph'中的段落变量 199 | ``` 200 | 201 | - ∆ - 结束each 202 | 203 | - ¥ - 结束if 204 | 205 | 一个漏洞包含了下面的这些属性,可以使用变量进行访问. 例如: 206 | 207 | ¬report/findings_list/findings¬ 208 | πtitleπ 209 | ∆ 210 | 属性列表: 211 | 212 | title 213 | damage 214 | reproducability 215 | exploitability 216 | affected_users 217 | discoverability 218 | effort 219 | type 220 | dread_total 221 | overview 222 | poc 223 | remediation 224 | notes 225 | assessment_type 226 | references 227 | risk 228 | affected_hosts 229 | presentation_points 230 | Status API Training Shop Blog About 231 | 232 | -------------------------------------------------------------------------------- /attachments/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/attachments/test.txt -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port":"8443", 3 | "use_ssl":true, 4 | "bind_address":"0.0.0.0", 5 | "ssl_certificate":"./cert.pem", 6 | "ssl_key":"key.pem", 7 | "ldap":"false", 8 | "ldap_domain":"", 9 | "ldap_dc":"", 10 | "dread":false, //DREAD risk scoring not used by default 11 | "cvss":false, 12 | "nessusmap":true, //maps nessus findings to serpico findings 13 | "burpmap":true, //maps burp findings to serpico findings 14 | "finding_types": ["Web Application","Business Logic","Network Services", "Best Practice", "Compliance", "Database", "Network Internal", "Router Configuration","Social Engineering", "Physical", "Wireless", "Network Security", "System Security", "Logging and Auditing", "Imported"], 15 | "logo":"/img/logo_1.png", 16 | "auto_import":true, //Experimental, will automatically create new findings on import 17 | "chart":true, //Enabled or disable support, 18 | "user_defined_variables":[], //set your global UDV's here; ["client_start_date","client_abbreviated_name"], 19 | "threshold":"20" // must be a string; "0","1","2"... 20 | } -------------------------------------------------------------------------------- /db/blank: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/db/blank -------------------------------------------------------------------------------- /helpers/asciidoc_exporter.rb: -------------------------------------------------------------------------------- 1 | def parse_input(text) 2 | if text == nil or text == "" 3 | return " None \n" 4 | else 5 | # replace paragragh 6 | text = text.gsub("","").gsub("","\n\n") 7 | 8 | # replace h4 9 | text = text.gsub("

","==== ").gsub("

","") 10 | 11 | # not sure asciidoc equivalent for indent 12 | text = text.gsub("","").gsub("","") 13 | 14 | # replace italics 15 | text = text.gsub("","_").gsub("","_") 16 | 17 | # replace bullet, doesn't take into account nesting 18 | text = text.gsub("","\n* ").gsub("","") 19 | 20 | # replace code 21 | text = text.gsub("","....").gsub("","....") 22 | end 23 | return text 24 | end 25 | 26 | # takes in a serpico finding and returns asciidoc version 27 | def gen_asciidoc(finding, dread) 28 | asciidoc = "" 29 | 30 | asciidoc << "== #{finding.title} \n\n" 31 | 32 | if(dread) 33 | asciidoc << "|====== \n" 34 | asciidoc << "|Damage|Reproducibility|Exploitability|Affected Users|Discoverability|Remediation Effort\n" 35 | asciidoc << "|#{finding.damage} \n" 36 | asciidoc << "|#{finding.reproducability} \n" 37 | asciidoc << "|#{finding.exploitability} \n" 38 | asciidoc << "|#{finding.affected_users} \n" 39 | asciidoc << "|#{finding.discoverability} \n" 40 | asciidoc << "|#{finding.effort} \n" 41 | asciidoc << "|======\n\n" 42 | else 43 | risk = ["Informational", "Low", "Moderate", "High", "Critical"] 44 | asciidoc << "|===\n" 45 | asciidoc << "|Risk |Remediation Effort\n" 46 | asciidoc << "|#{risk[finding.risk]} \n" 47 | asciidoc << "|#{finding.effort} \n" 48 | asciidoc << "|===\n\n" 49 | end 50 | 51 | asciidoc << "=== Overview \n" 52 | asciidoc << parse_input(finding.overview) + "\n" 53 | asciidoc << "=== Affected Hosts \n" 54 | asciidoc << parse_input(finding.affected_hosts) + "\n" 55 | asciidoc << "=== Proof of Concept \n" 56 | asciidoc << parse_input(finding.poc) + "\n" 57 | asciidoc << "=== Remediation \n" 58 | asciidoc << parse_input(finding.remediation) + "\n" 59 | asciidoc << "=== References \n" 60 | asciidoc << parse_input(finding.references) + "\n\n" 61 | asciidoc << "<<< \n\n" 62 | return asciidoc 63 | end 64 | -------------------------------------------------------------------------------- /helpers/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # The helper class exists to do string manipulation and heavy lifting 4 | 5 | def url_escape_hash(hash) 6 | hash.each do |k,v| 7 | v = CGI::escapeHTML(v) 8 | 9 | if v 10 | # convert bullets 11 | v = v.gsub("*-","") 12 | v = v.gsub("-*","") 13 | 14 | #convert h4 15 | v = v.gsub("[==","

") 16 | v = v.gsub("==]","

") 17 | 18 | #convert indent text 19 | v = v.gsub("[--","") 20 | v = v.gsub("--]","") 21 | 22 | #convert indent text 23 | v = v.gsub("[~~","") 24 | v = v.gsub("~~]","") 25 | end 26 | 27 | # replace linebreaks with paragraph xml elements 28 | if v =~ /\r\n/ 29 | new_v = "" 30 | brs = v.split("\r\n") 31 | brs.each do |br| 32 | new_v << "" 33 | new_v << br 34 | new_v << "" 35 | end 36 | 37 | v = new_v 38 | elsif k == "remediation" or k == "overview" or k == "poc" or k == "affected_hosts" 39 | new_v = "#{v}" 40 | v = new_v 41 | end 42 | 43 | hash[k] = v 44 | end 45 | 46 | return hash 47 | end 48 | 49 | def meta_markup(text) 50 | new_text = text.gsub(""," ").gsub("","") 51 | new_text = new_text.gsub("","*-").gsub("","-*") 52 | new_text = new_text.gsub("

","[==").gsub("

","==]") 53 | new_text = new_text.gsub("","[[[").gsub("","]]]") 54 | new_text = new_text.gsub("","[--").gsub("","--]") 55 | new_text = new_text.gsub("","[~~").gsub("","~~]") 56 | end 57 | 58 | 59 | # URL escaping messes up the inserted XML, this method switches it back to XML elements 60 | 61 | def meta_markup_unencode(findings_xml, customer_name) 62 | 63 | # code tags get added in later 64 | findings_xml = findings_xml.gsub("[[[","") 65 | findings_xml = findings_xml.gsub("]]]","") 66 | 67 | # creates paragraphs 68 | findings_xml = findings_xml.gsub("<paragraph>","") 69 | findings_xml = findings_xml.gsub("</paragraph>","") 70 | # same for the bullets 71 | findings_xml = findings_xml.gsub("<bullet>","") 72 | findings_xml = findings_xml.gsub("</bullet>","") 73 | # same for the h4 74 | findings_xml = findings_xml.gsub("<h4>","

") 75 | findings_xml = findings_xml.gsub("</h4>","

") 76 | # same for the code markings 77 | findings_xml = findings_xml.gsub("<code>","") 78 | findings_xml = findings_xml.gsub("</code>","") 79 | # same for the indented text 80 | findings_xml = findings_xml.gsub("<indented>","") 81 | findings_xml = findings_xml.gsub("</indented>","") 82 | # same for the indented text 83 | findings_xml = findings_xml.gsub("<italics>","") 84 | findings_xml = findings_xml.gsub("</italics>","") 85 | 86 | # changes the <> marks 87 | if customer_name 88 | findings_xml = findings_xml.gsub("&lt;&lt;CUSTOMER&gt;&gt;","#{customer_name}") 89 | end 90 | 91 | #this is for re-upping the comment fields 92 | findings_xml = findings_xml.gsub("<modified>","") 93 | findings_xml = findings_xml.gsub("</modified>","") 94 | 95 | findings_xml = findings_xml.gsub("<new_finding>","") 96 | findings_xml = findings_xml.gsub("</new_finding>","") 97 | 98 | # these are for beautification 99 | findings_xml = findings_xml.gsub("&quot;","\"") 100 | findings_xml = findings_xml.gsub("&","&") 101 | findings_xml = findings_xml.gsub("&lt;","<").gsub("&gt;",">") 102 | 103 | return findings_xml 104 | end 105 | 106 | def compare_text(new_text, orig_text) 107 | if orig_text == nil 108 | # there is no master finding, must be new 109 | t = "" 110 | t << "#{new_text}" 111 | return t 112 | end 113 | 114 | if new_text == orig_text 115 | return new_text 116 | else 117 | n_t = "" 118 | 119 | n_t << "#{new_text}" 120 | return n_t 121 | end 122 | end 123 | 124 | # CVSS helper, there is a lot of hardcoded stuff 125 | def cvss(data) 126 | av = data["av"].downcase 127 | ac = data["ac"].downcase 128 | au = data["au"].downcase 129 | c = data["c"].downcase 130 | i = data["i"].downcase 131 | a = data["a"].downcase 132 | e = data["e"].downcase 133 | rl = data["rl"].downcase 134 | rc = data["rc"].downcase 135 | cdp = data["cdp"].downcase 136 | td = data["td"].downcase 137 | cr = data["cr"].downcase 138 | ir = data["ir"].downcase 139 | ar = data["ar"].downcase 140 | if ac == "high" 141 | cvss_ac = 0.35 142 | elsif ac == "medium" 143 | cvss_ac = 0.61 144 | else 145 | cvss_ac = 0.71 146 | end 147 | 148 | if au == "none" 149 | cvss_au = 0.704 150 | elsif au == "single" 151 | cvss_au = 0.56 152 | else 153 | cvss_au = 0.45 154 | end 155 | 156 | if av == "local" 157 | cvss_av = 0.395 158 | elsif av == "local network" 159 | cvss_av = 0.646 160 | else 161 | cvss_av = 1 162 | end 163 | 164 | if c == "none" 165 | cvss_c = 0 166 | elsif c == "partial" 167 | cvss_c = 0.275 168 | else 169 | cvss_c = 0.660 170 | end 171 | 172 | if i == "none" 173 | cvss_i = 00 174 | elsif i == "partial" 175 | cvss_i = 0.275 176 | else 177 | cvss_i = 0.660 178 | end 179 | 180 | if a == "none" 181 | cvss_a = 0 182 | elsif a == "partial" 183 | cvss_a = 0.275 184 | else 185 | cvss_a = 0.660 186 | end 187 | 188 | 189 | # temporal score calculations 190 | if e == "unproven exploit exists" 191 | cvss_e = 0.85 192 | elsif e == "proof-of-concept code" 193 | cvss_e = 0.90 194 | elsif e == "functional exploit exists" 195 | cvss_e = 0.95 196 | else 197 | cvss_e = 1 198 | end 199 | 200 | if rl == "official fix" 201 | cvss_rl = 0.87 202 | elsif rl == "temporary fix" 203 | cvss_rl = 0.90 204 | elsif rl == "workaround" 205 | cvss_rl = 0.95 206 | else 207 | cvss_rl = 1 208 | end 209 | 210 | if rc == "unconfirmed" 211 | cvss_rc = 0.90 212 | elsif rc == "uncorroborated" 213 | cvss_rc = 0.95 214 | else 215 | cvss_rc = 1 216 | end 217 | 218 | 219 | #environemental 220 | if cdp == "low" 221 | cvss_cdp = 0.1 222 | elsif cdp == "low-medium" 223 | cvss_cdp = 0.3 224 | elsif cdp == "medium-high" 225 | cvss_cdp = 0.4 226 | elsif cdp == "high" 227 | cvss_cdp = 0.5 228 | else 229 | cvss_cdp = 0 230 | end 231 | 232 | if td == "none" 233 | cvss_td = 0 234 | elsif td == "low" 235 | cvss_td = 0.25 236 | elsif td == "medium" 237 | cvss_td = 0.75 238 | else 239 | cvss_td = 1 240 | end 241 | 242 | if cr == "low" 243 | cvss_cr = 0.5 244 | elsif cr == "high" 245 | cvss_cr = 1.51 246 | else 247 | cvss_cr = 1 248 | end 249 | 250 | if ir == "low" 251 | cvss_ir = 0.5 252 | elsif ir == "high" 253 | cvss_ir = 1.51 254 | else 255 | cvss_ir = 1 256 | end 257 | 258 | if ar == "low" 259 | cvss_ar = 0.5 260 | elsif ar == "high" 261 | cvss_ar = 1.51 262 | else 263 | cvss_ar = 1 264 | end 265 | 266 | 267 | cvss_impact = 10.41 * (1 - (1 - cvss_c) * (1 - cvss_i) * (1 - cvss_a)) 268 | cvss_exploitability = 20 * cvss_ac * cvss_au * cvss_av 269 | if cvss_impact == 0 270 | cvss_impact_f = 0 271 | else 272 | cvss_impact_f = 1.176 273 | end 274 | cvss_base = (0.6*cvss_impact + 0.4*cvss_exploitability-1.5)*cvss_impact_f 275 | 276 | cvss_temporal = cvss_base * cvss_e * cvss_rl * cvss_rc 277 | 278 | cvss_modified_impact = [10, 10.41 * (1 - (1 - cvss_c * cvss_cr) * (1 - cvss_i * cvss_ir) * (1 - cvss_a * cvss_ar))].min 279 | 280 | if cvss_modified_impact == 0 281 | cvss_modified_impact_f = 0 282 | else 283 | cvss_modified_impact_f = 1.176 284 | end 285 | 286 | cvss_modified_base = (0.6*cvss_modified_impact + 0.4*cvss_exploitability-1.5)*cvss_modified_impact_f 287 | cvss_adjusted_temporal = cvss_modified_base * cvss_e * cvss_rl * cvss_rc 288 | cvss_environmental = (cvss_adjusted_temporal + (10 - cvss_adjusted_temporal) * cvss_cdp) * cvss_td 289 | 290 | if cvss_environmental 291 | cvss_total = cvss_environmental 292 | elsif cvss_temporal 293 | cvss_total = cvss_temporal 294 | else 295 | cvss_total = cvss_base 296 | end 297 | 298 | 299 | data["cvss_base"] = sprintf("%0.1f" % cvss_base) 300 | data["cvss_impact"] = sprintf("%0.1f" % cvss_impact) 301 | data["cvss_exploitability"] = sprintf("%0.1f" % cvss_exploitability) 302 | data["cvss_temporal"] = sprintf("%0.1f" % cvss_temporal) 303 | data["cvss_environmental"] = sprintf("%0.1f" % cvss_environmental) 304 | data["cvss_modified_impact"] = sprintf("%0.1f" % cvss_modified_impact) 305 | data["cvss_total"] = sprintf("%0.1f" % cvss_total) 306 | 307 | return data 308 | end 309 | 310 | # there are three scoring types; risk, dread and cvss 311 | # this sets a score for all three in case the user switches later 312 | 313 | def convert_score(finding) 314 | if(finding.cvss_total == nil) 315 | puts "|!| No CVSS score exists" 316 | finding.cvss_total = 0 317 | end 318 | if(finding.dread_total == nil) 319 | puts "|!| No CVSS score exists" 320 | finding.dread_total = 0 321 | end 322 | if(finding.risk == nil) 323 | puts "|!| No CVSS score exists" 324 | finding.risk = 0 325 | end 326 | return finding 327 | end 328 | -------------------------------------------------------------------------------- /helpers/sinatra_ssl.rb: -------------------------------------------------------------------------------- 1 | require 'webrick/https' 2 | require 'openssl' 3 | 4 | # I can't take credit for this, this is from a stackoverflow article 5 | # http://stackoverflow.com/questions/2362148/how-to-enable-ssl-for-a-standalone-sinatra-app 6 | 7 | module Sinatra 8 | class Application 9 | def self.run! 10 | server_options = { 11 | :Port => port, 12 | :SSLEnable => use_ssl, 13 | :BindAddress => bind_address, 14 | } 15 | if (use_ssl) then 16 | certificate_content = File.open(ssl_certificate).read 17 | key_content = File.open(ssl_key).read 18 | server_options[:SSLCertificate] = OpenSSL::X509::Certificate.new(certificate_content) 19 | server_options[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(key_content) 20 | server_options[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_NONE 21 | end 22 | 23 | Rack::Handler::WEBrick.run self, server_options do |server| 24 | [:INT, :TERM].each { |sig| trap(sig) { server.stop } } 25 | server.threaded = settings.threaded if server.respond_to? :threaded= 26 | set :running, true 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /helpers/vuln_importer.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'nokogiri' 3 | require 'zipruby' 4 | require './model/master' 5 | 6 | # For now, we need this to clean up import text a bit 7 | def clean(text) 8 | return unless text 9 | 10 | text = text.squeeze(" ") 11 | text = text.gsub("
", "\n") 12 | text = text.gsub("

", "\n") 13 | text = text.gsub("","") 14 | text = text.gsub("","") 15 | text = text.gsub("","") 16 | text = text.gsub("","") 17 | text = text.gsub("","") 18 | text = text.gsub("","") 19 | text = text.gsub("\n\n","") #remove leading newline characters from nessus plugin output too! 20 | text = text.gsub("\n","") #remove leading newline character from nessus plugin output too! 21 | text = text.gsub("","") 22 | text = text.gsub("","") 23 | 24 | # burp stores html and needs to be removed, TODO better way to handle this 25 | text = text.gsub("

", "") 26 | text = text.gsub("
  • ", "\n") 27 | text = text.gsub("
  • ", "") 28 | text = text.gsub("", "") 30 | text = text.gsub("", "") 31 | text = text.gsub("
    ", "") 32 | text = text.gsub("", "\n") 33 | text = text.gsub("", "") 34 | text = text.gsub("", "") 35 | text = text.gsub("", "") 36 | text = text.gsub("", "") 37 | text = text.gsub("", "") 38 | text = text.gsub("","") 40 | text = text.gsub("\n\n","\n") 41 | 42 | text = text.gsub("\n","\r\n") 43 | 44 | text_ = url_escape_hash({'a' => text}) 45 | text = text_['a'] 46 | 47 | return text 48 | end 49 | 50 | def uniq_findings(findings) 51 | vfindings = [] 52 | # this gets a uniq on the findings and groups hosts, could be more efficient 53 | findings.each do |single| 54 | # check if the finding has been added before 55 | exists = vfindings.detect {|f| f["title"] == single.title } 56 | 57 | if exists 58 | #get the index 59 | i = vfindings.index(exists) 60 | exists.affected_hosts = clean(exists.affected_hosts+", #{single.affected_hosts}") 61 | if exists.notes 62 | exists.notes = exists.notes+"#{single.notes}" 63 | end 64 | vfindings[i] = exists 65 | else 66 | vfindings << single 67 | end 68 | end 69 | return vfindings 70 | end 71 | 72 | def parse_nessus_xml(xml,threshold) 73 | vulns = Hash.new 74 | findings = Array.new 75 | items = Array.new 76 | 77 | doc = Nokogiri::XML(xml) 78 | 79 | doc.css("//ReportHost").each do |hostnode| 80 | if (hostnode["name"] != nil) 81 | host = hostnode["name"] 82 | end 83 | hostnode.css("ReportItem").each do |itemnode| 84 | if (itemnode["port"] != "0" && itemnode["severity"] >= threshold) 85 | 86 | # create a temporary finding object 87 | finding = Findings.new() 88 | finding.title = itemnode['pluginName'].to_s() 89 | finding.overview = clean(itemnode.css("description").to_s) 90 | finding.remediation = clean(itemnode.css("solution").to_s) 91 | 92 | # can this be inherited from an import properly? 93 | finding.type = "Imported" 94 | 95 | finding.risk = itemnode["severity"] 96 | 97 | # hardcode the DREAD score, the user should fix this 98 | finding.damage = 1 99 | finding.reproducability = 1 100 | finding.exploitability = 1 101 | finding.affected_users = 1 102 | finding.discoverability = 1 103 | finding.dread_total = 1 104 | 105 | finding.affected_hosts = hostnode["name"] 106 | 107 | if itemnode.css("plugin_output") 108 | finding.notes = hostnode["name"]+" ("+itemnode["protocol"]+ " port " + itemnode["port"]+"):"+clean(itemnode.css("plugin_output").to_s) 109 | end 110 | 111 | finding.references = clean(itemnode.css("see_also").to_s) 112 | 113 | findings << finding 114 | items << itemnode['pluginID'].to_s() 115 | end 116 | end 117 | vulns[host] = items 118 | items = [] 119 | end 120 | 121 | vulns["findings"] = uniq_findings(findings) 122 | return vulns 123 | end 124 | 125 | def parse_burp_xml(xml) 126 | vulns = Hash.new 127 | findings = Array.new 128 | vulns["findings"] = [] 129 | 130 | doc = Nokogiri::XML(xml) 131 | doc.css('//issues/issue').each do |issue| 132 | if issue.css('severity').text 133 | # create a temporary finding object 134 | finding = Findings.new() 135 | finding.title = clean(issue.css('name').text.to_s()) 136 | finding.overview = clean(issue.css('issueBackground').text.to_s()+issue.css('issueDetail').text.to_s()) 137 | finding.remediation = clean(issue.css('remediationBackground').text.to_s()) 138 | 139 | if issue.css('severity').text == 'Low' 140 | finding.risk = 1 141 | elsif issue.css('severity').text == 'Medium' 142 | finding.risk = 2 143 | elsif issue.css('severity').text =='High' 144 | finding.risk = 3 145 | else 146 | finding.risk = 1 147 | end 148 | 149 | # hardcode the DREAD score, the user assign the risk 150 | finding.damage = 1 151 | finding.reproducability = 1 152 | finding.exploitability = 1 153 | finding.affected_users = 1 154 | finding.discoverability = 1 155 | finding.dread_total = 1 156 | finding.type = "Web Application" 157 | 158 | findings << finding 159 | 160 | host = issue.css('host').text 161 | ip = issue.css('host').attr('ip') 162 | id = issue.css('type').text 163 | hostname = "#{ip} #{host}" 164 | 165 | finding.affected_hosts = "#{host} (#{ip})" 166 | 167 | if vulns[hostname] 168 | vulns[hostname] << id 169 | else 170 | vulns[hostname] = [] 171 | vulns[hostname] << id 172 | end 173 | end 174 | end 175 | 176 | vulns["findings"] = uniq_findings(findings) 177 | return vulns 178 | end 179 | -------------------------------------------------------------------------------- /model/master.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'data_mapper' 3 | require 'digest/sha1' 4 | require 'dm-migrations' 5 | 6 | # Initialize the Master DB 7 | DataMapper.setup(:default, "sqlite://#{Dir.pwd}/db/master.db") 8 | 9 | 10 | class TemplateFindings 11 | include DataMapper::Resource 12 | 13 | property :id, Serial 14 | property :title, String, :required => true, :length => 200 15 | property :damage, Integer, :required => false 16 | property :reproducability, Integer, :required => false 17 | property :exploitability, Integer, :required => false 18 | property :affected_users, Integer, :required => false 19 | property :discoverability, Integer, :required => false 20 | property :dread_total, Integer, :required => false 21 | property :effort, String, :required => false 22 | property :type, String, :required => false 23 | property :overview, String, :length => 20000, :required => false 24 | property :poc, String, :length => 20000, :required => false 25 | property :remediation, String, :length => 20000, :required => false 26 | property :references, String, :length => 20000, :required => false 27 | property :approved, Boolean, :required => false, :default => true 28 | property :risk, Integer, :required => false 29 | property :affected_hosts, String, :length => 20000, :required => false 30 | # CVSS 31 | property :av, String, :required => false 32 | property :ac, String, :required => false 33 | property :au, String, :required => false 34 | property :c, String, :required => false 35 | property :i, String, :required => false 36 | property :a, String, :required => false 37 | property :e, String, :required => false 38 | property :rl, String, :required => false 39 | property :rc, String, :required => false 40 | property :cdp, String, :required => false 41 | property :td, String, :required => false 42 | property :cr, String, :required => false 43 | property :ir, String, :required => false 44 | property :ar, String, :required => false 45 | property :cvss_base, Float, :required => false 46 | property :cvss_impact, Float, :required => false 47 | property :cvss_exploitability, Float, :required => false 48 | property :cvss_temporal, Float, :required => false 49 | property :cvss_environmental, Float, :required => false 50 | property :cvss_modified_impact, Float, :required => false 51 | property :cvss_total, Float, :required => false 52 | property :ease, String, :required => false 53 | end 54 | 55 | class Findings 56 | include DataMapper::Resource 57 | 58 | property :id, Serial 59 | property :report_id, Integer, :required => true 60 | property :master_id, Integer, :required => false 61 | property :finding_modified, Boolean, :required => false 62 | property :title, String, :required => true, :length => 200 63 | property :damage, Integer, :required => false 64 | property :reproducability, Integer, :required => false 65 | property :exploitability, Integer, :required => false 66 | property :affected_users, Integer, :required => false 67 | property :discoverability, Integer, :required => false 68 | property :effort, String, :required => false 69 | property :type, String, :required => false 70 | property :dread_total, Integer, :required => false 71 | property :overview, String, :length => 20000, :required => false 72 | property :poc, String, :length => 20000, :required => false 73 | property :remediation, String, :length => 20000, :required => false 74 | property :notes, String, :length => 1000000, :required => false 75 | property :assessment_type, String, :required => false 76 | property :references, String, :length => 20000, :required => false 77 | property :risk, Integer, :required => false 78 | property :affected_hosts, String, :length => 1000000, :required => false 79 | property :presentation_points, String, :length => 100000, :required => false 80 | property :presentation_rem_points, String, :length => 100000, :required => false 81 | #CVSS 82 | property :av, String, :required => false 83 | property :ac, String, :required => false 84 | property :au, String, :required => false 85 | property :c, String, :required => false 86 | property :i, String, :required => false 87 | property :a, String, :required => false 88 | property :e, String, :required => false 89 | property :rl, String, :required => false 90 | property :rc, String, :required => false 91 | property :cdp, String, :required => false 92 | property :td, String, :required => false 93 | property :cr, String, :required => false 94 | property :ir, String, :required => false 95 | property :ar, String, :required => false 96 | property :cvss_base, Float, :required => false 97 | property :cvss_impact, Float, :required => false 98 | property :cvss_exploitability, Float, :required => false 99 | property :cvss_temporal, Float, :required => false 100 | property :cvss_environmental, Float, :required => false 101 | property :cvss_modified_impact, Float, :required => false 102 | property :cvss_total, Float, :required => false 103 | property :ease, String, :required => false 104 | 105 | end 106 | 107 | class TemplateReports 108 | include DataMapper::Resource 109 | 110 | property :id, Serial 111 | property :consultant_name, String, :required => false, :length => 200 112 | property :consultant_company, String, :required => false, :length => 200 113 | property :consultant_phone, String 114 | property :consultant_email, String, :required => false, :length => 200 115 | property :contact_name, String, :required => false, :length => 200 116 | property :contact_phone, String 117 | property :contact_email, String 118 | property :contact_city, String 119 | property :contact_address, String 120 | property :contact_zip, String 121 | property :full_company_name, String, :required => true, :length => 200 122 | property :short_company_name, String, :required => true, :length => 200 123 | property :company_website, String 124 | 125 | 126 | end 127 | 128 | class User 129 | include DataMapper::Resource 130 | 131 | property :id, Serial 132 | property :username, String, :key => true, :length => (3..40), :required => true 133 | property :hashed_password, String 134 | property :salt, String 135 | property :type, String 136 | property :auth_type, String, :required => false 137 | property :created_at, DateTime, :default => DateTime.now 138 | property :consultant_name, String, :required => false 139 | property :consultant_company, String, :required => false 140 | property :consultant_phone, String, :required => false 141 | property :consultant_email, String, :required => false 142 | property :consultant_title, String, :required => false 143 | 144 | attr_accessor :password 145 | validates_presence_of :username 146 | 147 | def password=(pass) 148 | @password = pass 149 | self.salt = rand(36**12).to_s(36) unless self.salt 150 | self.hashed_password = User.encrypt(@password, self.salt) 151 | end 152 | 153 | def self.encrypt(pass, salt) 154 | return Digest::SHA1.hexdigest(pass + salt) 155 | end 156 | 157 | def self.authenticate(username, pass) 158 | user = User.first(:username => username) 159 | if user 160 | return user.username if User.encrypt(pass, user.salt) == user.hashed_password 161 | end 162 | end 163 | 164 | end 165 | 166 | class Sessions 167 | include DataMapper::Resource 168 | 169 | property :id, Serial 170 | property :session_key, String, :length => 128 171 | property :username, String, :length => (3..40), :required => true 172 | 173 | def self.is_valid?(session_key) 174 | sessions = Sessions.first(:session_key => session_key) 175 | return true if sessions 176 | end 177 | 178 | def self.type(session_key) 179 | sess = Sessions.first(:session_key => session_key) 180 | 181 | if sess 182 | return User.first(:username => sess.username).type 183 | end 184 | end 185 | 186 | def self.get_username(session_key) 187 | sess = Sessions.first(:session_key => session_key) 188 | 189 | if sess 190 | return sess.username 191 | end 192 | end 193 | 194 | end 195 | 196 | # For a metasploit connector eventually 197 | class RemoteEndpoints 198 | include DataMapper::Resource 199 | 200 | property :id, Serial 201 | property :ip, String 202 | end 203 | 204 | class NessusMapping 205 | include DataMapper::Resource 206 | 207 | property :id, Serial 208 | property :templatefindings_id, String, :required => true 209 | property :pluginid, String, :required => true 210 | end 211 | 212 | class BurpMapping 213 | include DataMapper::Resource 214 | 215 | property :id, Serial 216 | property :templatefindings_id, String, :required => true 217 | property :pluginid, String, :required => true 218 | end 219 | 220 | class Reports 221 | include DataMapper::Resource 222 | 223 | property :id, Serial 224 | property :date, String, :length => 20 225 | property :report_type, String, :length => 200 226 | property :report_name, String, :length => 200 227 | property :consultant_name, String, :length => 200 228 | property :consultant_company, String, :length => 200 229 | property :consultant_phone, String 230 | property :consultant_title, String, :length => 200 231 | property :consultant_email, String, :length => 200 232 | property :contact_name, String, :length => 200 233 | property :contact_phone, String 234 | property :contact_title, String, :length => 200 235 | property :contact_email, String, :length => 200 236 | property :contact_city, String 237 | property :contact_address, String, :length => 200 238 | property :contact_state, String 239 | property :contact_zip, String 240 | property :full_company_name, String, :length => 200 241 | property :short_company_name, String, :length => 200 242 | property :company_website, String, :length => 200 243 | property :owner, String, :length => 200 244 | property :authors, CommaSeparatedList, :required => false, :lazy => false 245 | property :user_defined_variables, String, :length => 10000 246 | 247 | end 248 | 249 | class Attachments 250 | include DataMapper::Resource 251 | 252 | property :id, Serial 253 | property :filename, String, :length => 400 254 | property :filename_location, String, :length => 400 255 | property :report_id, String, :length => 30 256 | property :description, String, :length => 500 257 | 258 | end 259 | 260 | class Hosts 261 | include DataMapper::Resource 262 | 263 | property :id, Serial 264 | property :ip, String 265 | property :port, String 266 | 267 | end 268 | 269 | class Xslt 270 | include DataMapper::Resource 271 | 272 | property :id, Serial 273 | property :docx_location, String, :length => 400 274 | property :description, String, :length => 400 275 | property :xslt_location, String, :length => 400 276 | property :report_type, String, :length => 400 277 | property :finding_template, Boolean, :required => false, :default => false 278 | property :status_template, Boolean, :required => false, :default => false 279 | 280 | end 281 | 282 | DataMapper.finalize 283 | 284 | # any differences between the data store and the data model should be fixed by this 285 | # As discussed in http://datamapper.org/why.html it is limited. Hopefully we never create conflicts. 286 | DataMapper.auto_upgrade! 287 | -------------------------------------------------------------------------------- /public/css/signin.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee; 5 | } 6 | 7 | .form-signin { 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: 0 auto; 11 | } 12 | .form-signin .form-signin-heading, 13 | .form-signin .checkbox { 14 | margin-bottom: 10px; 15 | } 16 | .form-signin .checkbox { 17 | font-weight: normal; 18 | } 19 | .form-signin .form-control { 20 | position: relative; 21 | height: auto; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | padding: 10px; 26 | font-size: 16px; 27 | } 28 | .form-signin .form-control:focus { 29 | z-index: 2; 30 | } 31 | .form-signin input[type="email"] { 32 | margin-bottom: -1px; 33 | border-bottom-right-radius: 0; 34 | border-bottom-left-radius: 0; 35 | } 36 | .form-signin input[type="password"] { 37 | margin-bottom: 10px; 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/logo.jpg -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/logo.png -------------------------------------------------------------------------------- /public/img/logo_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/logo_1.jpg -------------------------------------------------------------------------------- /public/img/logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackingLab/PTReporter/20ebcfda4511273565c26c6ad2af725b1a486efd/public/img/logo_1.png -------------------------------------------------------------------------------- /public/js/bootstrap-affix.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-affix.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#affix 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* AFFIX CLASS DEFINITION 27 | * ====================== */ 28 | 29 | var Affix = function (element, options) { 30 | this.options = $.extend({}, $.fn.affix.defaults, options) 31 | this.$window = $(window) 32 | .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) 33 | .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) 34 | this.$element = $(element) 35 | this.checkPosition() 36 | } 37 | 38 | Affix.prototype.checkPosition = function () { 39 | if (!this.$element.is(':visible')) return 40 | 41 | var scrollHeight = $(document).height() 42 | , scrollTop = this.$window.scrollTop() 43 | , position = this.$element.offset() 44 | , offset = this.options.offset 45 | , offsetBottom = offset.bottom 46 | , offsetTop = offset.top 47 | , reset = 'affix affix-top affix-bottom' 48 | , affix 49 | 50 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 51 | if (typeof offsetTop == 'function') offsetTop = offset.top() 52 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() 53 | 54 | affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? 55 | false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 56 | 'bottom' : offsetTop != null && scrollTop <= offsetTop ? 57 | 'top' : false 58 | 59 | if (this.affixed === affix) return 60 | 61 | this.affixed = affix 62 | this.unpin = affix == 'bottom' ? position.top - scrollTop : null 63 | 64 | this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) 65 | } 66 | 67 | 68 | /* AFFIX PLUGIN DEFINITION 69 | * ======================= */ 70 | 71 | var old = $.fn.affix 72 | 73 | $.fn.affix = function (option) { 74 | return this.each(function () { 75 | var $this = $(this) 76 | , data = $this.data('affix') 77 | , options = typeof option == 'object' && option 78 | if (!data) $this.data('affix', (data = new Affix(this, options))) 79 | if (typeof option == 'string') data[option]() 80 | }) 81 | } 82 | 83 | $.fn.affix.Constructor = Affix 84 | 85 | $.fn.affix.defaults = { 86 | offset: 0 87 | } 88 | 89 | 90 | /* AFFIX NO CONFLICT 91 | * ================= */ 92 | 93 | $.fn.affix.noConflict = function () { 94 | $.fn.affix = old 95 | return this 96 | } 97 | 98 | 99 | /* AFFIX DATA-API 100 | * ============== */ 101 | 102 | $(window).on('load', function () { 103 | $('[data-spy="affix"]').each(function () { 104 | var $spy = $(this) 105 | , data = $spy.data() 106 | 107 | data.offset = data.offset || {} 108 | 109 | data.offsetBottom && (data.offset.bottom = data.offsetBottom) 110 | data.offsetTop && (data.offset.top = data.offsetTop) 111 | 112 | $spy.affix(data) 113 | }) 114 | }) 115 | 116 | 117 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-alert.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-alert.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#alerts 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* ALERT CLASS DEFINITION 27 | * ====================== */ 28 | 29 | var dismiss = '[data-dismiss="alert"]' 30 | , Alert = function (el) { 31 | $(el).on('click', dismiss, this.close) 32 | } 33 | 34 | Alert.prototype.close = function (e) { 35 | var $this = $(this) 36 | , selector = $this.attr('data-target') 37 | , $parent 38 | 39 | if (!selector) { 40 | selector = $this.attr('href') 41 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 42 | } 43 | 44 | $parent = $(selector) 45 | 46 | e && e.preventDefault() 47 | 48 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 49 | 50 | $parent.trigger(e = $.Event('close')) 51 | 52 | if (e.isDefaultPrevented()) return 53 | 54 | $parent.removeClass('in') 55 | 56 | function removeElement() { 57 | $parent 58 | .trigger('closed') 59 | .remove() 60 | } 61 | 62 | $.support.transition && $parent.hasClass('fade') ? 63 | $parent.on($.support.transition.end, removeElement) : 64 | removeElement() 65 | } 66 | 67 | 68 | /* ALERT PLUGIN DEFINITION 69 | * ======================= */ 70 | 71 | var old = $.fn.alert 72 | 73 | $.fn.alert = function (option) { 74 | return this.each(function () { 75 | var $this = $(this) 76 | , data = $this.data('alert') 77 | if (!data) $this.data('alert', (data = new Alert(this))) 78 | if (typeof option == 'string') data[option].call($this) 79 | }) 80 | } 81 | 82 | $.fn.alert.Constructor = Alert 83 | 84 | 85 | /* ALERT NO CONFLICT 86 | * ================= */ 87 | 88 | $.fn.alert.noConflict = function () { 89 | $.fn.alert = old 90 | return this 91 | } 92 | 93 | 94 | /* ALERT DATA-API 95 | * ============== */ 96 | 97 | $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) 98 | 99 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-button.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-button.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#buttons 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* BUTTON PUBLIC CLASS DEFINITION 27 | * ============================== */ 28 | 29 | var Button = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.button.defaults, options) 32 | } 33 | 34 | Button.prototype.setState = function (state) { 35 | var d = 'disabled' 36 | , $el = this.$element 37 | , data = $el.data() 38 | , val = $el.is('input') ? 'val' : 'html' 39 | 40 | state = state + 'Text' 41 | data.resetText || $el.data('resetText', $el[val]()) 42 | 43 | $el[val](data[state] || this.options[state]) 44 | 45 | // push to event loop to allow forms to submit 46 | setTimeout(function () { 47 | state == 'loadingText' ? 48 | $el.addClass(d).attr(d, d) : 49 | $el.removeClass(d).removeAttr(d) 50 | }, 0) 51 | } 52 | 53 | Button.prototype.toggle = function () { 54 | var $parent = this.$element.closest('[data-toggle="buttons-radio"]') 55 | 56 | $parent && $parent 57 | .find('.active') 58 | .removeClass('active') 59 | 60 | this.$element.toggleClass('active') 61 | } 62 | 63 | 64 | /* BUTTON PLUGIN DEFINITION 65 | * ======================== */ 66 | 67 | var old = $.fn.button 68 | 69 | $.fn.button = function (option) { 70 | return this.each(function () { 71 | var $this = $(this) 72 | , data = $this.data('button') 73 | , options = typeof option == 'object' && option 74 | if (!data) $this.data('button', (data = new Button(this, options))) 75 | if (option == 'toggle') data.toggle() 76 | else if (option) data.setState(option) 77 | }) 78 | } 79 | 80 | $.fn.button.defaults = { 81 | loadingText: 'loading...' 82 | } 83 | 84 | $.fn.button.Constructor = Button 85 | 86 | 87 | /* BUTTON NO CONFLICT 88 | * ================== */ 89 | 90 | $.fn.button.noConflict = function () { 91 | $.fn.button = old 92 | return this 93 | } 94 | 95 | 96 | /* BUTTON DATA-API 97 | * =============== */ 98 | 99 | $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { 100 | var $btn = $(e.target) 101 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 102 | $btn.button('toggle') 103 | }) 104 | 105 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-carousel.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-carousel.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#carousel 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* CAROUSEL CLASS DEFINITION 27 | * ========================= */ 28 | 29 | var Carousel = function (element, options) { 30 | this.$element = $(element) 31 | this.options = options 32 | this.options.pause == 'hover' && this.$element 33 | .on('mouseenter', $.proxy(this.pause, this)) 34 | .on('mouseleave', $.proxy(this.cycle, this)) 35 | } 36 | 37 | Carousel.prototype = { 38 | 39 | cycle: function (e) { 40 | if (!e) this.paused = false 41 | this.options.interval 42 | && !this.paused 43 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 44 | return this 45 | } 46 | 47 | , to: function (pos) { 48 | var $active = this.$element.find('.item.active') 49 | , children = $active.parent().children() 50 | , activePos = children.index($active) 51 | , that = this 52 | 53 | if (pos > (children.length - 1) || pos < 0) return 54 | 55 | if (this.sliding) { 56 | return this.$element.one('slid', function () { 57 | that.to(pos) 58 | }) 59 | } 60 | 61 | if (activePos == pos) { 62 | return this.pause().cycle() 63 | } 64 | 65 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) 66 | } 67 | 68 | , pause: function (e) { 69 | if (!e) this.paused = true 70 | if (this.$element.find('.next, .prev').length && $.support.transition.end) { 71 | this.$element.trigger($.support.transition.end) 72 | this.cycle() 73 | } 74 | clearInterval(this.interval) 75 | this.interval = null 76 | return this 77 | } 78 | 79 | , next: function () { 80 | if (this.sliding) return 81 | return this.slide('next') 82 | } 83 | 84 | , prev: function () { 85 | if (this.sliding) return 86 | return this.slide('prev') 87 | } 88 | 89 | , slide: function (type, next) { 90 | var $active = this.$element.find('.item.active') 91 | , $next = next || $active[type]() 92 | , isCycling = this.interval 93 | , direction = type == 'next' ? 'left' : 'right' 94 | , fallback = type == 'next' ? 'first' : 'last' 95 | , that = this 96 | , e 97 | 98 | this.sliding = true 99 | 100 | isCycling && this.pause() 101 | 102 | $next = $next.length ? $next : this.$element.find('.item')[fallback]() 103 | 104 | e = $.Event('slide', { 105 | relatedTarget: $next[0] 106 | }) 107 | 108 | if ($next.hasClass('active')) return 109 | 110 | if ($.support.transition && this.$element.hasClass('slide')) { 111 | this.$element.trigger(e) 112 | if (e.isDefaultPrevented()) return 113 | $next.addClass(type) 114 | $next[0].offsetWidth // force reflow 115 | $active.addClass(direction) 116 | $next.addClass(direction) 117 | this.$element.one($.support.transition.end, function () { 118 | $next.removeClass([type, direction].join(' ')).addClass('active') 119 | $active.removeClass(['active', direction].join(' ')) 120 | that.sliding = false 121 | setTimeout(function () { that.$element.trigger('slid') }, 0) 122 | }) 123 | } else { 124 | this.$element.trigger(e) 125 | if (e.isDefaultPrevented()) return 126 | $active.removeClass('active') 127 | $next.addClass('active') 128 | this.sliding = false 129 | this.$element.trigger('slid') 130 | } 131 | 132 | isCycling && this.cycle() 133 | 134 | return this 135 | } 136 | 137 | } 138 | 139 | 140 | /* CAROUSEL PLUGIN DEFINITION 141 | * ========================== */ 142 | 143 | var old = $.fn.carousel 144 | 145 | $.fn.carousel = function (option) { 146 | return this.each(function () { 147 | var $this = $(this) 148 | , data = $this.data('carousel') 149 | , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) 150 | , action = typeof option == 'string' ? option : options.slide 151 | if (!data) $this.data('carousel', (data = new Carousel(this, options))) 152 | if (typeof option == 'number') data.to(option) 153 | else if (action) data[action]() 154 | else if (options.interval) data.cycle() 155 | }) 156 | } 157 | 158 | $.fn.carousel.defaults = { 159 | interval: 5000 160 | , pause: 'hover' 161 | } 162 | 163 | $.fn.carousel.Constructor = Carousel 164 | 165 | 166 | /* CAROUSEL NO CONFLICT 167 | * ==================== */ 168 | 169 | $.fn.carousel.noConflict = function () { 170 | $.fn.carousel = old 171 | return this 172 | } 173 | 174 | /* CAROUSEL DATA-API 175 | * ================= */ 176 | 177 | $(document).on('click.carousel.data-api', '[data-slide]', function (e) { 178 | var $this = $(this), href 179 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 180 | , options = $.extend({}, $target.data(), $this.data()) 181 | $target.carousel(options) 182 | e.preventDefault() 183 | }) 184 | 185 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-collapse.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-collapse.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#collapse 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* COLLAPSE PUBLIC CLASS DEFINITION 27 | * ================================ */ 28 | 29 | var Collapse = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.collapse.defaults, options) 32 | 33 | if (this.options.parent) { 34 | this.$parent = $(this.options.parent) 35 | } 36 | 37 | this.options.toggle && this.toggle() 38 | } 39 | 40 | Collapse.prototype = { 41 | 42 | constructor: Collapse 43 | 44 | , dimension: function () { 45 | var hasWidth = this.$element.hasClass('width') 46 | return hasWidth ? 'width' : 'height' 47 | } 48 | 49 | , show: function () { 50 | var dimension 51 | , scroll 52 | , actives 53 | , hasData 54 | 55 | if (this.transitioning) return 56 | 57 | dimension = this.dimension() 58 | scroll = $.camelCase(['scroll', dimension].join('-')) 59 | actives = this.$parent && this.$parent.find('> .accordion-group > .in') 60 | 61 | if (actives && actives.length) { 62 | hasData = actives.data('collapse') 63 | if (hasData && hasData.transitioning) return 64 | actives.collapse('hide') 65 | hasData || actives.data('collapse', null) 66 | } 67 | 68 | this.$element[dimension](0) 69 | this.transition('addClass', $.Event('show'), 'shown') 70 | $.support.transition && this.$element[dimension](this.$element[0][scroll]) 71 | } 72 | 73 | , hide: function () { 74 | var dimension 75 | if (this.transitioning) return 76 | dimension = this.dimension() 77 | this.reset(this.$element[dimension]()) 78 | this.transition('removeClass', $.Event('hide'), 'hidden') 79 | this.$element[dimension](0) 80 | } 81 | 82 | , reset: function (size) { 83 | var dimension = this.dimension() 84 | 85 | this.$element 86 | .removeClass('collapse') 87 | [dimension](size || 'auto') 88 | [0].offsetWidth 89 | 90 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') 91 | 92 | return this 93 | } 94 | 95 | , transition: function (method, startEvent, completeEvent) { 96 | var that = this 97 | , complete = function () { 98 | if (startEvent.type == 'show') that.reset() 99 | that.transitioning = 0 100 | that.$element.trigger(completeEvent) 101 | } 102 | 103 | this.$element.trigger(startEvent) 104 | 105 | if (startEvent.isDefaultPrevented()) return 106 | 107 | this.transitioning = 1 108 | 109 | this.$element[method]('in') 110 | 111 | $.support.transition && this.$element.hasClass('collapse') ? 112 | this.$element.one($.support.transition.end, complete) : 113 | complete() 114 | } 115 | 116 | , toggle: function () { 117 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 118 | } 119 | 120 | } 121 | 122 | 123 | /* COLLAPSE PLUGIN DEFINITION 124 | * ========================== */ 125 | 126 | var old = $.fn.collapse 127 | 128 | $.fn.collapse = function (option) { 129 | return this.each(function () { 130 | var $this = $(this) 131 | , data = $this.data('collapse') 132 | , options = typeof option == 'object' && option 133 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 134 | if (typeof option == 'string') data[option]() 135 | }) 136 | } 137 | 138 | $.fn.collapse.defaults = { 139 | toggle: true 140 | } 141 | 142 | $.fn.collapse.Constructor = Collapse 143 | 144 | 145 | /* COLLAPSE NO CONFLICT 146 | * ==================== */ 147 | 148 | $.fn.collapse.noConflict = function () { 149 | $.fn.collapse = old 150 | return this 151 | } 152 | 153 | 154 | /* COLLAPSE DATA-API 155 | * ================= */ 156 | 157 | $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { 158 | var $this = $(this), href 159 | , target = $this.attr('data-target') 160 | || e.preventDefault() 161 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 162 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 163 | $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') 164 | $(target).collapse(option) 165 | }) 166 | 167 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-dropdown.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-dropdown.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* DROPDOWN CLASS DEFINITION 27 | * ========================= */ 28 | 29 | var toggle = '[data-toggle=dropdown]' 30 | , Dropdown = function (element) { 31 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 32 | $('html').on('click.dropdown.data-api', function () { 33 | $el.parent().removeClass('open') 34 | }) 35 | } 36 | 37 | Dropdown.prototype = { 38 | 39 | constructor: Dropdown 40 | 41 | , toggle: function (e) { 42 | var $this = $(this) 43 | , $parent 44 | , isActive 45 | 46 | if ($this.is('.disabled, :disabled')) return 47 | 48 | $parent = getParent($this) 49 | 50 | isActive = $parent.hasClass('open') 51 | 52 | clearMenus() 53 | 54 | if (!isActive) { 55 | $parent.toggleClass('open') 56 | } 57 | 58 | $this.focus() 59 | 60 | return false 61 | } 62 | 63 | , keydown: function (e) { 64 | var $this 65 | , $items 66 | , $active 67 | , $parent 68 | , isActive 69 | , index 70 | 71 | if (!/(38|40|27)/.test(e.keyCode)) return 72 | 73 | $this = $(this) 74 | 75 | e.preventDefault() 76 | e.stopPropagation() 77 | 78 | if ($this.is('.disabled, :disabled')) return 79 | 80 | $parent = getParent($this) 81 | 82 | isActive = $parent.hasClass('open') 83 | 84 | if (!isActive || (isActive && e.keyCode == 27)) return $this.click() 85 | 86 | $items = $('[role=menu] li:not(.divider):visible a', $parent) 87 | 88 | if (!$items.length) return 89 | 90 | index = $items.index($items.filter(':focus')) 91 | 92 | if (e.keyCode == 38 && index > 0) index-- // up 93 | if (e.keyCode == 40 && index < $items.length - 1) index++ // down 94 | if (!~index) index = 0 95 | 96 | $items 97 | .eq(index) 98 | .focus() 99 | } 100 | 101 | } 102 | 103 | function clearMenus() { 104 | $(toggle).each(function () { 105 | getParent($(this)).removeClass('open') 106 | }) 107 | } 108 | 109 | function getParent($this) { 110 | var selector = $this.attr('data-target') 111 | , $parent 112 | 113 | if (!selector) { 114 | selector = $this.attr('href') 115 | selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 116 | } 117 | 118 | $parent = $(selector) 119 | $parent.length || ($parent = $this.parent()) 120 | 121 | return $parent 122 | } 123 | 124 | 125 | /* DROPDOWN PLUGIN DEFINITION 126 | * ========================== */ 127 | 128 | var old = $.fn.dropdown 129 | 130 | $.fn.dropdown = function (option) { 131 | return this.each(function () { 132 | var $this = $(this) 133 | , data = $this.data('dropdown') 134 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 135 | if (typeof option == 'string') data[option].call($this) 136 | }) 137 | } 138 | 139 | $.fn.dropdown.Constructor = Dropdown 140 | 141 | 142 | /* DROPDOWN NO CONFLICT 143 | * ==================== */ 144 | 145 | $.fn.dropdown.noConflict = function () { 146 | $.fn.dropdown = old 147 | return this 148 | } 149 | 150 | 151 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 152 | * =================================== */ 153 | 154 | $(document) 155 | .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) 156 | .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 157 | .on('touchstart.dropdown.data-api', '.dropdown-menu', function (e) { e.stopPropagation() }) 158 | .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) 159 | .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) 160 | 161 | }(window.jQuery); -------------------------------------------------------------------------------- /public/js/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v2.2.2 3 | * http://twitter.github.com/bootstrap/javascript.html#modals 4 | * ========================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* MODAL CLASS DEFINITION 27 | * ====================== */ 28 | 29 | var Modal = function (element, options) { 30 | this.options = options 31 | this.$element = $(element) 32 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 33 | this.options.remote && this.$element.find('.modal-body').load(this.options.remote) 34 | } 35 | 36 | Modal.prototype = { 37 | 38 | constructor: Modal 39 | 40 | , toggle: function () { 41 | return this[!this.isShown ? 'show' : 'hide']() 42 | } 43 | 44 | , show: function () { 45 | var that = this 46 | , e = $.Event('show') 47 | 48 | this.$element.trigger(e) 49 | 50 | if (this.isShown || e.isDefaultPrevented()) return 51 | 52 | this.isShown = true 53 | 54 | this.escape() 55 | 56 | this.backdrop(function () { 57 | var transition = $.support.transition && that.$element.hasClass('fade') 58 | 59 | if (!that.$element.parent().length) { 60 | that.$element.appendTo(document.body) //don't move modals dom position 61 | } 62 | 63 | that.$element 64 | .show() 65 | 66 | if (transition) { 67 | that.$element[0].offsetWidth // force reflow 68 | } 69 | 70 | that.$element 71 | .addClass('in') 72 | .attr('aria-hidden', false) 73 | 74 | that.enforceFocus() 75 | 76 | transition ? 77 | that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : 78 | that.$element.focus().trigger('shown') 79 | 80 | }) 81 | } 82 | 83 | , hide: function (e) { 84 | e && e.preventDefault() 85 | 86 | var that = this 87 | 88 | e = $.Event('hide') 89 | 90 | this.$element.trigger(e) 91 | 92 | if (!this.isShown || e.isDefaultPrevented()) return 93 | 94 | this.isShown = false 95 | 96 | this.escape() 97 | 98 | $(document).off('focusin.modal') 99 | 100 | this.$element 101 | .removeClass('in') 102 | .attr('aria-hidden', true) 103 | 104 | $.support.transition && this.$element.hasClass('fade') ? 105 | this.hideWithTransition() : 106 | this.hideModal() 107 | } 108 | 109 | , enforceFocus: function () { 110 | var that = this 111 | $(document).on('focusin.modal', function (e) { 112 | if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { 113 | that.$element.focus() 114 | } 115 | }) 116 | } 117 | 118 | , escape: function () { 119 | var that = this 120 | if (this.isShown && this.options.keyboard) { 121 | this.$element.on('keyup.dismiss.modal', function ( e ) { 122 | e.which == 27 && that.hide() 123 | }) 124 | } else if (!this.isShown) { 125 | this.$element.off('keyup.dismiss.modal') 126 | } 127 | } 128 | 129 | , hideWithTransition: function () { 130 | var that = this 131 | , timeout = setTimeout(function () { 132 | that.$element.off($.support.transition.end) 133 | that.hideModal() 134 | }, 500) 135 | 136 | this.$element.one($.support.transition.end, function () { 137 | clearTimeout(timeout) 138 | that.hideModal() 139 | }) 140 | } 141 | 142 | , hideModal: function (that) { 143 | this.$element 144 | .hide() 145 | .trigger('hidden') 146 | 147 | this.backdrop() 148 | } 149 | 150 | , removeBackdrop: function () { 151 | this.$backdrop.remove() 152 | this.$backdrop = null 153 | } 154 | 155 | , backdrop: function (callback) { 156 | var that = this 157 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 158 | 159 | if (this.isShown && this.options.backdrop) { 160 | var doAnimate = $.support.transition && animate 161 | 162 | this.$backdrop = $('