├── .gitignore ├── assets ├── images │ ├── soapLogo.png │ └── soapLogoW.png ├── SOAP-alphaReport.pdf ├── js │ ├── views │ │ ├── home.js │ │ ├── policyView.js │ │ └── questionsView.js │ ├── controllers │ │ ├── policyPage.js │ │ ├── home.js │ │ └── questionPage.js │ ├── templates │ │ ├── intro.js │ │ ├── homeSection.js │ │ ├── policyTemplate.js │ │ └── questionsTemplate.js │ ├── transition.js │ ├── styling.js │ ├── utils.js │ ├── snapshot.js │ ├── init.js │ ├── overlay.js │ ├── keyboard.js │ ├── subpolicies.js │ ├── start.js │ ├── edit.js │ └── policy.js ├── css │ ├── webfonts │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.woff │ │ ├── fa-solid-900.woff2 │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── SinkinSans-100Thin.otf │ │ ├── SinkinSans-300Light.otf │ │ ├── SinkinSans-700Bold.otf │ │ ├── SinkinSans-800Black.otf │ │ ├── SinkinSans-200XLight.otf │ │ ├── SinkinSans-400Italic.otf │ │ ├── SinkinSans-400Regular.otf │ │ ├── SinkinSans-500Medium.otf │ │ ├── SinkinSans-600SemiBold.otf │ │ ├── SinkinSans-900XBlack.otf │ │ ├── SinkinSans-100ThinItalic.otf │ │ ├── SinkinSans-700BoldItalic.otf │ │ ├── SinkinSans-200XLightItalic.otf │ │ ├── SinkinSans-300LightItalic.otf │ │ ├── SinkinSans-500MediumItalic.otf │ │ ├── SinkinSans-600SemiBoldItali.otf │ │ ├── SinkinSans-800BlackItalic.otf │ │ └── SinkinSans-900XBlackItalic.otf │ ├── media.css │ └── styles.css └── SecuringCivilSociety-report.pdf ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── content-request.md │ └── bug_report.md ├── server.py ├── content ├── section-7.js ├── rest-of-site.js ├── section-6.js ├── section-1.js └── section-3.js ├── 404.html ├── code-of-conduct.md ├── README.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store -------------------------------------------------------------------------------- /assets/images/soapLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/images/soapLogo.png -------------------------------------------------------------------------------- /assets/SOAP-alphaReport.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/SOAP-alphaReport.pdf -------------------------------------------------------------------------------- /assets/images/soapLogoW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/images/soapLogoW.png -------------------------------------------------------------------------------- /assets/js/views/home.js: -------------------------------------------------------------------------------- 1 | views.home = function(data, params){ 2 | controllers['homePage'](data, params); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /assets/css/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /assets/SecuringCivilSociety-report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/SecuringCivilSociety-report.pdf -------------------------------------------------------------------------------- /assets/css/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /assets/css/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /assets/css/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /assets/css/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /assets/css/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /assets/css/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /assets/css/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /assets/css/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /assets/css/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /assets/css/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /assets/js/views/policyView.js: -------------------------------------------------------------------------------- 1 | views.policyView = function(data, params){ 2 | controllers['policyPage'](data, params); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-100Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-100Thin.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-300Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-300Light.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-700Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-700Bold.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-800Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-800Black.otf -------------------------------------------------------------------------------- /assets/js/views/questionsView.js: -------------------------------------------------------------------------------- 1 | views.questionsView = function(data, params){ 2 | controllers['questionPage'](data, params); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-200XLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-200XLight.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-400Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-400Italic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-400Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-400Regular.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-500Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-500Medium.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-600SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-600SemiBold.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-900XBlack.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-900XBlack.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-100ThinItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-100ThinItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-700BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-700BoldItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-200XLightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-200XLightItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-300LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-300LightItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-500MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-500MediumItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-600SemiBoldItali.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-600SemiBoldItali.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-800BlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-800BlackItalic.otf -------------------------------------------------------------------------------- /assets/css/webfonts/SinkinSans-900XBlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gembarrett/soap/HEAD/assets/css/webfonts/SinkinSans-900XBlackItalic.otf -------------------------------------------------------------------------------- /assets/js/controllers/policyPage.js: -------------------------------------------------------------------------------- 1 | controllers.policyPage = function(data, params){ 2 | var policyContainer = templates.policyTemplate(); 3 | utils.render('page', policyContainer); 4 | }; 5 | -------------------------------------------------------------------------------- /assets/js/controllers/home.js: -------------------------------------------------------------------------------- 1 | controllers.homePage = function(data, params){ 2 | var homeContent = templates.intro(); 3 | homeContent += '
'; 4 | homeContent += templates.homeSection(textStore.ros[0].what); 5 | homeContent += templates.homeSection(textStore.ros[0].who); 6 | homeContent += templates.homeSection(textStore.ros[0].how); 7 | homeContent += templates.homeSection(textStore.ros[0].security); 8 | homeContent += templates.homeSection(textStore.ros[0].background); 9 | homeContent += templates.homeSection(textStore.ros[0].support); 10 | homeContent += '
'; 11 | document.querySelector('body').classList.remove('buildPage'); 12 | utils.render('page', homeContent); 13 | }; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: gembarrett 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /assets/js/templates/intro.js: -------------------------------------------------------------------------------- 1 | templates.intro = function(data){ 2 | var text = textStore.ros[0]; 3 | var content = ` 4 | 15 | 16 | `; 17 | return content; 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/content-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Content request 3 | about: Suggest improvements to the content 4 | title: "[CONTENT]" 5 | labels: content 6 | assignees: gembarrett 7 | 8 | --- 9 | 10 | **Is your content request related to existing content or content you would like to see added? Please describe.** 11 | A clear and concise description of what the content is. Ex. It is useful when security policies include [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Share any supporting resources** 17 | This information will help with understanding the topic, it's importance and how to present it in the process. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the content request here. 21 | -------------------------------------------------------------------------------- /assets/js/templates/homeSection.js: -------------------------------------------------------------------------------- 1 | templates.homeSection = function(data){ 2 | var text = data[0]; 3 | var list = ""; 4 | // if there's a list 5 | if (text.list !== ""){ 6 | list += `<`+text.list[0].type+`>`; 7 | // get each list item 8 | for (var i = 0; i`+text.list[0].content[i]+``; 10 | } 11 | list += ``; 12 | } 13 | var moreText = text.more.join('\n'); 14 | var content = ` 15 |
16 |

`+text.head+`

17 |

`+text.subhead+`

` 18 | +list+ 19 | `
20 | More 21 |

`+moreText+`

22 |
23 |
24 |
`; 25 | return content; 26 | }; 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: gembarrett 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Modification of `python -m SimpleHTTPServer` with a fallback to /index.html 4 | on requests for non-existing files. 5 | 6 | This is useful when serving a static single page application using the HTML5 7 | history API. 8 | """ 9 | 10 | 11 | import os 12 | import sys 13 | import urlparse 14 | import SimpleHTTPServer 15 | import BaseHTTPServer 16 | 17 | 18 | class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler): 19 | def do_GET(self): 20 | urlparts = urlparse.urlparse(self.path) 21 | request_file_path = urlparts.path.strip('/') 22 | 23 | if not os.path.exists(request_file_path): 24 | self.path = 'index.html' 25 | 26 | return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 27 | 28 | 29 | host = '127.0.0.1' 30 | try: 31 | port = int(sys.argv[1]) 32 | except IndexError: 33 | port = 8000 34 | httpd = BaseHTTPServer.HTTPServer((host, port), Handler) 35 | 36 | 37 | print 'Serving HTTP on %s port %d ...' % (host, port) 38 | httpd.serve_forever() 39 | -------------------------------------------------------------------------------- /assets/js/transition.js: -------------------------------------------------------------------------------- 1 | function getNameFromHome(){ 2 | // find the input field 3 | var homeOrgName = document.getElementById('home-q1-0-answer'); 4 | // if there's an Org Name entered 5 | if (homeOrgName.value !== ""){ 6 | // collect the name and store it in a variable 7 | homeName = homeOrgName.value; 8 | // go to #build 9 | window.location.href="/#build"; 10 | } else { 11 | console.log('No organization name provided'); 12 | } 13 | } 14 | 15 | function checkForName() { 16 | // if there's a name been entered previously 17 | if (homeName !== ""){ 18 | // autofill q1 input field with the variable 19 | var orgNameInput = document.querySelectorAll('#q1-0-answer'); 20 | orgNameInput[0].value = homeName; 21 | // document.getElementById('q1').classList.add('editable'); 22 | // currentState.questionC++; 23 | // currentState.questionQ++; 24 | // currentState.questionP++; 25 | // moveForward(1); 26 | // nextQuestion(); 27 | } else { 28 | console.log('No organization name stored'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/js/styling.js: -------------------------------------------------------------------------------- 1 | // this needs to be applied to textareas too 2 | function resizingBoxes() { 3 | var boxes = document.querySelectorAll("textarea"); 4 | for (var i = 0; i < boxes.length; i++) { 5 | boxes[i].setAttribute('style', 'height:' + (boxes[i].scrollHeight) + 'px;overflow-x:hidden;'); 6 | boxes[i].addEventListener("input", onInput, false); 7 | } 8 | } 9 | 10 | function onInput() { 11 | this.style.height = 'auto'; 12 | this.style.height = (this.scrollHeight) + 'px'; 13 | } 14 | 15 | var headChange = document.getElementById('head'); 16 | window.onscroll = function () { 17 | "use strict"; 18 | if (document.documentElement.scrollTop >= headChange.offsetHeight ) { 19 | headChange.classList.add("nav-scroll"); 20 | headChange.classList.remove("nav-start"); 21 | document.getElementById('logo').src="assets/images/soapLogo.png"; 22 | } 23 | else { 24 | headChange.classList.add("nav-start"); 25 | headChange.classList.remove("nav-scroll"); 26 | document.getElementById('logo').src="assets/images/soapLogoW.png"; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /assets/js/utils.js: -------------------------------------------------------------------------------- 1 | var utils = (function(){ 2 | var extract_params = function(params_string){ 3 | var params = {}; 4 | var raw_params = params_string.split('&'); 5 | 6 | var j = 0; 7 | for(var i = raw_params.length - 1; i >= 0; i--){ 8 | var url_params = raw_params[i].split('='); 9 | if(url_params.length == 2){ 10 | params[url_params[0]] = url_params[1]; 11 | } 12 | else if(url_params.length == 1){ 13 | params[j] = url_params[0]; 14 | j += 1; 15 | } 16 | else{ 17 | //param not readable. pass. 18 | } 19 | } 20 | 21 | return params; 22 | }; 23 | 24 | return { 25 | router: function(route, data){ 26 | route = route || location.hash.slice(1) || 'home'; 27 | var temp = route.split('?'); 28 | var route_split = temp.length; 29 | var function_to_invoke; 30 | if ((temp[0] === 'build') || (temp[0].startsWith('b'))) { 31 | function_to_invoke = 'questionsView'; 32 | } else if (temp[0] === 'policy') { 33 | function_to_invoke = 'policyView'; 34 | } else { 35 | function_to_invoke = temp[0] || false; 36 | } 37 | 38 | if(route_split > 1){ 39 | var params = extract_params(temp[1]); 40 | } 41 | 42 | if(function_to_invoke){ 43 | views[function_to_invoke](data, params); 44 | } 45 | }, 46 | 47 | render: function(element_id, content){ 48 | document.getElementById(element_id).innerHTML = content; 49 | }, 50 | 51 | }; 52 | 53 | })(); 54 | -------------------------------------------------------------------------------- /assets/js/controllers/questionPage.js: -------------------------------------------------------------------------------- 1 | controllers.questionPage = function(data, params){ 2 | document.getElementById('bu').classList.add('active'); 3 | // add class to body to change nav-start/scroll 4 | document.querySelector('body').classList.add('buildPage'); 5 | var templateContext = []; 6 | window.scrollTo(0,0); 7 | // queue up all the questions in this section 8 | // for each of the sections 9 | for (var i = 0; i < sections.length; i++){ 10 | // get each of the questions 11 | for (var j = 0; j < sections[i].length; j++){ 12 | var el = sections[i][j]; 13 | if (el.isQuestion === true) { 14 | var item = { 15 | 'q': el.q, 16 | 'answers': el.answers, 17 | 'id': el.id, 18 | 'tips': el.tips, 19 | 'isQ':true, 20 | 'required': el.required 21 | }; 22 | } else if (el.id === 'q0'){ 23 | var item = { 24 | 'id':el.id, 25 | 'title':el.title, 26 | 'contentArray': el.steps, 27 | }; 28 | } else if (el.id === 'q49'){ // update this if new questions added so it's always targetting last one 29 | var item = { 30 | 'id':el.id, 31 | 'q': el.q, 32 | 'desc': el.desc, 33 | 'teams':el.teams, 34 | 'areas':el.contents 35 | } 36 | } else { 37 | var item = { 38 | 'id':el.id, 39 | 'title':el.title, 40 | 'contentArray': el.paragraph, 41 | }; 42 | } 43 | templateContext.push(item); 44 | } 45 | } 46 | 47 | // put that data into the template and return it for rendering 48 | var questionContainer = templates.questionsTemplate(templateContext, params); 49 | utils.render('page', questionContainer); 50 | }; 51 | -------------------------------------------------------------------------------- /assets/js/snapshot.js: -------------------------------------------------------------------------------- 1 | function copyUrl(){ 2 | try { 3 | // does copy work here? 4 | if (document.queryCommandSupported('copy')) { 5 | var copyText = document.querySelector('#snapshotLink'); 6 | copyText.select(); 7 | document.execCommand('copy'); 8 | } else { 9 | window.alert("Sorry, SOAP can\'t access your clipboard right now."); 10 | } 11 | } catch (error){ 12 | console.log(error); 13 | } 14 | } 15 | 16 | function getSnapshotURL(){ 17 | // var snapshotUrl = "https://usesoap.app/#b"; 18 | var snapshotUrl = thisEnv+"/#b-"+soapv+"-p"; 19 | var qNo = "0"; 20 | for (var i = 0; i < currentState.answers.length; i++){ 21 | // if we're on the same question 22 | if (qNo === currentState.answers[i].q){ 23 | // add the a value to rest of that answer group 24 | snapshotUrl += currentState.answers[i].a; 25 | } else if (isCheckableQ(parseInt(currentState.answers[i].q))) { 26 | // get the new question number 27 | qNo = currentState.answers[i].q; 28 | // start new answer group, format appropriately if it's the first answer 29 | snapshotUrl += snapshotUrl[snapshotUrl.length - 1] === "p" ? "?" : "_"; 30 | // add the question number and first answer for that question 31 | snapshotUrl += qNo + "-" + currentState.answers[i].a; 32 | } else { 33 | // tell the user why they're not getting what they expect 34 | console.log('either no answers or no questions'); 35 | } 36 | } 37 | // get the input box and update the value 38 | document.querySelector('#snapshotLink').value = snapshotUrl; 39 | // show the link 40 | document.querySelector('#snapshotGroup').classList.remove('hidden'); 41 | } 42 | 43 | function isCheckableQ(q){ 44 | var btnsArr = [2, 3, 5, 6, 7, 8, 10, 11, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 29, 30, 31, 32, 36, 37, 38, 40, 41, 44, 45, 46, 47]; 45 | return btnsArr.includes(q) ? true : false; 46 | } 47 | -------------------------------------------------------------------------------- /content/section-7.js: -------------------------------------------------------------------------------- 1 | var section7 = [ 2 | // this section should always be last so update numbers accordingly when new content is added to other sections 3 | { 4 | "isQuestion": false, 5 | "id": "q49", 6 | "q": "Last question! Do you need specific policies for different teams within your organisation?", 7 | "desc": ["Using the options below, create up to 10 team-specific policies with rules that are applicable to particular groups of people. This can be useful if your organisation has several departments for whom the full policy may not be applicable. For instance, your finance team might not require the travel security section, or your volunteers may only need to know about your organisation's account and device security rules.", "In the left column you can specify who these shorter policies should be for. Name and select a team to see the policy areas that will be included in their subpolicy - choose as many as are applicable. Incident response measures and contextual information are included in all team policies by default.", "You can see what's in each policy area by checking the preview. When you're done, just hit Next and your team policies will be available along with your full policy on the next page. If you don't need any team policies then leave everything unselected and you'll get just your full policy and appendix."], 8 | "teams": [ 9 | { 10 | "name": "Team 1" 11 | }, 12 | { 13 | "name": "Team 2" 14 | }, 15 | ], 16 | "contents": [ 17 | { 18 | "name": "device security", 19 | "desc":"(inc. work profiles, touchID and backups)" 20 | }, 21 | { 22 | "name": "communications security", 23 | "desc":"(inc. encrypting messages, safe browsing, preferred platforms)", 24 | }, 25 | { 26 | "name": "accounts security", 27 | "desc":"(inc. password management and other authentication methods)", 28 | }, 29 | { 30 | "name": "travel security", 31 | "desc":"(inc. preparing devices, VPNs, emergency contact)", 32 | }, 33 | { 34 | "name": "environmental security", 35 | "desc":"(inc. locked storage, visitors, office security)", 36 | }, 37 | { 38 | "name": "network security", 39 | "desc":"(inc. WiFi passwords, browser security, firewalls)", 40 | } 41 | ] 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page not found - SOAP: Securing Organizations with Automated Policymaking 7 | 8 | 9 | 10 | 11 | 12 | 27 |
28 |
29 |

That page doesn't seem to exist

30 |

Looking to build a security policy? Start here

31 |

Want more information about SOAP? Go to the home page

32 |

See a problem with this site? Raise an issue on GitHub here or send an email.

33 |
34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /assets/js/init.js: -------------------------------------------------------------------------------- 1 | // update current SOAP version here 2 | const soapv = "1.2.0"; 3 | const thisEnv = window.location.origin; 4 | 5 | const templates = {}; 6 | const controllers = {}; 7 | const views = {}; 8 | 9 | let textStore = { 10 | qs : {}, 11 | ros : {}, 12 | } 13 | 14 | // set up progress tracking 15 | let currentState = { 16 | // which number in the section queue are we? 17 | sectionC: 0, 18 | // which section's data is in use? 19 | sectionQ: null, 20 | // which number in the question queue are we? 21 | questionC: 0, 22 | // which question's data is in use? 23 | questionQ: null, 24 | // position in section 25 | questionP: 0, 26 | // which answers have been given for which questions? 27 | answers: [], 28 | // list of exclusions, updated on every submission and checked on every question load 29 | exclusions: [] 30 | } 31 | 32 | let sections = []; 33 | let questionsList = []; 34 | 35 | // for storing the storeAs names and values 36 | let dict = {}; 37 | // for storing the team-content names and values 38 | let teamContent = []; 39 | 40 | // for storing the orgName when it's entered on the home page 41 | let homeName = ""; 42 | 43 | // for holding the end result 44 | let policyText = []; 45 | let appendixText = []; 46 | let output; 47 | 48 | async function loadContentFromJSON(lang) { 49 | const response = await fetch(`${thisEnv}/content/${lang}-content.json`); 50 | const json = await response.json(); 51 | textStore.qs = json.qs; 52 | textStore.ros = json.ros; 53 | buildTheQuestionsQueue(textStore.qs); 54 | } 55 | 56 | window.onload = function(){ 57 | // document.querySelector('#no-js').remove(); 58 | 59 | loadContentFromJSON('en').then(()=>{ 60 | // buildMobileMenu(sections); 61 | window.addEventListener( 62 | "hashchange", 63 | function(){utils.router()} 64 | ); 65 | utils.router(); 66 | // setUpFeedback(); 67 | }); 68 | 69 | }; 70 | 71 | // initialise counters with the first section and question, this is updated at the end of questions and sections 72 | 73 | function buildTheQuestionsQueue(qjson){ 74 | // list of sections 75 | sections = [qjson.section0, qjson.section1, qjson.section2, qjson.section3, qjson.section4, qjson.section5, qjson.section6, qjson.section7]; 76 | // loop through and create list of questions 77 | // for each of the sections 78 | for (var i = 0; i < sections.length; i++) { 79 | // get the section data 80 | tmpContent = sections[i]; 81 | // for each of the questions in that section 82 | for (var j = 0; j < tmpContent.length; j++) { 83 | // push the id to the queue 84 | questionsList.push(tmpContent[j].id); 85 | } 86 | } 87 | startProgress(sections[0], questionsList[0]); 88 | } 89 | 90 | function startProgress(startingSection, startingQuestion){ 91 | currentState.sectionQ = startingSection; 92 | currentState.questionQ = startingQuestion; 93 | } 94 | 95 | 96 | -------------------------------------------------------------------------------- /assets/js/overlay.js: -------------------------------------------------------------------------------- 1 | // File for all the overlay-related functions 2 | 3 | // function to add the overlay to the page 4 | function injectOverlay() { 5 | var parent = document.querySelector("#page"); 6 | parent.insertAdjacentHTML('afterend', '
'); 7 | 8 | var modal = document.querySelector("#preview"); 9 | var scrollbox = document.querySelector("#inner"); 10 | var overlay = document.querySelector("#overlay"); 11 | var close = document.querySelector("#closePreview"); 12 | var open = document.querySelector("#previewPolicy"); 13 | 14 | close.addEventListener("click", function() { 15 | toggleModal(modal, overlay); 16 | }); 17 | overlay.addEventListener("click", function() { 18 | toggleModal(modal, overlay); 19 | }); 20 | 21 | open.addEventListener("click", function() { 22 | collectAnswers(false); 23 | policyText = compileDoc(true, false); 24 | scrollbox.innerHTML = policyText.html; 25 | toggleModal(modal, overlay); 26 | }); 27 | } 28 | 29 | // function to show/hide the preview overlay 30 | function toggleModal(m, o) { 31 | m.classList.toggle("closed"); 32 | o.classList.toggle("closed"); 33 | } 34 | 35 | // function to show/hide the info panel 36 | function toggleInfo(id) { 37 | el = ".panel-"+id; 38 | panel = document.querySelector(el); 39 | panel.classList.toggle("closed"); 40 | query = '#info-trigger-'+id; 41 | document.querySelector(query).classList.toggle("highlight"); 42 | } 43 | 44 | function setUpFeedback(){ 45 | // listen for click on feedback button 46 | var feedbackBtn = document.getElementById('feedbackBtn'); 47 | var mod = document.getElementById("feedback"); 48 | var ove = document.getElementById("overlay-feedback"); 49 | // when it happens, toggle the visibility of these elements and generate the mailto link 50 | feedbackBtn.addEventListener('click', function() { 51 | // what page are we on 52 | page = whereAreWe(); 53 | var link; 54 | // if we're on a question page 55 | if (page !== "Question ID not found"){ 56 | // generate the mailto link, containing the info if it exists 57 | link = "mailto:feedback@usesoap.app?subject=Suggested%20change%20(question%20"+page+")"; 58 | } else { 59 | link = "mailto:feedback@usesoap.app?subject=Suggested%20change"; 60 | } 61 | 62 | // update the link used in the modal 63 | document.getElementById('updateLink').href = link; 64 | // then show the modal and overlay 65 | toggleModal(mod,ove); 66 | }, false); 67 | // listen for click on close button or outside overlay 68 | var close = document.querySelector("#closeFeedback"); 69 | close.addEventListener("click", function() { 70 | toggleModal(mod, ove); 71 | }); 72 | ove.addEventListener("click", function() { 73 | toggleModal(mod, ove); 74 | }); 75 | } 76 | 77 | function whereAreWe(){ 78 | // is currentState available 79 | if (currentState.questionQ){ 80 | // return the questionQ 81 | return currentState.questionQ; 82 | } 83 | // if not then handle error 84 | else { 85 | return "Question ID not found"; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gem@usesoap.app. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /assets/js/templates/policyTemplate.js: -------------------------------------------------------------------------------- 1 | templates.policyTemplate = function(data){ 2 | var docContent = compileDoc(true, true); 3 | output = docContent; 4 | var resources = textStore.ros[0].background[0].links; 5 | // insert resourcesIntro 6 | var links = "

For more information on organizational and personal security, and how you can get the most out of your new policy, check out these resources:

"; 7 | for (var r = 0; r`+resources[r].name+``; 10 | } 11 | // insert textPolicyLabel 12 | txt = ''; 13 | // insert editPolicyLabel 14 | editTxt = ''; 15 | // insert markdownPolicyLabel 16 | md = ''; 17 | // insert htmlPolicyLabel 18 | html = ''; 19 | // insert policyHeading, policyPreFormat, editPolicyHeading, editPolicyInstruction, feedbackInstruction, feedbackTitle, feedbackBtnLabel, resourcesHeading, resetHeading, resetInstruction, resetBtnLabel 20 | var content = 21 | `
22 |
23 |

Get policy

24 |
25 |

Download pre-formatted versions of your organizational security policy documents in plaintext, markdown and HTML.

26 |
`+ txt + md + html + `
27 |
28 |
29 |
30 |

Edit policy

31 |
32 |

Edit your policy below for copying and pasting into your own file.

33 | 34 | 37 | `+editTxt+` 38 |
39 |
40 |
41 |

What did you think?

42 |
43 |

If you have a few minutes, it would be great to hear your thoughts on SOAP so it can be improved in the future. SOAP doesn't use analytics to follow you around the site, so this is the best way to share your opinion.

44 | Sure, I'll complete a quick survey! 45 |
46 |
47 |
48 |

Learn more

` 49 | +links+`
50 |
51 |
52 |

Start over

53 |
54 |

All done? You'll need to reload the page before building another policy.

55 | 58 |
59 |
60 |
`; 61 | return content; 62 | }; 63 | -------------------------------------------------------------------------------- /assets/js/keyboard.js: -------------------------------------------------------------------------------- 1 | // user presses a key 2 | document.addEventListener('keydown', reactToPress); 3 | 4 | function reactToPress(e){ 5 | var keyNavArr = ['KeyS', 'KeyE', 'KeyP', 'Enter']; 6 | 7 | // is key on the list - stop if no 8 | if (keyNavArr.includes(e.code)){ 9 | // is key being pressed continuously - stop if yes 10 | if (onRepeat(e)){ 11 | return false; 12 | } else { 13 | // get active element 14 | var focusEl = document.activeElement; 15 | simulateClick(focusEl, e, keyNavArr); 16 | } 17 | } else { 18 | return false; 19 | } 20 | } 21 | 22 | function simulateClick(el, key, keys){ 23 | switch (key.code) { 24 | case keys[0]: // select 25 | if (el.type === "radio" || el.type === "checkbox"){ // focus is on a selectable thing 26 | // if preview is open, do nothing, else select the thing 27 | isPreviewOpen() ? false : el.click(); 28 | } else { 29 | return false; 30 | } 31 | break; 32 | case keys[1]: // edit 33 | if (isThisText(el) === false){ // if it's not text entry 34 | if (isPreviewOpen() === false){ // if the preview isn't open 35 | document.getElementById('editBtn').classList.contains('disabled') ? false : document.getElementById('editBtn').click(); 36 | } else { // else if the preview is open 37 | return false; 38 | } 39 | } else { // else if it's a text entry, do nothing 40 | return false; 41 | } 42 | break; 43 | case keys[2]: // preview 44 | if (isThisText(el) === false){ // if it's not a text entry 45 | if (document.getElementById('previewPolicy') !== null){ // if preview policy button exists 46 | document.querySelector("#previewPolicy").disabled ? false : document.querySelector("#previewPolicy").click(); 47 | } else { // else if the button isn't on the page, do nothing 48 | return false; 49 | } 50 | } else { // else if it's a text entry, do nothing 51 | return false; 52 | } 53 | break; 54 | case keys[3]: // next 55 | if (isThisText(el) === false){ // if it's not a text entry 56 | if (isPreviewOpen() === false){ // if the preview isn't open 57 | if (document.getElementById("submitAnswers").disabled === false){ // if the next button is enabled 58 | document.getElementById('submitAnswers').click(); 59 | document.querySelector('.current summary') ? document.querySelector('.current summary').focus() : document.querySelector('#logo'); 60 | key.preventDefault(); 61 | } else { // if the next button is disabled, do nothing 62 | return false; 63 | } 64 | } else { // if the preview is open 65 | return false; 66 | } 67 | } else if (el.type === "textarea") { // if it's a textbox 68 | el.value += '\n'; 69 | key.preventDefault(); 70 | } else { 71 | key.preventDefault(); 72 | } 73 | break; 74 | default: 75 | return false; 76 | } 77 | 78 | } 79 | 80 | function isPreviewOpen(){ 81 | pre = document.getElementById('preview'); 82 | // if modal doesn't exist, or does exist and is closed 83 | if (!pre || pre.classList.contains('closed')){ 84 | return false; 85 | } else { // else if modal is open 86 | return true; 87 | } 88 | } 89 | 90 | function isThisText(el){ 91 | if (el.type == "textarea" || el.type == "text" || el.isContentEditable){ 92 | return true; 93 | } else { 94 | return false; 95 | } 96 | } 97 | 98 | function onRepeat(e){ 99 | if (e.repeat){ 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SOAP - the tool for building security policies 2 | ============================== 3 | 4 | ## SYNOPSIS 5 | 6 | SOAP is a free, online security policy generator. The acronym stands for Securing Organizations with Automated Policymaking—reflecting the need to "clean up" standard organizational security policies, which all-too-often are unread, irrelevant, or ignored. Thanks to SOAP, organizations can now customize their own unique security policies. Say goodbye to the one-size-fits-all approaches of the past! And along the way, SOAP provides support and implementation tips to ensure the entire process is as easy as possible. 7 | 8 | SOAP was researched, designed, and built by Gem Barrett, with funding from the Open Technology Fund. The need for a program like SOAP became apparent through their work in online rapid response coordination. Many organizations need to meet a baseline level of security but for various reasons are prevented from having a policy that works for them. SOAP seeks to change that. 9 | 10 | In developing SOAP, research was conducted to understand the frustrations faced and obstacles encountered by organizations when creating, implementing, and maintaining security policies. The findings of this research and accompanying interviews are shared in the report [“Securing Civil Society”](https://usesoap.app/assets/SecuringCivilSociety-report.pdf) (PDF currently available in English, other versions coming soon). These findings, coupled with research into organizational security best practices, helped create the methodology that forms the basis of SOAP’s innovative process. 11 | 12 | ## Run SOAP locally (quick demo only) 13 | 14 | 1. python -m server.py 15 | 1. open browser to: http://localhost:8000/ 16 | 17 | ## Run your own SOAP 18 | 19 | You can run SOAP on any web server that can fallback to index.html on 404 not found. SOAP is a single-page JavaScript application, and needs this fallback to support navigation with the History API. 20 | 21 | To minimize the surface area for attack we recommend hosting your own SOAP instance on a fully-managed static hosting service. If you want to host SOAP on your own hardware/OS we recommend following all reasonable precautions in securing a public web server, including modern TLS profiles. Services such as Amazon S3 or GitHub Pages are options for self-hosting. 22 | 23 | ## FOLDER STRUCTURE 24 | 25 | * **index.html**: sits at the root and is the place where all the JavaScript is injected 26 | * **content**: holds all the text content for the site in JSON format 27 | * **assets**: contains CSS files, images, report PDFs and JavaScript files 28 | * **assets/js**: holds several files containing various feature-specific functions 29 | * **assets/js/views**: files here direct the controller files to pull the data into the templates and serve it up for display. For example, views/questionsView.js calls on controllers/questionPage.js to get all the question data and plug it into templates/questionsTemplate.js 30 | * **assets/js/controllers**: these files are instructed by their counterparts in the views folder to get all the data needed for that section and parcel it up ready for the corresponding template 31 | * **assets/js/templates**: these files take the data provided and plug it into the appropriate HTML elements for serving back to the controller, who then renders it in the browser 32 | 33 | 34 | 35 | ## SUPPORT 36 | 37 | Thanks for asking! It’s true: SOAP takes a lot of time (and coffee!) to maintain. So if you’re interested in giving back and supporting the project, here are a few options: 38 | * **Donations** to fund SOAP’s maintenance can be made at 39 | [ko-fi.com/supportsoap](https://ko-fi.com/supportsoap) 40 | * **Fluent in a language other than English?** Email feedback(at)usesoap.app with the language(s) you can help with and we'll let you know how you can get started with translating SOAP to make it more accessible to non-English speakers 41 | * **SOAP is an open-source project**. Contributions to the code (JavaScript, HTML or CSS) or documentation are always welcomed on the [issues page](https://github.com/gembarrett/soap/issues) 42 | * **Just want to say hi or ask a question?** Have an idea for how SOAP could be improved? Email us at: feedback(at)usesoap.app. 43 | 44 | ## BUGS 45 | 46 | When you find issues, please report them via the following methods - be sure to include any output from the browser console if possible: 47 | 48 | * **[GitHub Issues](https://github.com/gembarrett/soap/issues)**: there are templates set up to help you provide all the information needed 49 | * **Email**: feedback at usesoap.app 50 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SOAP: Securing Organizations with Automated Policymaking 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 46 |
47 | 54 |
55 | 69 |
70 |
Spinning circle - the page is loading

LOADING

71 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /assets/js/templates/questionsTemplate.js: -------------------------------------------------------------------------------- 1 | templates.questionsTemplate = function(data, params){ 2 | // if there's a snapshot url then build the currentState array 3 | if (params){ 4 | // get the answer groups 5 | var snapQs = params[0].split("_"); 6 | var count = 0; 7 | // for each answer group 8 | for (var s = 0; s < snapQs.length; s++){ 9 | snapQ = snapQs[s].split("-")[0]; 10 | snapA = snapQs[s].split("-")[1]; 11 | // if there's multiple answers for this question 12 | if (snapA.length > 1){ 13 | snapA = snapA.split(""); 14 | for (var a = 0; a < snapA.length; a++){ 15 | // get each answer and add it to the currentState array 16 | currentState.answers[count] = { 17 | q:snapQ, 18 | a:snapA[a] 19 | } 20 | count++; 21 | } 22 | } else { 23 | // if there's just one (radio) then add that answer 24 | currentState.answers[count] = { 25 | q:snapQ, 26 | a:snapA 27 | } 28 | count++; 29 | 30 | } 31 | } 32 | } 33 | // build the page elements 34 | var content = ` 35 |
`; 36 | 37 | for(var i = 0; i < questionsList.length; i++) { 38 | var question = data[i]; 39 | 40 | // create the start of the form 41 | content += `
' : ' current">'); 42 | // if it's a question 43 | if (question.isQ) { 44 | var panel = ""; 45 | if (question.tips[0].relevance) { 46 | panel += '

' + question.tips[0].relevance + '

'; 47 | } 48 | if (question.tips[1].meaning) { 49 | panel += '

' + question.tips[1].meaning + '

'; 50 | } 51 | if (question.tips[2].implementation) { 52 | panel += '

' + question.tips[2].implementation + '

'; 53 | } 54 | if (question.tips[3].more) { 55 | panel += '

| '; 56 | for (var s = 0; s < question.tips[3].more.length; s++){ 57 | more = encodeURIComponent(question.tips[3].more[s]); 58 | panel += ''+question.tips[3].more[s]+' | '; 59 | } 60 | panel += '

'; 61 | } 62 | // add the question 63 | content += '

' + question.q + '

'; 64 | // if the panel content exists add it 65 | content += panel !== "" ? panel+'
' : '
'; 66 | 67 | content += '
'; 68 | 69 | // add the answers 70 | for (var j = 0; j < question.answers.length; j++){ 71 | // premake the id and name 72 | thisID = 'id="' +question.id+ "-"+ j+ '-answer"'; 73 | thisName = 'name="' +question.id+ '-el"'; 74 | required = question.required ? "required" : ""; 75 | // if there's a placeholder then grab it 76 | if (question.answers[j].placeholder) { 77 | thisPlaceholder = 'placeholder="' + question.answers[j].placeholder + '"'; 78 | } 79 | // if this is an input field then create the label 80 | if (question.answers[j].type !== 'textarea') { 81 | thisLabel = ''; 88 | } 89 | // start the form 90 | content += '
'; 91 | 92 | // if there's a textarea 93 | if (question.answers[j].type === 'textarea') { 94 | content += ''; 95 | } 96 | 97 | // if there's a textbox 98 | else if (question.answers[j].type === 'text') { 99 | content += thisLabel + ''; 100 | } 101 | 102 | // if there's a radio or checkbox, check if there's an answer already that can be prepopulated 103 | else { 104 | // find a match for this question and answer object 105 | var qMatch = currentState.answers.filter(o => (o.q === question.id.split('q')[1] && o.a === String(j))); 106 | 107 | // if there's a previous answer for this 108 | if ((qMatch !== 'undefined' && qMatch.length > 0)){ 109 | // add a checked element 110 | content += '' + thisLabel; 111 | } else { 112 | // otherwise make an unchecked one 113 | content += '' + thisLabel; 114 | } 115 | } 116 | // end the form 117 | content += '
'; 118 | } 119 | } else if (question.id === "q0") { 120 | // if it's the first question, lay out the content a bit differently 121 | for (var k = 0; k < question.contentArray.length; k++){ 122 | content += `

`+question.contentArray[k].title+`

`+question.contentArray[k].text+`

`; 123 | } 124 | } else if (question.id === ("q"+(questionsList.length-1))){ 125 | // call a function to deal with this 126 | content += buildSubPolicies(question); 127 | } else { 128 | // add the title and paragraphs 129 | content += '

' + question.title + '

'; 130 | content = formatArray(question.contentArray, content); 131 | } 132 | // if its the first question 133 | // no closing div, just closing form 134 | content += question.isQ ? '
' : ''; 135 | } 136 | pause = '
'; 137 | content += '
'+pause+'
'; 138 | return content; 139 | }; 140 | -------------------------------------------------------------------------------- /assets/js/subpolicies.js: -------------------------------------------------------------------------------- 1 | function buildSubPolicies(data){ 2 | layout = `
3 |

`+data.q+`

`; 4 | // add each of the description paragraphs 5 | for (var p = 0; p < data.desc.length; p++){ 6 | layout += `

`+data.desc[p]+`

`; 7 | } 8 | layout += `
`; 9 | // add each of the default teams 10 | // TODO: change this from e to avoid confusion 11 | for (var e = 0; e < data.teams.length; e++){ 12 | layout += `
`; 13 | layout += e === 0 ? `` : ``; 14 | layout += `
`; 15 | } 16 | layout += `
`; 17 | // add all of the content areas 18 | for (var t = 0; t < data.areas.length; t++){ 19 | layout += `
20 | 21 |
`; 22 | } 23 | // close the columns and add the summary 24 | layout += `
25 |
26 | Your policy package: 27 |
`; 30 | 31 | // return the complete page 32 | return layout; 33 | } 34 | 35 | function addTeam(e){ 36 | // get the id of this last question 37 | lastQid = e.target.id.split('-')[0]; 38 | // count the number of input-label pairs in the teams column 39 | allEls = document.querySelectorAll('#teams input'); 40 | // if it won't exceed the maximum 41 | if (allEls.length < 10){ 42 | // build another team element 43 | thingToAdd = `
44 | 45 | 46 |
`; 47 | // then add it to the screen 48 | e.target.insertAdjacentHTML('beforebegin', thingToAdd); 49 | // add this team to teamContent 50 | teamContent.push({ 51 | "tId" : lastQid+`-0`+allEls.length+`-answer`, 52 | "name": "New team", 53 | "areas": [], 54 | "output": "" 55 | }); 56 | 57 | // store the currently selected things 58 | // get the currently selected team 59 | // find it in the array 60 | // compare the stored areas with these selections 61 | // if they're the same, do nothing 62 | // else replace the stored values with the selections 63 | 64 | // then clear all those selections 65 | // and select the thingToAdd team 66 | 67 | // don't do any other buttony stuff 68 | e.preventDefault(); 69 | } else { 70 | // if it's more than 10, do nothing (maybe show error text) 71 | // don't do any other buttony stuff 72 | e.preventDefault(); 73 | } 74 | 75 | } 76 | 77 | // TODO change the data references to something based on what can be grabbed from the e values 78 | function updateTeams(e){ 79 | // use teamContent to store the pairs as array of objects 80 | var areaTitles = ["device", "communications", "accounts", "travel", "environmental", "network"]; 81 | 82 | // if user clicked a team and its now checked 83 | if ((e.target.type === "radio") && (e.target.checked)){ 84 | 85 | // clear selections and load the team-specific ones 86 | // get all the areas 87 | allAreas = document.querySelectorAll('#policyAreas input'); 88 | // for each area 89 | for (var aa = 0; aa < allAreas.length; aa++){ 90 | // get the position number of that input 91 | thisEl = allAreas[aa].id.split('-')[1].split('')[1]; 92 | // and the position number of that team 93 | thisT = e.target.id.split('-')[1].split('')[1]; 94 | // if it's in the areas array then check it 95 | if (teamContent[parseInt(thisT)].areas.includes(parseInt(thisEl))){ 96 | allAreas[aa].checked = true; 97 | } else { 98 | // otherwise deselect 99 | allAreas[aa].checked = false; 100 | } 101 | } 102 | // compare label to what's stored and replace if necessary 103 | // TODO: pick up the name on blur 104 | teamContent[parseInt(thisT)].name = e.target.labels[0].textContent !== teamContent[parseInt(thisT)].name ? e.target.labels[0].textContent : teamContent[parseInt(thisT)].name; 105 | 106 | } else if (e.target.type === "checkbox"){ // if user clicked a content area 107 | // which team is selected 108 | thisTeam = document.querySelector('#teams input:checked'); // returns a radio button 109 | // get all selected areas 110 | theseAreas = document.querySelectorAll('#policyAreas input:checked'); // returns multiple checkboxes 111 | areaRefs = []; 112 | 113 | // for each of the elements, get the id, then split it down to single figure 114 | for (var ta = 0; ta < theseAreas.length; ta++){ 115 | areaRefs[ta] = parseInt(theseAreas[ta].id.split('-')[1].split('')[1]); 116 | } 117 | 118 | // for each of the stored teams 119 | for (var tc = 0; tc < teamContent.length; tc++){ 120 | 121 | // check the id against currently selected team 122 | if (teamContent[tc].tId === thisTeam.id){ 123 | // update the stored name if needs be 124 | teamContent[tc].name = thisTeam.labels[0].textContent; 125 | // update the stored values if needed 126 | teamContent[tc].areas = areaRefs; 127 | teamContent[tc].output = teamContent[tc].name +" policy (inc. "; 128 | 129 | // concatenate the area titles 130 | for (var at=0; at 0)){ 138 | // if its the last area title being added, then include appropriate ending 139 | teamContent[tc].output += "and " + areaTitles[teamContent[tc].areas[at]]; 140 | } 141 | else { 142 | // otherwise just separate the areas with a comma 143 | teamContent[tc].output += areaTitles[teamContent[tc].areas[at]] + ", "; 144 | } 145 | } 146 | 147 | teamContent[tc].output += " security)"; 148 | // update the summary text 149 | updateSummary(teamContent[tc]); 150 | // then break the loop 151 | tc = teamContent.length; 152 | 153 | } else { 154 | // if it isn't a match then do nothing 155 | console.log('Not a match'); 156 | } 157 | } 158 | } else { 159 | console.log(e.target); 160 | } 161 | } 162 | 163 | function updateSummary(el){ 164 | // we are given a teamContent element 165 | // in the summary, is there an li with a matching id 166 | list = document.querySelectorAll('#expectedOutput ul li'); 167 | itemID = el.tId+'-output'; 168 | var found = false; 169 | // if there is a matching one 170 | for (var li = 0; li'+el.output+''; 190 | // add new summary text to the top of the list 191 | list[0].insertAdjacentHTML('beforebegin', item); 192 | } 193 | } 194 | 195 | 196 | function setUpTeamContent(){ 197 | // get all the default teams on the page 198 | teams = document.querySelectorAll('#teams input'); 199 | // for each of those defaults 200 | for (var a = 0; a < teams.length; a++){ 201 | // store the id in the teamContent 202 | teamContent.push({ 203 | "tId" : teams[a].id, 204 | "name" : "", 205 | "areas" : [], 206 | "output": "" 207 | }); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /content/rest-of-site.js: -------------------------------------------------------------------------------- 1 | var ros = [ 2 | { 3 | "head":"The free and simple tool that enables civil society organizations to build better security policies.", 4 | "subhead":"To get started, enter your organization's name (or an alias):", 5 | "what":[ 6 | { 7 | "head":"What is SOAP?", 8 | "subhead":"SOAP is a free, online security policy generator. It helps you to:", 9 | "list": [ 10 | { 11 | "type": "ul", 12 | "content": ["Design customized security policies for your organization","Make informed decisions about your organization’s security plan","Successfully implement and maintain your organization’s security policy"], 13 | } 14 | ], 15 | "more": ["The acronym stands for Securing Organizations with Automated Policymaking—reflecting the need to \"clean up\" standard organizational security policies, which all-too-often are unread, irrelevant, or ignored. Thanks to SOAP, organizations can now customize their own unique security policies. Say goodbye to the one-size-fits-all approaches of the past! And along the way, SOAP provides support and implementation tips to ensure the entire process is as easy as possible."], 16 | "links":[] 17 | } 18 | ], 19 | "who":[ 20 | { 21 | "head":"Who should use SOAP?", 22 | "subhead":"Almost any organization that needs or uses a security policy! In particular, SOAP is geared towards helping civil society organizations (“CSOs”) and other mission-driven groups increase their digital resilience by building an organizational security policy that works for their specific situation.", 23 | "list":"", 24 | "more":["Research indicates nearly half of all CSOs don’t have a security policy. These organizations instead rely on informal guidelines and “trust” to manage their security. In today’s world, that’s not enough. Although such flexibility allows organizations to adapt quickly to new threats, it makes it much harder to ensure workers are operating under the same security standard. And new members are often left unprepared for even the most basic security incidents. SOAP provides a set of security rules that everyone can work with—all totally customizable to your organization’s specific needs.","So whether your organization needs to update an existing policy, create an entirely new one, or is just preparing to work with a security consultant, SOAP can help!"], 25 | "links":[] 26 | } 27 | ], 28 | "how":[ 29 | { 30 | "head":"How does SOAP work?", 31 | "subhead":"", 32 | "list":[ 33 | { 34 | "type":"ol", 35 | "content":["SOAP asks questions about your organizational security practices.","Your answers help SOAP to build a policy","Your policy is presented to you at the end of the process"] 36 | } 37 | ], 38 | "more":["You can preview your policy at any point, and each question is accompanied by information to help you fully understand, research, and implement the associated practices. SOAP’s unique process focuses on key areas commonly included in policies, like devices or travel security.","Each section is preceded by a hypothetical—but common—security incident to help orient the questions within the context of a realistic threat."], 39 | "links":[] 40 | } 41 | ], 42 | "security":[ 43 | { 44 | "head":"How secure and private is it?", 45 | "subhead":"", 46 | "list":[ 47 | { 48 | "type":"ol", 49 | "content":["Neither your answers nor any identifying data are stored or sent anywhere","SOAP relies on JavaScript to work, which may be relevant to your threat model","The code is open-source and available at github.com/gembarrett/soap"], 50 | } 51 | ], 52 | "more":["SOAP is a web app that works solely in your browser: no cookies or server-side processing is involved in building your policy. No IP addresses, web logs, or any text entered into the answer boxes is stored or sent back to the SOAP servers. A report on SOAP's security (alpha version - will be updated soon) was carried out by SubGraph as part of OTF's Red Team program, and verified that \"there are no meaningful attack vectors that could introduce substantial risk with the use of SOAP.\"","Like many websites, SOAP relies on JavaScript to achieve its functionality, so it must be enabled in your browser for SOAP to work (hint: if you’re reading this, it’s already enabled). If your threat model means you can’t turn on JavaScript, you can download the code (github.com/gembarrett/soap) to run the tool in your own secure environment. Questions? Concerns? Email feedback@usesoap.app"], 53 | "links":[] 54 | } 55 | ], 56 | "background":[ 57 | { 58 | "head":"What's behind the SOAP project?", 59 | "subhead":"SOAP was researched, designed, and built by Gem Barrett, with funding from the Open Technology Fund. In developing SOAP, research was conducted to understand the frustrations faced and obstacles encountered by organizations when creating, implementing, and maintaining security policies.", 60 | "list":"", 61 | "more":["The findings of this research and accompanying interviews are shared in the report “Securing Civil Society” (PDF currently available in English, other versions coming soon). These findings, coupled with research into organizational security best practices, helped create the methodology that forms the basis of SOAP’s innovative process. For updates on SOAP's future development, follow @thatSOAPapp on Twitter."], 62 | "links":[ 63 | { 64 | "name":"SAFETAG by Internews", 65 | "url":"https://safetag.org" 66 | }, 67 | { 68 | "name": "Umbrella by Security First", 69 | "url": "https://secfirst.org/umbrella" 70 | }, 71 | { 72 | "name": "LevelUp by Engine Room", 73 | "url":"https://www.level-up.cc" 74 | }, 75 | { 76 | "name":"Security Planner by Consumer Reports", 77 | "url":"https://securityplanner.org" 78 | } 79 | ] 80 | } 81 | ], 82 | "support":[ 83 | { 84 | "head":"Can I help support this project?", 85 | "subhead":"Thanks for asking! It’s true: SOAP takes a lot of time (and coffee!) to maintain. So if you’re interested in giving back and supporting the project, here are a few options:", 86 | "list":[ 87 | { 88 | "type":"ul", 89 | "content":["Donations to fund SOAP’s upkeep can be made at ko-fi.com/supportsoap","Fluent in a language other than English? Email feedback(at)usesoap.app with the languages you can translate to and we'll let you know how you can help translate SOAP and make it more accessible to non-English speakers","SOAP is an open-source project. Contributions to the code (JavaScript, HTML or CSS) or documentation are always welcomed at github.com/gembarrett/soap"] 90 | } 91 | ], 92 | "more":["Just want to say hi or ask a question? Have an idea for how SOAP could be improved? Email us at: feedback(at)usesoap.app."] 93 | } 94 | ], 95 | "policyPage":{ 96 | "resourcesHeading": "Learn more", 97 | "resourcesIntro": "For more information on organizational and personal security, and how you can get the most out of your new policy, check out these resources:", 98 | "resourceBtnLabel": "Get more info about", 99 | "textPolicyLabel": "Text", 100 | "editPolicyLabel": "Download this policy text", 101 | "editPolicyHeading":"Edit policy", 102 | "editPolicyInstruction":"Edit your policy below for copying and pasting into your own file.", 103 | "markdownPolicyLabel": "Markdown", 104 | "htmlPolicyLabel":"HTML", 105 | "policyHeading": "Get policy", 106 | "policyPreFormat": "Download pre-formatted versions of your organizational security policy documents in plaintext, markdown and HTML.", 107 | "feedbackHeading": "What did you think?", 108 | "feedbackInstruction": "If you have a few minutes, it would be great to hear your thoughts on SOAP so it can be improved in the future. SOAP doesn't use analytics to follow you around the site, so this is the best way to share your opinion.", 109 | "feedbackTitle": "Click to take a short survey about SOAP", 110 | "feedbackBtnLabel": "Sure, I'll complete a quick survey!", 111 | "resetHeading":"Start over", 112 | "resetInstruction":"All done? You'll need to reload the page before building another policy.", 113 | "resetBtnLabel":"Build another policy" 114 | } 115 | }, 116 | ] 117 | -------------------------------------------------------------------------------- /assets/js/start.js: -------------------------------------------------------------------------------- 1 | function addChangeListeners() { 2 | var notice = document.querySelectorAll('.q0-only'); 3 | for (var n = 0; n input')); 17 | var boxes = Array.from(document.querySelectorAll('.form-el > textarea')); 18 | elements = elements.concat(boxes); 19 | // TODO: add these event listeners back in when required questions are reimplemented 20 | // for (var e = 0; e < elements.length; e++) { 21 | // // if it's a radio or checkbox 22 | // if ((elements[e].type === "radio") || (elements[e].type === "checkbox")){ 23 | // elements[e].addEventListener('change', toggleSkip); 24 | // } else { 25 | // elements[e].oninput = toggleSkip; 26 | // } 27 | // } 28 | } 29 | 30 | 31 | function updateValue(e) { 32 | log.textContent = e.target.value; 33 | } 34 | 35 | function toggleSkip(e){ 36 | var button = document.getElementById('submitAnswers'); 37 | // if the box contains text and the button is currently "Skip" 38 | if ((e.data !== null) && (button.innerText === "Skip")){ 39 | // change button text 40 | button.innerText = "Next"; 41 | button.disabled = false; 42 | } else if ((e.data === null) && (button.innerText === "Next")){ 43 | button.innerText = "Skip"; 44 | button.disabled = true; 45 | } 46 | } 47 | 48 | function moveForward(id) { 49 | // this increases the counter 50 | currentState.questionC++; 51 | // start looking at the next question 52 | // increase the question id number 53 | id++; 54 | currentState.questionQ = 'q' + id; 55 | // increase position in the array 56 | currentState.questionP++; 57 | var el = document.querySelector('progress'); 58 | el.value++; 59 | return id; 60 | } 61 | 62 | 63 | function isExcludedQ(id) { 64 | // start looking at the next question 65 | id = moveForward(id); 66 | // for each of the questions remaining 67 | for (var q = id; q < questionsList.length; q++) { 68 | // if the question isn't on the list 69 | if (currentState.exclusions.indexOf(parseInt(id)) === -1) { 70 | // if the question is not excluded 71 | // break the loop 72 | console.log('not excluded: '+id); 73 | // console.log('Go to next question.'); 74 | } else { 75 | // update everything to the next question 76 | id = moveForward(id); 77 | } 78 | } 79 | } 80 | 81 | // this updates the progress bar 82 | function updateProgressBar(){ 83 | var el = document.querySelector('progress'); 84 | el.value++; 85 | } 86 | 87 | function toggleSpinner(){ 88 | document.getElementById('spinner').classList.toggle('loading'); 89 | } 90 | 91 | // if prev button is disabled then call this 92 | function enablePreview(p){ 93 | // if the section is 0 94 | if (currentState.sectionC === 0){ 95 | // if there's text AND button answers 96 | if ((currentState.answers.length !== 0) && (Object.values(dict).length !== 0)){ 97 | p.removeAttribute('disabled'); 98 | } else { 99 | console.log('More answers needed for preview'); 100 | } 101 | } else { 102 | // if we're past the contextual section 103 | // and there are text OR button answers 104 | if ((currentState.answers.length !== 0) || (Object.values(dict).length !== 0)){ 105 | p.removeAttribute('disabled'); 106 | } else { 107 | console.log('More answers needed for preview'); 108 | } 109 | } 110 | } 111 | 112 | // if snapshot button is disabled then call this 113 | function enableSnapshot(s){ 114 | // if there's answers stored 115 | if (currentState.answers.length !== 0){ 116 | // check the ids against the checkableQs 117 | for (count = 0; count< currentState.answers.length; count++){ 118 | if (isCheckableQ(parseInt(currentState.answers[count].q))){ 119 | // enable the snapshot button 120 | s.removeAttribute('disabled'); 121 | s.addEventListener('click', getSnapshotURL); 122 | document.querySelector('#copyBtn').addEventListener('click', copyUrl); 123 | return; 124 | } else { 125 | console.log(currentState.answers[count].q + ' is not a question that can be snapshotted'); 126 | } 127 | } 128 | } else { 129 | console.log('More answers needed for snapshotting'); 130 | } 131 | } 132 | 133 | // this is the function that's called when a user submits an answer 134 | function handleSubmit() { 135 | toggleSpinner(); 136 | // search for the currently shown element - question and answer 137 | var match = document.querySelector('.current'); 138 | // this gets the current question id number e.g. q0 139 | var id = currentState.questionQ.split('q')[1]; 140 | // currently lets everything through, will change when required Qs are back 141 | canProceed = true; 142 | // before doing anything else, check if this is a required question 143 | // isRequired = match[0] ? match[0].required : false; 144 | // compare the size of answers array to find out if answers have been provided for this question 145 | // if (id > 0){ 146 | // if it's required and there are no answers provided 147 | // if (isRequired && noAnswers){ 148 | // canProceed = false; 149 | // } 150 | // } 151 | 152 | if (canProceed){ 153 | 154 | setUpPage(id); 155 | 156 | // if we're past the intro 157 | if (parseInt(id) > 0){ 158 | // show the edit button 159 | document.getElementById('editBtn').classList.remove('disabled'); 160 | // mark the current question as editable 161 | match.classList.add("editable"); 162 | collectAnswers(false); 163 | 164 | // show the preview button if answers are available 165 | prev = document.querySelector('#previewPolicy'); 166 | snapshotBtn = document.querySelector('#snapshotPolicy'); 167 | 168 | if (prev.disabled){ 169 | enablePreview(prev); 170 | } 171 | if (snapshotBtn.disabled){ 172 | enableSnapshot(snapshotBtn); // enable the snapshot button 173 | } 174 | } 175 | 176 | // if we're at the last question 177 | if(parseInt(id) === questionsList.length-1){ 178 | // disable the edit button 179 | document.getElementById('editBtn').classList.add('disabled'); 180 | } else { 181 | // collect the exclusions for this question 182 | collectExclusions(id); 183 | } 184 | // this hides the current question, 185 | match.classList.remove("current"); 186 | // is the next question excluded 187 | // go to next question 188 | id = isExcludedQ(id); 189 | // TODO change to Skip when skip/next is working 190 | document.getElementById('submitAnswers').innerText = "Next"; 191 | nextQuestion(); 192 | window.scrollTo(0,0); 193 | } 194 | toggleSpinner(); 195 | } 196 | 197 | 198 | // not sure this needs to be a function as it's only done once 199 | function setUpPage(id){ 200 | // add the additional stuff after everything else has loaded 201 | if (parseInt(id) === 0) { 202 | injectOverlay(); 203 | // sneaking this in here so it's done when textboxes exist 204 | resizingBoxes(); 205 | addChangeListeners(); 206 | setUpTeamContent(); 207 | checkForName(); 208 | } 209 | } 210 | 211 | 212 | function nextQuestion(){ 213 | // reset the snapshot visibility 214 | document.querySelector('#snapshotGroup').classList.add('hidden'); 215 | // if there's more questions left in this section 216 | if (currentState.questionP < currentState.sectionQ.length) { 217 | // grab the next question's element and add class of current 218 | var nextQ = document.getElementById(currentState.questionQ); 219 | nextQ.classList.add("current"); 220 | } 221 | // if there's more sections left 222 | // consider whether this should happen here, before the last q, or after it 223 | else if (currentState.sectionC < sections.length-1) { 224 | // increase the section counter 225 | currentState.sectionC++; 226 | // reset the position counter 227 | currentState.questionP = 0; 228 | // get the next section 229 | currentState.sectionQ = sections[currentState.sectionC]; 230 | // as before, grab the next question's element and add class of current 231 | var nextQ = document.getElementById(currentState.questionQ); 232 | nextQ.classList.add("current"); 233 | } 234 | // if we're out of questions and sections then show the policy 235 | else { 236 | // if there are answers 237 | if (currentState.answers.length > 0) { 238 | var policyContainer = templates.policyTemplate(); 239 | utils.render('page', policyContainer); 240 | } else { 241 | if (window.confirm("Oh no! It seems you haven't answered enough questions to build a policy. Start again?")) { 242 | location.reload(false); 243 | } else { 244 | window.location.href = "/"; 245 | } 246 | } 247 | } 248 | // if (nextQ){ 249 | // // if there's a next question and it's required 250 | // if (nextQ[0] && nextQ[0].required){ 251 | // // get and set the submit button to disabled 252 | // submit = document.querySelector('#submitAnswers'); 253 | // submit.setAttribute("disabled", ""); 254 | // } 255 | // } 256 | } 257 | 258 | // function to add formatting to array 259 | function formatArray(arr, storage) { 260 | if (Array.isArray([arr])) { 261 | for (var i=0; i'; 263 | } 264 | return storage; 265 | } 266 | } 267 | 268 | function stripCode(t){ 269 | t = t.replace(//g, ">"); 270 | return t; 271 | } 272 | -------------------------------------------------------------------------------- /assets/css/media.css: -------------------------------------------------------------------------------- 1 | @media (min-width:600px) { 2 | #head a { 3 | width: auto; 4 | } 5 | #menu-check:checked ~ ul { 6 | align-items: center; 7 | } 8 | #head img { 9 | max-width: 47%; 10 | } 11 | #head { 12 | justify-content: space-between; 13 | } 14 | #expand { 15 | display: none; 16 | } 17 | #menu-check:checked ~ #collapse { 18 | display:none; 19 | } 20 | #policy-edit p{ 21 | display:none; 22 | } 23 | .btn-wrap.wrap-r { 24 | padding-left: 0; 25 | } 26 | #snapshotPolicy { 27 | margin-left: 0; 28 | } 29 | .btn-wrap.wrap-r > div:last-child { 30 | flex: 1 1 60%; 31 | text-align: right; 32 | } 33 | } 34 | 35 | @media (min-width:600px) and (max-width:1399px) { 36 | .feature { 37 | flex: 1 2 30%; 38 | } 39 | #overview section { 40 | flex: 1 2 40%; 41 | margin: 1.5em 2em; 42 | } 43 | #overview { 44 | flex-wrap: wrap; 45 | } 46 | .main-content > div { 47 | padding:2em; 48 | } 49 | .text-buttons-wrap { 50 | margin: 0; 51 | max-width: initial; 52 | } 53 | #img-intro { 54 | width: 100%; 55 | margin: 0; 56 | } 57 | #intro .text-buttons-wrap { 58 | flex: 1 2 40%; 59 | } 60 | #intro .text-img-wrap { 61 | margin-bottom: 1.5em; 62 | align-items: flex-start;; 63 | } 64 | #bgContainer section img { 65 | max-width: 50%; 66 | margin: 0; 67 | padding: 0; 68 | } 69 | .nav-start.trio { 70 | flex-direction: column; 71 | } 72 | #bgContainer section.dbl img { 73 | margin-right: 4em; 74 | } 75 | #bgContainer .nav-start.dbl.reverse { 76 | align-items: flex-start; 77 | } 78 | .grid h4 { 79 | text-align: center; 80 | } 81 | #q0 section { 82 | flex:1 0 40%; 83 | } 84 | .trioPics { 85 | float: 1 1 30%; 86 | margin: 1em; 87 | } 88 | } 89 | 90 | @media (max-width:1399px) { 91 | .desktopVis { 92 | display:none; 93 | } 94 | .mobileVis{ 95 | display: block; 96 | } 97 | } 98 | 99 | @media (min-width: 900px) { 100 | #feedback{ 101 | height:50%; 102 | max-width: 800px; 103 | min-height: 300px; 104 | } 105 | } 106 | 107 | @media (min-width:1400px){ 108 | .desktopVis { 109 | display:block; 110 | } 111 | .mobileVis{ 112 | display: none; 113 | } 114 | } 115 | 116 | @media (max-width:1024px) { 117 | #intro { 118 | padding: 4em 4em 6em; 119 | } 120 | #logoContainer { 121 | align-items: flex-start; 122 | } 123 | #intro h1 { 124 | margin-top: 0em; 125 | margin-bottom: 1.4em; 126 | } 127 | #intro .btn-wrap.wrap-c { 128 | padding-top: 0; 129 | } 130 | .window h3 { 131 | font-size: 3em; 132 | } 133 | .buildPage .window { 134 | margin-top: 1em; 135 | margin-bottom: 4em; 136 | } 137 | .buildPage .window:last-child { 138 | margin-bottom:0; 139 | } 140 | #preview h1 { 141 | line-height: 2em; 142 | font-size: 1.5em; 143 | } 144 | } 145 | 146 | @media (max-width:768px) { 147 | #logoContainer { 148 | align-items: center; 149 | } 150 | #logoContainer > div { 151 | margin-right: 2em; 152 | margin-bottom: 2em; 153 | } 154 | #logoContainer h1 { 155 | font-size: 1.5em; 156 | width:auto; 157 | } 158 | .window { 159 | flex-direction: column; 160 | } 161 | .homeSectionOverview { 162 | padding-left: 0; 163 | } 164 | .buildPage .window > :not(h3) { 165 | padding:0; 166 | } 167 | .policyHolder { 168 | margin-top: 1em; 169 | } 170 | } 171 | 172 | @media (max-width:600px) { 173 | #feedback .text-buttons-wrap { 174 | flex-direction: column; 175 | } 176 | progress { 177 | margin-top:1em; 178 | } 179 | #feedback { 180 | height: 60%; 181 | } 182 | #head label { 183 | font-size: 1.5rem; 184 | padding: 0.5em 0.5em 1em 1em; 185 | margin: 10px; 186 | margin-right:0; 187 | } 188 | .incidentBox { 189 | width:100%; 190 | min-width:unset; 191 | padding:2em 0; 192 | } 193 | .links a { 194 | padding:0; 195 | } 196 | .questionContent h1 { 197 | font-size: 1.5em; 198 | } 199 | #q0 h2 { 200 | width:100%; 201 | font-size: 1.2em; 202 | } 203 | #intro { 204 | padding: 2em 2em 5em; 205 | } 206 | #intro h2 { 207 | padding-bottom: 0; 208 | margin-bottom: 0; 209 | } 210 | .text-img-wrap { 211 | flex-direction: column-reverse; 212 | } 213 | .text-buttons-wrap { 214 | margin:0; 215 | max-width: inherit; 216 | } 217 | #img-intro { 218 | width:100%; 219 | margin:0; 220 | } 221 | #head { 222 | display:flex; 223 | justify-content: space-between; 224 | } 225 | #menu-check:checked ~ ul { 226 | flex-direction: column; 227 | text-align: center; 228 | margin:0; 229 | padding:0; 230 | align-items: end; 231 | width:unset; 232 | border-top: 0; 233 | font-size: 0.8em; 234 | } 235 | #collapse i { 236 | opacity: 0.5; 237 | } 238 | #page { 239 | padding-top: 7em; 240 | } 241 | .links li:last-child { 242 | padding: 0.1em 1em; 243 | } 244 | .links li { 245 | min-width: 90px; 246 | } 247 | .feature { 248 | width:100%; 249 | margin:1em auto; 250 | } 251 | #overview section { 252 | width:100%; 253 | padding-bottom: 4em; 254 | } 255 | .links { 256 | display: flex; 257 | flex-flow: row wrap; 258 | text-align: center; 259 | /* fallback */ 260 | justify-content: center; 261 | justify-content: space-evenly; 262 | padding-left:0; 263 | } 264 | #overview { 265 | flex-direction: column; 266 | padding: 1em; 267 | } 268 | .grid { 269 | margin-left: 0; 270 | margin-bottom: 2em; 271 | } 272 | #bgContainer .trio img { 273 | margin: 0; 274 | } 275 | #bgContainer section { 276 | padding: 2em; 277 | flex-direction: column; 278 | } 279 | .nav-start.trio { 280 | padding: 1em; 281 | } 282 | .wrap-c { 283 | flex-direction: column; 284 | } 285 | .qContainer { 286 | padding:1.5em; 287 | } 288 | #q0 section { 289 | margin:2em 0; 290 | } 291 | .question-panel { 292 | margin-top: 0.5em; 293 | } 294 | .form-el { 295 | margin: 1em; 296 | font-size: 0.9em; 297 | } 298 | .form-el label { 299 | line-height: 1em; 300 | font-size: 1.1em; 301 | } 302 | #closePreview { 303 | font-size: 1.5em; 304 | top: -1.5em; 305 | font-weight: bold; 306 | } 307 | #preview { 308 | width:90%; 309 | height:80%; 310 | } 311 | #inner h4 { 312 | border-top:1px solid; 313 | padding-top:0.5em; 314 | } 315 | #policy-edit { 316 | width: unset; 317 | } 318 | #policy-edit h3 { 319 | margin-top: 0; 320 | } 321 | .buildPage textarea { 322 | padding-left: 1em; 323 | } 324 | .buildPage text-img-wrap { 325 | margin-top:2em; 326 | } 327 | .btn.btn-prim { 328 | min-width: 10em; 329 | } 330 | .btn-wrap.wrap-c { 331 | flex-wrap: wrap; 332 | } 333 | #head img { 334 | max-width: 50%; 335 | } 336 | #menu-check:checked ~ #collapse { 337 | float:none; 338 | } 339 | .header-menu { 340 | display: flex; 341 | flex-direction: row-reverse; 342 | } 343 | #bgContainer img { 344 | width:100%; 345 | margin:0 auto; 346 | } 347 | #bgContainer section p { 348 | margin-top:0; 349 | } 350 | .nav-start a { 351 | color:#fff; 352 | font-weight: bold; 353 | } 354 | .nav-start .links a:hover, .nav-start .links a.active { 355 | border-color: #fff; 356 | } 357 | #reset { 358 | margin:1em auto; 359 | display:block; 360 | } 361 | .btn-wrap.wrap-r { 362 | justify-content: center; 363 | margin: 3em 0; 364 | } 365 | .btn-wrap.wrap-r button { 366 | min-width: initial; 367 | flex: 0 0 40%; 368 | margin-left: 0; 369 | margin-right: 0; 370 | } 371 | .buildPage .btn-wrap.wrap-r{ 372 | flex-direction: column-reverse; 373 | margin: 0; 374 | padding-left: 0; 375 | } 376 | .buildPage .btn-wrap.wrap-r div { 377 | display: flex; 378 | } 379 | .buildPage .btn-wrap.wrap-r div:first-child{ 380 | flex-direction: column; 381 | } 382 | .buildPage .btn-wrap.wrap-r div:last-child { 383 | justify-content: space-evenly; 384 | } 385 | #logoContainer { 386 | flex-direction: column; 387 | } 388 | #logoContainer > div { 389 | margin-right: 0; 390 | } 391 | #logoContainer h1 { 392 | text-align: center; 393 | } 394 | #intro .btn { 395 | margin-top:2em; 396 | } 397 | .questionContent h2 { 398 | font-size: 1em; 399 | line-height: 1.5em; 400 | } 401 | #head { 402 | padding: 5px 2em; 403 | } 404 | .links { 405 | padding-left: 0; 406 | font-size: 1em; 407 | } 408 | #head > a { 409 | flex: 0 0 30%; 410 | } 411 | .answers-container { 412 | flex-direction: column; 413 | } 414 | #closePreview .fas { 415 | font-size: 1.5em; 416 | } 417 | #preview h1 { 418 | font-size: 1.3em; 419 | } 420 | .form-el { 421 | display:flex; 422 | align-items: center; 423 | text-align: left; 424 | justify-content: space-between; 425 | margin:1em; 426 | } 427 | .form-el label { 428 | font-size: 1em; 429 | line-height: 1.5em; 430 | flex: 0 0 25%; 431 | } 432 | .form-el input[type="text"] { 433 | margin-left: 1em; 434 | flex: 1 0 auto; 435 | padding: 2em 1em; 436 | text-align: left; 437 | } 438 | #q1 .type-text label { 439 | flex: 0 0 auto; 440 | } 441 | #q1 input { 442 | text-align: center; 443 | } 444 | .type-checkbox, .type-radio { 445 | text-align: center; 446 | display: block; 447 | margin: 2em 0 0; 448 | } 449 | .modalScrollbox { 450 | padding: 1em; 451 | } 452 | #foot .links { 453 | padding: 0 1em; 454 | display: flex; 455 | justify-content: space-around; 456 | align-items: center; 457 | } 458 | .editMode { 459 | border-width: 1em; 460 | } 461 | #page { 462 | padding-top: 0; 463 | } 464 | .window h3 { 465 | font-size: 2.5em; 466 | } 467 | .form-el.type-text { 468 | flex-direction: column; 469 | margin-top: 2em; 470 | } 471 | .form-el input[type="text"] { 472 | text-align: center; 473 | margin: 0 0 1em; 474 | } 475 | .buildPage #menu li:first-child { 476 | display: none; 477 | } 478 | #head > a { 479 | flex: 0 0 auto; 480 | } 481 | #editBtn { 482 | min-height: 44px; 483 | line-height: 44px; 484 | } 485 | #snapshotGroup{ 486 | display: flex; 487 | align-items: center; 488 | justify-content: space-around; 489 | } 490 | .question-panel div { 491 | flex-direction: column; 492 | padding: 1em 0; 493 | } 494 | #teamContentCols { 495 | flex-direction: column; 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /assets/js/edit.js: -------------------------------------------------------------------------------- 1 | document.querySelectorAll('[contenteditable=true]').forEach(function (el) { 2 | el.addEventListener('keydown', function(event){ 3 | if (event.keyCode === 8) { 4 | var node = event.srcElement || event.target; 5 | if ( !isActiveFormItem(node) ) { 6 | event.preventDefault(); 7 | } 8 | } 9 | }); 10 | }); 11 | 12 | function isActiveFormItem(node) { 13 | var tagName = node.tagName.toUpperCase(); 14 | var isInput = ( tagName === "INPUT" && node.type.toUpperCase() in validInputTypes); 15 | var isTextarea = ( tagName === "TEXTAREA" ); 16 | var container = node.ownerDocument.contains ? node.ownerDocument : node.ownerDocument.body; 17 | if ( isInput || isTextarea ) { 18 | var isDisabled = node.readOnly || node.disabled; 19 | return !isDisabled && container.contains(node); // the element may have been disconnected from the dom between the event happening and the end of the event chain, which is another case that triggers history changes 20 | } 21 | else if ( isInActiveContentEditable(node) ) { 22 | return container.contains(node); 23 | } 24 | else { 25 | return false; 26 | } 27 | } 28 | 29 | function isInActiveContentEditable(node) { 30 | while (node) { 31 | if ( node.getAttribute && 32 | node.getAttribute("contenteditable") && 33 | node.getAttribute("contenteditable").toUpperCase() === "TRUE" ) { 34 | return true; 35 | } 36 | node = node.parentNode; 37 | } 38 | return false; 39 | } 40 | 41 | function editAnswers() { 42 | toggleEditMode(); 43 | collectAnswers(true); 44 | } 45 | 46 | // when clicked, go through array of questions marked as editable and add/remove showAllQs class 47 | // this should be used when compiling a policy or pressing Done to end an editing session 48 | function collectAnswers(isEdited){ 49 | var dic = {}; 50 | var exc = []; 51 | var ans = []; 52 | 53 | // get all the editable questions 54 | var questions = []; 55 | // are the edited questions visible? 56 | questions = document.querySelectorAll(".showAllQs"); 57 | 58 | // if we're in edit mode but no previous questions are visible 59 | if (isEdited && questions.length === 0){ 60 | // starting the edit session 61 | // grab all the hidden editable questions 62 | questions = document.querySelectorAll(".editable, .current"); 63 | // show each of the questions 64 | for (var a = 0; a < questions.length; a++){ 65 | questions[a].classList.toggle("showAllQs"); 66 | } 67 | } else if (isEdited && questions.length > 0) { 68 | // closing the edit session so collect all the visible answers 69 | // for each question 70 | for (var b = 0; b < questions.length; b++){ 71 | // get the input fields 72 | var inputFields = checkForInputs(questions[b]); 73 | // if there are input fields and we're not on the last question 74 | if ((inputFields !== false) && (b !== questions.length-1)){ 75 | // grab the question number and data 76 | qData = getQData(inputFields[0]); 77 | // for each of the input fields 78 | for (var bb = 0; bb < inputFields.length; bb++){ 79 | // get the ID 80 | aNum = inputFields[bb].id.split("-")[1]; 81 | // if the element is checked or is a type of text box 82 | if (inputFields[bb].checked || (inputFields[bb].type.includes("text") && inputFields[bb].value !== "")) { 83 | // grab any exclusions 84 | exc = updateExc(qData.data.answers[aNum], exc); 85 | // save the answer 86 | dic = saveToDict(inputFields[bb], qData.data.answers[aNum], dic); 87 | ans = storeThisA(ans, qData.ref, aNum); 88 | } else { 89 | console.log('This element is '+inputFields[bb].id); 90 | } 91 | } 92 | } else { 93 | console.log('No inputs to collect'); 94 | } 95 | // then hide the question 96 | questions[b].classList.toggle("showAllQs"); 97 | } 98 | } else { 99 | // we're collecting for a policy so get all the answers available so far 100 | questions = document.querySelectorAll(".editable, .current"); 101 | // for each question, except the last one 102 | for (var c = 0; c < questions.length; c++){ 103 | 104 | // if this question id is the same as that of the last element in questionsList then skip 105 | if (questions[c].id === questionsList[questionsList.length - 1]){ 106 | console.log('Skip'); 107 | } else { 108 | // get the input fields 109 | var inputFields = checkForInputs(questions[c]); 110 | // if there are input fields 111 | if (inputFields !== false){ 112 | 113 | // get the question number and data 114 | qData = getQData(inputFields[0]); 115 | // for each of the input fields 116 | for (var cc = 0; cc < inputFields.length; cc++){ 117 | // get the ID 118 | aNum = inputFields[cc].id.split("-")[1]; 119 | // if the element is checked or is a type of not-empty text box 120 | if (inputFields[cc].checked || (inputFields[cc].type.includes("text") && inputFields[cc].value !== "")) { 121 | // grab any exclusions 122 | exc = updateExc(qData.data.answers[aNum], exc); 123 | // save the answer 124 | dic = saveToDict(inputFields[cc], qData.data.answers[aNum], dic); 125 | ans = storeThisA(ans, qData.ref, aNum); 126 | } else { 127 | console.log('Checked or text values only'); 128 | } 129 | } 130 | } else { 131 | console.log('Input fields only'); 132 | } 133 | } 134 | } 135 | } 136 | 137 | dict = dic; 138 | currentState.answers = ans; 139 | // collect any excluded question numbers 140 | if (exc.length > 0){ 141 | currentState.exclusions = exc; 142 | } 143 | } 144 | 145 | function collectExclusions(id){ 146 | find = "#q"+id+' input:checked'; 147 | var exc = []; 148 | if (inputs = document.querySelectorAll(find)){ 149 | // for each checked input 150 | for (var i = 0; i < inputs.length; i++){ 151 | // look up the question's data 152 | q = getQData(inputs[i]); 153 | 154 | // get the input's answer's id 155 | id = inputs[i].id.split('-')[1]; 156 | // if it has exclusions 157 | // add them to the array 158 | exc = updateExc(q.data.answers[id], exc); 159 | } 160 | if (exc.length > 0){ 161 | currentState.exclusions = exc.concat(currentState.exclusions); 162 | } 163 | } else { 164 | console.log('none found'); 165 | } 166 | } 167 | 168 | 169 | function findContent(q){ 170 | switch (true) { 171 | case q < 9: 172 | q = 'q'+q; 173 | return sections[0].find(question => question.id === q); 174 | break; 175 | case q < 14: 176 | q = 'q'+q; 177 | return sections[1].find(question => question.id === q); 178 | break; 179 | case q < 21: 180 | q = 'q'+q; 181 | return sections[2].find(question => question.id === q); 182 | break; 183 | case q < 28: 184 | q = 'q'+q; 185 | return sections[3].find(question => question.id === q); 186 | break; 187 | case q < 35: 188 | q = 'q'+q; 189 | return sections[4].find(question => question.id === q); 190 | break; 191 | case q < 43: 192 | q = 'q'+q; 193 | return sections[5].find(question => question.id === q); 194 | break; 195 | case q < 49: 196 | q = 'q'+q; 197 | return sections[6].find(question => question.id === q); 198 | break; 199 | default: 200 | console.log('Question not found'); 201 | break; 202 | } 203 | } 204 | 205 | function getQData(el){ 206 | var q = {}; 207 | //# get the question number 208 | q.ref = el.id.split("-")[0]; 209 | //# get the question data 210 | q.data = findContent(q.ref.split('q')[1]); 211 | return q; 212 | } 213 | 214 | function saveToDict(el, a, storage){ 215 | // if this answer has a storeas value 216 | if (a.storeAs !== ""){ 217 | // if it's a selected button 218 | if (el.checked){ 219 | // get the edited or unedited text 220 | storage = el.nextSibling.contentEditable === "true" ? storeThisPair(a.storeAs, storage, el.nextSibling.innerText) : storeThisPair(a.storeAs, storage, a.answerText); 221 | } else if (el.type.includes('text') && el.value !== "") { 222 | // or store the contents of the text field 223 | storage = storeThisPair(a.storeAs, storage, el.value); 224 | } 225 | } else { 226 | console.log('No storing that here'); 227 | } 228 | return storage; 229 | } 230 | 231 | 232 | function storeThisA(storage, q, a){ 233 | q = q.split('q')[1]; 234 | storage.push({ 235 | q: q, 236 | a: a 237 | }); 238 | return storage; 239 | }; 240 | 241 | function storeThisPair(el, storage, text) { 242 | text = stripCode(text); 243 | // if the storeAs key already exists in the dictionary because it's a continuation of a list 244 | if (el in storage) { 245 | // copy its current value into a temp array with the new value 246 | // if it's already an array, just push 247 | if (Array.isArray(storage[el])){ // checks if array - broken? 248 | storage[el].push(text); 249 | } else { 250 | // if not then add values to create an array 251 | temp = [storage[el], text]; 252 | // then assign this temp array back to the key, overwriting the old value 253 | storage[el] = temp; 254 | } 255 | } else { 256 | // add the new key and value 257 | storage[el] = text; 258 | } 259 | return storage; 260 | } 261 | 262 | function toggleEditMode(){ 263 | // get the edit button 264 | var editBtn = document.getElementById("editBtn"); 265 | // toggle the editing class on button and page 266 | editBtn.classList.toggle('editMode'); 267 | // toggle edit button inner text 268 | editBtn.innerText = editBtn.innerText == "EDIT" ? "DONE" : "EDIT"; 269 | 270 | // if the buttons are disabled, enable them, otherwise disable them 271 | document.getElementById('previewPolicy').disabled === true ? document.getElementById('previewPolicy').disabled = false : document.getElementById('previewPolicy').disabled = true; 272 | document.getElementById('submitAnswers').disabled === true ? document.getElementById('submitAnswers').disabled = false : document.getElementById('submitAnswers').disabled = true; 273 | } 274 | 275 | 276 | function checkForInputs(q){ 277 | els = q.querySelectorAll('input, textarea'); 278 | // if this question has answers 279 | if (els.length > 0){ 280 | // return the elements 281 | return els; 282 | } else { 283 | return false; 284 | } 285 | } 286 | 287 | function updateExc(a, e){ 288 | // check for exclusions 289 | if (a.excludes.length > 0){ 290 | // add them to the list of excluded questions 291 | e = e.concat(a.excludes); 292 | return e; 293 | } else { 294 | return e; 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /content/section-6.js: -------------------------------------------------------------------------------- 1 | var section6 = [ 2 | { 3 | "isQuestion": false, 4 | "id":"q43", 5 | "title": "Scenario six", 6 | "paragraph":["Your organization receives a stern letter from your internet service provider explaining that illegal downloading violates their terms of service and warning of consequences for continued violation. It soon emerges that the organization’s WiFi network is reachable in the cafe below the office, and an opponent to your work has been sitting in there using your network to torrent films.", "Intrusions involving the network can range in their spread and impact, and there are several ways you can limit both, whether at browser, device or network level. Browser-based intrusions can involve websites themselves, but they can also expose confidential browsing information to any attacker who gains control of the computer.", "This section will also cover preparedness for viruses, suspicious network activity, and other aspects of device and network level attacks."] 7 | }, 8 | { 9 | "isQuestion": true, 10 | "id":"q44", 11 | "q":"How often are the office WiFi passwords changed?", 12 | "required":true, 13 | "policyEntry":"To ensure that the WiFi access is restricted to authorised staff, [organisation-name] rotates the password [how-often-wifi-changed] and the new credentials will be made available to you by [security-contact-name].", 14 | "routineEntry":"", 15 | "appendixEntry":"", 16 | "answers":[ 17 | { 18 | "type":"radio", 19 | "answerText":"at every security review", 20 | "storeAs":"[how-often-wifi-changed]", 21 | "excludes":[], 22 | "policyEntry":"", 23 | "routineEntry":"", 24 | "appendixEntry":[ 25 | { 26 | "reviewList":"It's time to change the office WiFi passwords!", 27 | "tipList": "", 28 | "linksList": "" 29 | } 30 | ] 31 | }, 32 | { 33 | "type":"radio", 34 | "answerText":"every 6 months", 35 | "storeAs":"[how-often-wifi-changed]", 36 | "excludes":[], 37 | "policyEntry":"", 38 | "routineEntry":"", 39 | "appendixEntry":[ 40 | { 41 | "reviewList":"", 42 | "tipList": "", 43 | "linksList": "" 44 | } 45 | ] 46 | }, 47 | { 48 | "type":"radio", 49 | "answerText":"every month", 50 | "storeAs":"[how-often-wifi-changed]", 51 | "excludes":[], 52 | "policyEntry":"", 53 | "routineEntry":"", 54 | "appendixEntry":[ 55 | { 56 | "reviewList":"", 57 | "tipList": "", 58 | "linksList": "" 59 | } 60 | ] 61 | }, 62 | { 63 | "type":"radio", 64 | "answerText":"each week", 65 | "storeAs":"[how-often-wifi-changed]", 66 | "excludes":[], 67 | "policyEntry":"", 68 | "routineEntry":"", 69 | "appendixEntry":[ 70 | { 71 | "reviewList":"", 72 | "tipList": "", 73 | "linksList": "" 74 | } 75 | ] 76 | }, 77 | ], 78 | "tips":[ 79 | {"relevance":"If your organization uses a password-protected wireless internet connection."}, 80 | {"meaning":"It’s important to keep in mind that WiFi extends beyond the physical walls of the office, and so an attacker can gain access even if they’re outside or next door. Strong passwords changed regularly are key to ensuring that, even if the password is compromised somehow, its ability to grant access is time-limited."}, 81 | {"implementation":"Consider how you can share the latest password easily and securely, for instance through an encrypted email or secure file share."}, 82 | {"more": [""]} 83 | ] 84 | }, 85 | { 86 | "isQuestion": true, 87 | "id":"q45", 88 | "q":"What measures do you take to protect your office network?", 89 | "required":true, 90 | "policyEntry":"When using the internal network, you can expect to find the following security measures in place.", 91 | "routineEntry":"", 92 | "appendixEntry":"", 93 | "answers":[ 94 | { 95 | "type":"checkbox", 96 | "answerText":"Password protection", 97 | "storeAs":"", 98 | "excludes":[], 99 | "policyEntry":"You will need a password in order to gain access, which can be obtained from [security-contact-name].", 100 | "routineEntry":"", 101 | "appendixEntry":[ 102 | { 103 | "reviewList":"", 104 | "tipList": "", 105 | "linksList": "" 106 | } 107 | ] 108 | }, 109 | { 110 | "type":"checkbox", 111 | "answerText":"Firewall", 112 | "storeAs":"", 113 | "excludes":[], 114 | "policyEntry":"A firewall has been set up to prevent malicious connections. If you believe the firewall may be negatively impacting your internet activities, and therefore your work, please speak to [security-contact-name].", 115 | "routineEntry":"", 116 | "appendixEntry":[ 117 | { 118 | "reviewList":"", 119 | "tipList": "", 120 | "linksList": "" 121 | } 122 | ] 123 | }, 124 | { 125 | "type":"checkbox", 126 | "answerText":"Approved devices only", 127 | "storeAs":"", 128 | "excludes":[], 129 | "policyEntry":"Only approved devices are to be on the [organisation-name] internal network. If you need to use an unapproved device you should request permission to connect from [security-contact-name] so it can be added to the safe list.", 130 | "routineEntry":"", 131 | "appendixEntry":[ 132 | { 133 | "reviewList":"", 134 | "tipList": "", 135 | "linksList": "" 136 | } 137 | ] 138 | }, 139 | { 140 | "type":"checkbox", 141 | "answerText":"Wired connections available", 142 | "storeAs":"", 143 | "excludes":[], 144 | "policyEntry":"Wired connections are available and recommended. Let [security-contact-name] know if you need a cable for connecting your device.", 145 | "routineEntry":"", 146 | "appendixEntry":[ 147 | { 148 | "reviewList":"", 149 | "tipList": "", 150 | "linksList": "" 151 | } 152 | ] 153 | }, 154 | { 155 | "type":"checkbox", 156 | "answerText":"Traffic inspection", 157 | "storeAs":"", 158 | "excludes":[], 159 | "policyEntry":"It is important to understand that any and all traffic can be inspected while on the [organisation-name] internal network.", 160 | "routineEntry":"", 161 | "appendixEntry":[ 162 | { 163 | "reviewList":"", 164 | "tipList": "", 165 | "linksList": "" 166 | } 167 | ] 168 | }, 169 | { 170 | "type":"checkbox", 171 | "editable":true, 172 | "answerText":"something else", 173 | "storeAs":"[alt-network-security]", 174 | "excludes":[], 175 | "policyEntry":"Please also note the following rule: [alt-network-security]", 176 | "routineEntry":"", 177 | "appendixEntry":[ 178 | { 179 | "reviewList":"", 180 | "tipList": "", 181 | "linksList": "" 182 | } 183 | ] 184 | }, 185 | ], 186 | "tips":[ 187 | {"relevance":"Your organization controls the security of the internet it uses, e.g. in an owned office, rather than a coworking space."}, 188 | {"meaning":"There are several measures you can put in place to protect your office network from intruders and each one helps to build a trusted environment in which you are aware of who is on the network with you. A wired connection is recommended, and where a wireless network is in use it should be password-protected (secured with WPA2 or WPA3). A firewall guards your network by monitoring the traffic and allowing or denying connections based on your rules. Organizations may also ask staff to request permission before connecting their personal devices to the network as this can make it easier to spot suspicious activity when monitoring traffic."}, 189 | {"implementation":""}, 190 | {"more": ["how to configure a firewall", "security of wired vs wireless network", "difference between WPA2 and WPA3", "vulnerabilities in WPA2", "spotting suspicious network activity"]} 191 | ] 192 | }, 193 | { 194 | "isQuestion": true, 195 | "id":"q46", 196 | "q":"How can staff protect themselves from browser-based intrusions?", 197 | "required":true, 198 | "policyEntry":"Your web browser can provide a window into your online activities, potentially exposing confidential data such as activist identities and [organisation-name] project details. To mitigate this, we recommend implementing the following security hygiene techniques in your browser:", 199 | "routineEntry":"", 200 | "appendixEntry":"", 201 | "answers":[ 202 | { 203 | "type":"checkbox", 204 | "answerText":"Clear cookies", 205 | "storeAs":"", 206 | "excludes":[], 207 | "policyEntry":"- clear cookies", 208 | "routineEntry":"", 209 | "appendixEntry":[ 210 | { 211 | "reviewList":"", 212 | "tipList": "", 213 | "linksList": "" 214 | } 215 | ] 216 | }, 217 | { 218 | "type":"checkbox", 219 | "answerText":"Clear history", 220 | "storeAs":"", 221 | "excludes":[], 222 | "policyEntry":"- delete browser history (this can be set up on an automatic schedule)", 223 | "routineEntry":"When did you last clear your browsing history?", 224 | "appendixEntry":[ 225 | { 226 | "reviewList":"", 227 | "tipList": "", 228 | "linksList": "" 229 | } 230 | ] 231 | }, 232 | { 233 | "type":"checkbox", 234 | "answerText":"Enable \"Do Not Track\"", 235 | "storeAs":"", 236 | "excludes":[], 237 | "policyEntry":"- enable Do Not Track to hide from trackers", 238 | "routineEntry":"", 239 | "appendixEntry":[ 240 | { 241 | "reviewList":"", 242 | "tipList": "", 243 | "linksList": "" 244 | } 245 | ] 246 | }, 247 | { 248 | "type":"checkbox", 249 | "answerText":"JavaScript off", 250 | "storeAs":"", 251 | "excludes":[], 252 | "policyEntry":"- disable JavaScript", 253 | "routineEntry":"", 254 | "appendixEntry":[ 255 | { 256 | "reviewList":"", 257 | "tipList": "", 258 | "linksList": "" 259 | } 260 | ] 261 | }, 262 | { 263 | "type":"checkbox", 264 | "editable":true, 265 | "answerText":"something else", 266 | "storeAs":"[alt-browser-security]", 267 | "excludes":[], 268 | "policyEntry":"- also, [alt-browser-security]", 269 | "routineEntry":"", 270 | "appendixEntry":[ 271 | { 272 | "reviewList":"", 273 | "tipList": "", 274 | "linksList": "" 275 | } 276 | ] 277 | }, 278 | ], 279 | "tips":[ 280 | {"relevance":""}, 281 | {"meaning":"In the course of your employees’ work, their online searches, clicks and website visits can put together a picture of their activities that exposes sensitive or confidential information. Anyone accessing the computer can see the sites accessed and the logins used, so clearing browsing history regularly can reduce how much history is accessible. Cookies and scripting attacks have been used maliciously in the past as they are usually trusted elements of websites which are used to execute code or store data files on the client computer. Disabling cookies and JavaScript in the browser can affect the user’s ability to use websites which rely on JavaScript and cookies though, so the seriousness of the threat should be balanced against the inconvenience. Enabling Do Not Track in the browser helps to reduce the amount of information that advertisers and tracking companies can gather about your employees’ searches, but does rely on websites complying with the request."}, 282 | {"implementation":"If your organization provides devices to staff, then these solutions can be implemented in the set up."}, 283 | {"more": ["how to turn on Do Not Track", "risks of cookies and scripting attacks", "why how to clear browsing history", "why turn javascript off"]} 284 | ] 285 | }, 286 | { 287 | "isQuestion": true, 288 | "id":"q47", 289 | "q":"How should devices be set up to protect against network-based attacks?", 290 | "required":true, 291 | "policyEntry":"", 292 | "routineEntry":"", 293 | "appendixEntry":"", 294 | "answers":[ 295 | { 296 | "type":"checkbox", 297 | "answerText":"Anti-virus", 298 | "storeAs":"", 299 | "excludes":[], 300 | "policyEntry":"Anti-virus software is required on your computer, in order to deal with any virus-related threats. However, this will not take care of all viruses, and so you should continue to be vigilant against suspicious downloads.", 301 | "routineEntry":"", 302 | "appendixEntry":[ 303 | { 304 | "reviewList":"", 305 | "tipList": "", 306 | "linksList": "" 307 | } 308 | ] 309 | }, 310 | { 311 | "type":"checkbox", 312 | "answerText":"Automatic updates", 313 | "storeAs":"", 314 | "excludes":[], 315 | "policyEntry":"Automatic updates simply ensures that your software is updated quietly in the background, so there is less chance of it being out-of-date and therefore vulnerable to exploits. On occasion, the latest software version may be unstable, in which case you will be informed to prevent the update.", 316 | "routineEntry":"", 317 | "appendixEntry":[ 318 | { 319 | "reviewList":"", 320 | "tipList": "", 321 | "linksList": "" 322 | } 323 | ] 324 | }, 325 | { 326 | "type":"checkbox", 327 | "answerText":"Firewall", 328 | "storeAs":"", 329 | "excludes":[], 330 | "policyEntry":"A firewall will be set up on your device to manage the incoming and outgoing connections and prevent malicious data from being transferred. If you find that this interferes with your ability to carry out work activities then you should inform [contactname].", 331 | "routineEntry":"", 332 | "appendixEntry":[ 333 | { 334 | "reviewList":"", 335 | "tipList": "", 336 | "linksList": "" 337 | } 338 | ] 339 | }, 340 | { 341 | "type":"checkbox", 342 | "editable":true, 343 | "answerText":"something else", 344 | "storeAs":"[alt-device-security]", 345 | "excludes":[], 346 | "policyEntry":"In addition, please note the following rule: [alt-device-security]", 347 | "routineEntry":"", 348 | "appendixEntry":[ 349 | { 350 | "reviewList":"", 351 | "tipList": "", 352 | "linksList": "" 353 | } 354 | ] 355 | }, 356 | ], 357 | "tips":[ 358 | {"relevance":""}, 359 | {"meaning":"There are many types of virus, many of which can be picked up by anti-virus software. Some viruses exploit out-of-date software with unpatched security holes, and so enabling automatic software updates can help to reduce these vulnerabilities. Using a firewall at the device level to manage incoming and outgoing connections can assist with flagging suspicious activity early."}, 360 | {"implementation":"If your organization provides devices to staff, then these solutions can be implemented in the set up."}, 361 | {"more": ["how to enable automatic updates", "how to configure firewall"]} 362 | ] 363 | }, 364 | { 365 | "isQuestion": true, 366 | "id":"q48", 367 | "q":"What steps should staff members take if their device is taken over by ransomware?", 368 | "required":false, 369 | "policyEntry":"... you suspect your device is infected with ransomware: it’s important not to panic and pay the ransom as that gives no guarantee that your files will be released. Instead, you should follow these steps: [do-this-if-ransomware]", 370 | "appendixEntry": "", 371 | "routineEntry":"", 372 | "answers":[ 373 | { 374 | "type":"textarea", 375 | "answerText":"", 376 | "placeholder":"- note down the exact message displayed on your screen, \n- contact the security officer, \n- locate your backups to prepare for a system restore", 377 | "storeAs":"[do-this-if-ransomware]", 378 | "excludes":[], 379 | "policyEntry":"", 380 | "routineEntry":"", 381 | "appendixEntry":[ 382 | { 383 | "reviewList":"", 384 | "tipList": "", 385 | "linksList": "" 386 | } 387 | ] 388 | } 389 | ], 390 | "tips":[ 391 | {"relevance":""}, 392 | {"meaning":"Ransom malware, or ransomware, infects computers and mobile devices through spam, phishing and malicious advertising. Its goal is to lock the user out of their device while demanding a ransom for the decryption and/or release of their files. It’s a popular type of malware attack that is increasingly used against companies and organizations, due to the higher value of the data being held ransom. The first rule of dealing with ransomware is to never pay the ransom: it encourages the attacker and there’s no guarantee your files will be released. There are decryptors available for some ransomware and remediation software for removing the threat, but a full system restore may be the only solution for some infections."}, 393 | {"implementation":""}, 394 | {"more": ["what to do when infected with ransomware"]} 395 | ] 396 | } 397 | ] 398 | -------------------------------------------------------------------------------- /content/section-1.js: -------------------------------------------------------------------------------- 1 | var section1 = [ 2 | { 3 | "isQuestion": false, 4 | "id":"q9", 5 | "title": "Scenario one", 6 | "paragraph":["In this first scenario, we'll look at security threats which occur through organizational communications channels. A very common example of this that you've no doubt already experienced, is that of the suspicious email. Staff may already be aware of some 'red flags' from their personal experience of spam emails, but scammers are becoming more sophisticated all the time and attacks can be especially persuasive in instances where they have been directly targeting civil society organizations.", "An example of this is fake Google notifications sent with a request to 'review' some account detail by logging in using the provided link - at which point the login details are stolen by the attackers. Other risks can come from unexpected attachments containing malware or urgent requests for sensitive information from attackers impersonating one of the recipients trusted contacts.","Although we're talking about email here, this section will also look at other methods as really any communications channel can be vulnerable and should be considered when building your security policy."] 7 | }, 8 | { 9 | "isQuestion": true, 10 | "id":"q10", 11 | "q":"Do all members of staff need a PGP key for their work email?", 12 | "required":false, 13 | "policyEntry":"", 14 | "appendixEntry":"", 15 | "routineEntry":"", 16 | "answers":[ 17 | { 18 | "type":"radio", 19 | "answerText":"Yes", 20 | "storeAs":"", 21 | "excludes":[], 22 | "policyEntry":"[organisation-name] requires staff to use PGP to encrypt sensitive emails. If you need help with setting up, please contact [security-contact-name].", 23 | "routineEntry":"Sending an email containing confidential information? Hide it from prying eyes by encrypting it with your PGP key.", 24 | "appendixEntry":[ 25 | { 26 | "reviewList":"", 27 | "tipList": "", 28 | "linksList": "" 29 | } 30 | ], 31 | }, 32 | { 33 | "type":"radio", 34 | "answerText":"Some staff need a key", 35 | "storeAs":"", 36 | "excludes":[], 37 | "policyEntry":"[organisation-name] prefers some staff to use PGP to encrypt sensitive emails. You will be informed if this applies to you and offered help with setting up, should you require it.", 38 | "routineEntry":"If you have a PGP key, please use it to encrypt emails containing sensitive information.", 39 | "appendixEntry":[ 40 | { 41 | "reviewList":"", 42 | "tipList": "", 43 | "linksList": "" 44 | } 45 | ], 46 | }, 47 | { 48 | "type":"radio", 49 | "answerText":"None of our staff use PGP", 50 | "storeAs":"", 51 | "excludes":[], 52 | "policyEntry":"", 53 | "routineEntry":"", 54 | "appendixEntry":[ 55 | { 56 | "reviewList":"", 57 | "tipList": "", 58 | "linksList": "" 59 | } 60 | ] 61 | } 62 | ], 63 | "tips":[ 64 | {"relevance":"If you think there’s a chance that your organization’s emails could be intercepted, or someone could impersonate a member of your staff via email."}, 65 | {"meaning":"PGP (Pretty Good Privacy) is a reliable way to encrypt the contents of your emails and verify that it was you who sent them. Putting your public key on the keyserver helps ensure people can contact you securely, or you can include the public key in your email signature."}, 66 | {"implementation":"The process to set up PGP can be intimidating, so it’s best to read up on it before starting. An introduction to it during an all-staff meeting will help with getting buy-in, and additional 1-1 training should be given to staff with little to no experience of encrypting email. Implementation will depend on the email clients your staff use, but Enigmail with Thunderbird is a popular option. Note: PGP uses encryption and therefore may not be suitable for all working contexts."}, 67 | {"more": ["how to set up PGP", "using PGP with Enigmail"]} 68 | ] 69 | }, 70 | { 71 | "isQuestion": true, 72 | "id":"q11", 73 | "q":"How should staff protect their in-browser interactions?", 74 | "required":false, 75 | "policyEntry":"In order to protect your online activities from common tracking and interference, we recommend all staff install the following privacy-enhancing extensions: [use-these-browser-extensions].", 76 | "appendixEntry":"", 77 | "routineEntry":"", 78 | "answers":[ 79 | { 80 | "type":"checkbox", 81 | "answerText":"Privacy Badger", 82 | "storeAs":"[use-these-browser-extensions]", 83 | "excludes":[], 84 | "policyEntry":"", 85 | "routineEntry":"", 86 | "appendixEntry":[ 87 | { 88 | "reviewList":"", 89 | "tipList": "", 90 | "linksList": "Privacy Badger website: https://www.eff.org/privacybadger" 91 | } 92 | ] 93 | }, 94 | { 95 | "type":"checkbox", 96 | "answerText":"Privacy Possum", 97 | "storeAs":"[use-these-browser-extensions]", 98 | "excludes":[], 99 | "policyEntry":"", 100 | "routineEntry":"", 101 | "appendixEntry":[ 102 | { 103 | "reviewList":"", 104 | "tipList": "", 105 | "linksList": "GitHub for Privacy Possum: https://github.com/cowlicks/privacypossum" 106 | } 107 | ] 108 | }, 109 | { 110 | "type":"checkbox", 111 | "answerText":"uBlock Origin", 112 | "storeAs":"[use-these-browser-extensions]", 113 | "excludes":[], 114 | "policyEntry":"", 115 | "routineEntry":"", 116 | "appendixEntry":[ 117 | { 118 | "reviewList":"", 119 | "tipList": "", 120 | "linksList": "GitHub for uBlock: https://github.com/gorhill/uBlock" 121 | } 122 | ] 123 | }, 124 | { 125 | "type":"checkbox", 126 | "answerText":"Better", 127 | "storeAs":"[use-these-browser-extensions]", 128 | "excludes":[], 129 | "policyEntry":"", 130 | "routineEntry":"", 131 | "appendixEntry":[ 132 | { 133 | "reviewList":"", 134 | "tipList": "", 135 | "linksList": "Better website: https://better.fyi" 136 | } 137 | ] 138 | }, 139 | { 140 | "type":"checkbox", 141 | "answerText":"HTTPSEverywhere", 142 | "storeAs":"[use-these-browser-extensions]", 143 | "excludes":[], 144 | "policyEntry":"", 145 | "routineEntry":"", 146 | "appendixEntry":[ 147 | { 148 | "reviewList":"", 149 | "tipList": "", 150 | "linksList": "HTTPSEverywhere website: https://www.eff.org/https-everywhere" 151 | } 152 | ] 153 | }, 154 | { 155 | "type":"checkbox", 156 | "answerText":"DuckDuckGo Privacy Essentials", 157 | "storeAs":"[use-these-browser-extensions]", 158 | "excludes":[], 159 | "policyEntry":"", 160 | "routineEntry":"", 161 | "appendixEntry":[ 162 | { 163 | "reviewList":"", 164 | "tipList": "", 165 | "linksList": "DuckDuckGo Privacy Essentials description: https://spreadprivacy.com/privacy-simplified" 166 | } 167 | ] 168 | }, 169 | { 170 | "type":"checkbox", 171 | "answerText":"Password Alert", 172 | "storeAs":"[use-these-browser-extensions]", 173 | "excludes":[], 174 | "policyEntry":"", 175 | "routineEntry":"", 176 | "appendixEntry":[ 177 | { 178 | "reviewList":"", 179 | "tipList": "", 180 | "linksList": "Password Alert FAQ: https://support.google.com/a/answer/6197508" 181 | } 182 | ] 183 | }, 184 | { 185 | "type":"checkbox", 186 | "editable": true, 187 | "answerText":"another add-on", 188 | "storeAs": "[use-these-browser-extensions]", 189 | "excludes":[], 190 | "policyEntry":"", 191 | "routineEntry":"", 192 | "appendixEntry":[ 193 | { 194 | "reviewList":"", 195 | "tipList": "", 196 | "linksList": "" 197 | } 198 | ] 199 | } 200 | ], 201 | "tips":[ 202 | {"relevance":"If your organization’s staff use internet browsers that are capable of having extensions or plug-ins extend their functionality."}, 203 | {"meaning":"Using an internet browser is such an everyday part of most people’s work that we often forget it is also a prime target for privacy invasions and attacks. Thankfully, browser extensions – add-ons that extend a browser’s features beyond the default – are available for defending against tracking by Google, Facebook and other advertising companies. They can also be used to detect fake Google login pages (used in spear phishing attacks) and encrypt communication with a website."}, 204 | {"implementation":"Many of these extensions do their job silently in the background, only disturbing the user’s workflow on the occasions when doing their job causes disruption to the page’s functionality. Mobile browsing can also be protected through tracker-blocking apps and privacy-conscious browsers, and this can help with building a consistent defence for staff whenever they are using the internet for work purposes."}, 205 | {"more": ["privacy browser extensions", "Privacy Badger Possum", "uBlock Origin", "HTTPSEverywhere", "Better privacy tool", "DuckDuckGo search engine", "Google password alert"]} 206 | ] 207 | }, 208 | { 209 | "isQuestion": true, 210 | "id":"q12", 211 | "q":"For each of the following communication types, which tools should staff use when discussing sensitive work topics?", 212 | "required":false, 213 | "policyEntry":"When communicating sensitive work data, [organisation-name] prefers staff use the following tools.", 214 | "appendixEntry":"Pay attention to any changes in encryption standards, data breaches or security vulnerabilities that are in the news and relevant to your communication platforms.", 215 | "routineEntry":"", 216 | "answers":[ 217 | { 218 | "type":"text", 219 | "answerText":"1-1 messaging", 220 | "placeholder":"e.g. Signal, WhatsApp, encrypted email", 221 | "storeAs":"[use-these-for-direct-chat]", 222 | "excludes":[], 223 | "policyEntry":"- For 1-1 communications (e.g. direct message to a colleague), please use [use-these-for-direct-chat].", 224 | "routineEntry":"Use [use-these-for-direct-chat] for direct 1-1 messaging.", 225 | "appendixEntry":[ 226 | { 227 | "reviewList":"Is [use-these-for-direct-chat] still the most appropriate method for your direct communications?", 228 | "tipList": "", 229 | "linksList": "" 230 | } 231 | ], 232 | }, 233 | { 234 | "type":"text", 235 | "answerText":"Team messaging", 236 | "placeholder":"e.g. Mattermost, Signal, unencrypted email", 237 | "storeAs":"[use-these-for-group-chat]", 238 | "excludes":[], 239 | "policyEntry":"- When communicating with a group (e.g. your project team), please use [use-these-for-group-chat].", 240 | "routineEntry":"[use-these-for-group-chat] should be used for team messaging.", 241 | "appendixEntry":[ 242 | { 243 | "reviewList":"Should you continue recommending [use-these-for-group-chat] for group communications?", 244 | "tipList": "", 245 | "linksList": "" 246 | } 247 | ], 248 | }, 249 | { 250 | "type":"text", 251 | "answerText":"Internal video/audio calls", 252 | "placeholder":"e.g. Jitsi, Signal, Google Hangouts", 253 | "storeAs":"[use-these-for-internal-chat]", 254 | "excludes":[], 255 | "policyEntry":"- We use [use-these-for-internal-chat] for internal video or audio calls, e.g. for remote staff meetings.", 256 | "routineEntry":"Video or audio calls within [organisation-name] should be carried out on [use-these-for-internal-chat].", 257 | "appendixEntry":[ 258 | { 259 | "reviewList":"How well has [use-these-for-internal-chat] been working as an internal video calls platform?", 260 | "tipList": "", 261 | "linksList": "" 262 | } 263 | ], 264 | }, 265 | { 266 | "type":"text", 267 | "answerText":"External video/audio calls", 268 | "placeholder":"e.g. Jitsi, Signal, Skype", 269 | "storeAs":"[use-these-for-external-chat]", 270 | "excludes":[], 271 | "policyEntry":"- External video and audio calls take place through [use-these-for-external-chat].", 272 | "routineEntry":"Video or audio calls with those outside of [organisation-name] should be hosted on [use-these-for-external-chat].", 273 | "appendixEntry":[ 274 | { 275 | "reviewList":"Is [use-these-for-external-chat] still a good option for external video and audio calls?", 276 | "tipList": "", 277 | "linksList": "" 278 | } 279 | ], 280 | } 281 | ], 282 | "tips":[ 283 | {"relevance":"If your organization would prefer staff to use particular tools in specific scenarios rather than leaving it to individual choice."}, 284 | {"meaning":"Specifying particular tools for each common use case helps with consistent communications management and security. It can also help staff to spot suspicious communications that aren’t using the approved methods. For instance, a request for sensitive information to be sent via Telegram can be more easily flagged if Telegram is not an approved method for work communications. Each of the tools has its pros and cons, particularly when it comes to privacy and security, and it is up to your organization to decide which is best for your working environments."}, 285 | {"implementation":"Changing tools can be tricky as it’s hard to break a habit, but the previously-selected security champion and management can all set an example to rest of the organization. For instance,they might only accept internal video call invites which use the approved platform and gently redirect staff to update their invite with the new, more appropriate method."}, 286 | {"more": ["secure group chat and conferencing tools", "Slack vs Mattermost vs Matrix", "secure messaging app comparison", "WhatsApp vs Signal vs Telegram", "Hangouts vs Jitsi vs Skype", "cell sms security interception"]} 287 | ] 288 | }, 289 | { 290 | "isQuestion": true, 291 | "id":"q13", 292 | "q":"What steps should staff take if they are faced with a suspicious email?", 293 | "required":false, 294 | "policyEntry":"... you receive a suspicious email: Phishing involves being duped into providing information to parties that you otherwise wouldn’t share, usually through fake account notifications or impersonating a known contact. Unfortunately this kind of attack in common and so it is important to understand the steps we recommend taking to both mitigate and recover from such an attack: [do-this-if-phished]", 295 | "appendixEntry": "To check whether a link or attachment contains known malware, upload it to VirusTotal; a service owned by Google which reads the file and detects familiar malicious code. It won’t find every attack, only the known ones it’s seen before, but it’s a valuable tool nonetheless.", 296 | "routineEntry":"", 297 | "answers":[ 298 | { 299 | "type":"textarea", 300 | "answerText":"", 301 | "placeholder":"Here are a few examples: \n\nIf the email gives you a login link: open a new browser window and sign into the site there without using the link. Emailed links can have subtle changes made to them which redirect users to malicious sites for collecting login details. \n\nIf the email isn’t addressed to you, but to multiple people: attackers often send messages to large groups in order to ‘catch out’ as many people as possible \n\nIf the email has an unexpected attachment: don’t open the file unless you have contacted the sender through another channel to verify it. If you clicked on a link or opened an unexpected attachment, even from a trusted contact: disconnect from the internet and cease using the device – use a different device to change any passwords you used on that device.", 302 | "storeAs":"[do-this-if-phished]", 303 | "excludes":[], 304 | "policyEntry":"", 305 | "routineEntry":"", 306 | "appendixEntry":[ 307 | { 308 | "reviewList":"Are all of the incident response plans up-to-date?", 309 | "tipList": "", 310 | "linksList": "" 311 | } 312 | ] 313 | } 314 | ], 315 | "tips":[ 316 | {"relevance":""}, 317 | {"meaning":"A common attack used against civil society organizations involves sending fake account (most often Google) notifications that ask the reader to sign in via a provided link in order to take some urgent action, such as “reactivating” the account. This kind of attack is known as phishing, and its aim is to convince the reader to enter their username and password into the sender’s fake login page, thus giving them access to the account. Spear phishing is also a threat in our work as, rather than impersonating big companies, it uses personal information about its target (e.g. the name of a trusted contact) to appear more convincing. Phishing emails of all types can be hard to spot, so it’s best to prepare staff for the main red flags as well as the inevitable day when one slips through the net."}, 318 | {"implementation":"Changing tools can be tricky as it’s hard to break a habit, but the previously-selected security champion and management can all set an example to rest of the organization. For instance,they might only accept internal video call invites which use the approved platform and gently redirect staff to update their invite with the new, more appropriate method."}, 319 | {"more": ["how to avoid a phishing attack", "what is spear phishing", "report google phishing email"]} 320 | ] 321 | } 322 | ]; 323 | -------------------------------------------------------------------------------- /content/section-3.js: -------------------------------------------------------------------------------- 1 | var section3 = [ 2 | { 3 | "isQuestion": false, 4 | "id":"q21", 5 | "title": "Scenario three", 6 | "paragraph":["A staff member was arrested while attending a protest and their phone was confiscated by the police. While the phone is out of your colleague's sight, data from the phone, SIM and SD card could all be copied and searched. This means that contacts on the SIM card, photos on the SD card and any logged in apps on the phone may have been accessed and could be potentially used to justify warrants for escalated surveillance in the future.", "Replacing the SIM, phone and SD card, rotating credentials and watching for suspicious activity can be time-consuming remedies to the situation, and so defining how to secure devices is an important and useful tool against such incidents.", "A lost or compromised device can be remotely wiped with a device management tool, and backup maintenance can ensure it's easy to get back up and running with a new or reset device. This section will also cover the prevention of unauthorised device and backup access."] 7 | }, 8 | { 9 | "isQuestion": true, 10 | "id":"q22", 11 | "q":"Can staff use biometric authentication to restrict device access?", 12 | "required":true, 13 | "policyEntry":"To protect devices from unauthorised physical infiltration, all devices which are used for [organisation-name] purposes should have an automatic screen lock enabled and may be configured to use [biometric-authentication-options].", 14 | "appendixEntry":"Be aware that touchscreen devices are susceptible to holding fingerprint marks which can be used to guess recently-used key sequences.", 15 | "routineEntry":"", 16 | "answers":[ 17 | { 18 | "type":"checkbox", 19 | "answerText":"fingerprint authentication", 20 | "storeAs":"[biometric-authentication-options]", 21 | "excludes":[], 22 | "policyEntry":"", 23 | "routineEntry":"", 24 | "appendixEntry":[ 25 | { 26 | "reviewList":"", 27 | "tipList": "", 28 | "linksList": "" 29 | } 30 | ] 31 | }, 32 | { 33 | "type":"checkbox", 34 | "answerText":"facial recognition authentication", 35 | "storeAs":"[biometric-authentication-options]", 36 | "excludes":[], 37 | "policyEntry":"", 38 | "routineEntry":"", 39 | "appendixEntry":[ 40 | { 41 | "reviewList":"", 42 | "tipList": "Be aware that facial recognition authentication is vulnerable to unauthorised access as most systems won't flag a sleeping face, an unwilling face, or a twin.", 43 | "linksList": "" 44 | } 45 | ] 46 | } 47 | ], 48 | "tips":[ 49 | {"relevance":"If your organization provides devices to staff or installs work profiles on their personal devices."}, 50 | {"meaning":"Recent developments in facial and fingerprint recognition have led to biometric authentication becoming a convenient alternative to passwords, although there are security flaws which should be considered. It's important to also set up a password, code or phrase as an extra barrier that can't be as easily circumvented."}, 51 | {"implementation":"The unlock method is irrelevant if devices are almost never locked; enabling the automatic screen lock helps to ensure that others can’t walk right through the open doors of an unlocked device."}, 52 | {"more": ["security of face unlock id", "creating strong passwords", "security of biometric authentication"]} 53 | ] 54 | }, 55 | { 56 | "isQuestion": true, 57 | "id":"q23", 58 | "q":"Does your organization use any tools for device security management?", 59 | "required":false, 60 | "policyEntry":"To support device security across the organization, we use the following tools: [we-use-this-device-manager]", 61 | "appendixEntry":"", 62 | "routineEntry":"", 63 | "answers":[ 64 | { 65 | "type":"checkbox", 66 | "answerText":"Flock", 67 | "storeAs":"[we-use-this-device-manager]", 68 | "excludes":[], 69 | "policyEntry":"", 70 | "routineEntry":"", 71 | "appendixEntry":[ 72 | { 73 | "reviewList":"", 74 | "tipList": "", 75 | "linksList": "Flock Agent for macOS & Linux system 'health checks': https://github.com/firstlookmedia/flock-agent" 76 | } 77 | ] 78 | }, 79 | { 80 | "type":"checkbox", 81 | "answerText":"Stethoscope", 82 | "storeAs":"[we-use-this-device-manager]", 83 | "excludes":[], 84 | "policyEntry":"", 85 | "routineEntry":"", 86 | "appendixEntry":[ 87 | { 88 | "reviewList":"", 89 | "tipList": "", 90 | "linksList": "Stethoscope for Windows & macOS systems: https://ragtag.org/stethoscope" 91 | } 92 | ] 93 | }, 94 | { 95 | "type":"checkbox", 96 | "answerText":"Apple MDM", 97 | "storeAs":"[we-use-this-device-manager]", 98 | "excludes":[], 99 | "policyEntry":"", 100 | "routineEntry":"", 101 | "appendixEntry":[ 102 | { 103 | "reviewList":"", 104 | "tipList": "", 105 | "linksList": "Apple MDM documentation: https://developer.apple.com/documentation/devicemanagement" 106 | } 107 | ] 108 | }, 109 | { 110 | "type":"checkbox", 111 | "answerText":"Google Endpoint Management", 112 | "storeAs":"[we-use-this-device-manager]", 113 | "excludes":[], 114 | "policyEntry":"", 115 | "routineEntry":"", 116 | "appendixEntry":[ 117 | { 118 | "reviewList":"", 119 | "tipList": "", 120 | "linksList": "Google Endpoint Management: https://gsuite.google.com/products/admin/endpoint" 121 | } 122 | ] 123 | }, 124 | { 125 | "type":"checkbox", 126 | "editable":true, 127 | "answerText":"another device manager", 128 | "storeAs":"[we-use-this-device-manager]", 129 | "excludes":[], 130 | "policyEntry":"", 131 | "routineEntry":"", 132 | "appendixEntry":[ 133 | { 134 | "reviewList":"", 135 | "tipList": "", 136 | "linksList": "" 137 | } 138 | ] 139 | } 140 | ], 141 | "tips":[ 142 | {"relevance":"For organizations where there are more than a handful of devices in use."}, 143 | {"meaning":"A security policy is of little use unless everyone it applies to is adhering to the rules it lays out, but it’s time-consuming to dig into the settings of every device in your organization. Tools like Flock and Stethoscope can help with this; enabling you to perform a security “health check” on devices. Managing the security of multiple devices can also be made easier with Mobile Device Management (MDM) services, as it can enable the remote install of VPNs and other security apps, as well as remote wipe in the case of device confiscation."}, 144 | {"implementation":"Getting this sort of management tool in place is better done earlier rather than later while there are a manageable number of devices and people to introduce to the system."}, 145 | {"more": ["mobile device management solutions", "Flock agent firstlookmedia", "Stethoscope security tool", "Apple MDM device enrollment", "Google Endpoint Management"]} 146 | ] 147 | }, 148 | { 149 | "isQuestion": true, 150 | "id":"q24", 151 | "q":"Does your organization use physical or cloud storage for backups?", 152 | "required":false, 153 | "policyEntry":"[organisation-name] is responsible for managing data backups which are kept in [we-use-this-backup-storage] storage.", 154 | "appendixEntry":"", 155 | "routineEntry":"", 156 | "answers":[ 157 | { 158 | "type":"checkbox", 159 | "answerText":"physical", 160 | "storeAs":"[we-use-this-backup-storage]", 161 | "excludes":[], 162 | "policyEntry":"", 163 | "routineEntry":"", 164 | "appendixEntry":[ 165 | { 166 | "reviewList":"Is your physical storage still in good working order or is it time to upgrade?", 167 | "tipList": "There are physical storage devices available which offer additional security measures, such as keypads – but be sure to keep them in a protective case to prevent accidental damage that could corrupt or wipe the data.", 168 | "linksList": "" 169 | } 170 | ] 171 | }, 172 | { 173 | "type":"checkbox", 174 | "answerText":"cloud", 175 | "storeAs":"[we-use-this-backup-storage]", 176 | "excludes":[], 177 | "policyEntry":"", 178 | "routineEntry":"", 179 | "appendixEntry":[ 180 | { 181 | "reviewList":"Does your cloud storage provider still provide the most appropriate package for your data or is it time to migrate?", 182 | "tipList": "Advise staff on how to recognise a legitimate automatic backup so that suspicious network activity can be flagged quickly.", 183 | "linksList": "" 184 | } 185 | ] 186 | } 187 | ], 188 | "tips":[ 189 | {"relevance":""}, 190 | {"meaning":"Physical storage includes devices like memory sticks and external hard drives and is preferable when the internet connection is insecure or unreliable, but physically vulnerable to loss or damage. Cloud storage includes services like SpiderOak One, Dropbox and iCloud and are preferable when a secure, reliable internet connection is available, but digitally vulnerable to data breaches and loss. When choosing a cloud storage solution, look for zero- or no-knowledge security and/or one which stores your data in a country with progressive privacy laws. Using a zero-knowledge service can make your backup data inaccessible to the service provider’s employees and, by extension, protects you from intrusion via government access requests."}, 191 | {"implementation":"Backing up to both physical and cloud storage helps to ensure that if one is corrupted or lost then the other remains available to restore devices from."}, 192 | {"more": ["physical storage vs cloud storage", "backing up data", "data backup types", "zero-knowledge cloud storage", "cloud backup vs traditional backup"]} 193 | ] 194 | }, 195 | { 196 | "isQuestion": true, 197 | "id":"q25", 198 | "q":"How often are backups performed?", 199 | "required":true, 200 | "policyEntry":"To ensure we’re able to recover data during a data loss or compromise incident, we perform backups on an [how-often-backups-done] basis.", 201 | "appendixEntry":"Plan to test your backups on a regular basis! In an ideal world you might never have to restore from backup, but the last thing you want to find during an emergency is that your backups are corrupted or incomplete.", 202 | "routineEntry":"", 203 | "answers":[ 204 | { 205 | "type":"radio", 206 | "answerText":"hourly", 207 | "storeAs":"[how-often-backups-done]", 208 | "excludes":[], 209 | "policyEntry":"", 210 | "routineEntry":"", 211 | "appendixEntry":[ 212 | { 213 | "reviewList":"", 214 | "tipList": "", 215 | "linksList": "" 216 | } 217 | ] 218 | }, 219 | { 220 | "type":"radio", 221 | "answerText":"daily", 222 | "storeAs":"[how-often-backups-done]", 223 | "excludes":[], 224 | "policyEntry":"", 225 | "routineEntry":"", 226 | "appendixEntry":[ 227 | { 228 | "reviewList":"Are daily backups still frequent enough or do you need backups throughout the day?", 229 | "tipList": "It may be sensible to time automatic backups for after the majority of the day's work has been done.", 230 | "linksList": "" 231 | } 232 | ] 233 | }, 234 | { 235 | "type":"radio", 236 | "answerText":"weekly", 237 | "storeAs":"[how-often-backups-done]", 238 | "excludes":[], 239 | "policyEntry":"", 240 | "routineEntry":"", 241 | "appendixEntry":[ 242 | { 243 | "reviewList":"Is your data changing more frequently? Could an increase in the backup frequency be appropriate?", 244 | "tipList": "A week can be a long time, so give your staff a way to trigger infrequent manual backups of their work.", 245 | "linksList": "" 246 | } 247 | ] 248 | }, 249 | { 250 | "type":"radio", 251 | "editable":true, 252 | "answerText":"another schedule", 253 | "storeAs":"[how-often-backups-done]", 254 | "excludes":[], 255 | "policyEntry":"", 256 | "routineEntry":"", 257 | "appendixEntry":[ 258 | { 259 | "reviewList":"", 260 | "tipList": "", 261 | "linksList": "" 262 | } 263 | ] 264 | } 265 | ], 266 | "tips":[ 267 | {"relevance":""}, 268 | {"meaning":"Whether automatic or manual, it’s important to back up every device’s data on a regular basis to avoid potential loss due to device confiscation or damage. The frequency of your backups should be dictated by how often your data is created or updated, with hourly or end-of-day being the ideal for organizations that rely heavily on digital documents."}, 269 | {"implementation":"If all staff are on the same operating system then using the built-in software to perform automatic backups can make the implementation process easier. There are great cross-platform alternatives available too, and whichever option you choose it’s advisable to go for a zero-knowledge service wherever possible."}, 270 | {"more": ["how often should I back up my data", "zero-knowledge cloud storage", "cross-platform backup solutions"]} 271 | ] 272 | }, 273 | { 274 | "isQuestion": true, 275 | "id":"q26", 276 | "q":"How does your organization secure its backups?", 277 | "required":false, 278 | "policyEntry":"[organisation-name] requires that devices used for work purposes be backed up as the loss, exposure or corruption of [organisation-name] data puts us, and those we work with, at risk. Backups are managed in the following ways: ", 279 | "appendixEntry":"Be sure to discuss any recent data loss or similar incidents at each review - they are good learning opportunities. An approach to backups is the 3-2-1 rule: at least 3 backup copies of your data on at least 2 different kinds of medium, with at least 1 of these stored offsite.", 280 | "routineEntry":"", 281 | "answers":[ 282 | { 283 | "type":"checkbox", 284 | "answerText":"Backups are encrypted", 285 | "storeAs":"", 286 | "excludes":[], 287 | "policyEntry":"- Backups are encrypted to help protect against unauthorised access.", 288 | "routineEntry":"", 289 | "appendixEntry":[ 290 | { 291 | "reviewList":"", 292 | "tipList": "", 293 | "linksList": "" 294 | } 295 | ] 296 | }, 297 | { 298 | "type":"checkbox", 299 | "answerText":"Backups are tested regularly", 300 | "storeAs":"", 301 | "excludes":[], 302 | "policyEntry":"- Sometimes backup data becomes corrupted, so to minimise the impact of this we test the backups regularly to detect any issues.", 303 | "routineEntry":"", 304 | "appendixEntry":[ 305 | { 306 | "reviewList":"Have there been any recent instances of backup failure that can be learned from?", 307 | "tipList": "If backups occur overnight, always check that they have finished without errors in the morning.", 308 | "linksList": "" 309 | } 310 | ] 311 | }, 312 | { 313 | "type":"checkbox", 314 | "answerText":"Multi-factor authentication", 315 | "storeAs":"", 316 | "excludes":[], 317 | "policyEntry":"- Multi-factor authentication is required to access the backups.", 318 | "routineEntry":"", 319 | "appendixEntry":[ 320 | { 321 | "reviewList":"Do those with access to backups still require it?", 322 | "tipList": "Carefully consider who should be granted access to [organisation-name]'s cloud storage. Too many people and it becomes tougher to manage. Too few and you may struggle to gain access for emergency data restoration.", 323 | "linksList": "" 324 | } 325 | ] 326 | }, 327 | { 328 | "type":"checkbox", 329 | "answerText":"Delayed file deletion", 330 | "storeAs":"", 331 | "excludes":[], 332 | "policyEntry":"- There is a delay in file deletion to minimise accidental destruction.", 333 | "routineEntry":"", 334 | "appendixEntry":[ 335 | { 336 | "reviewList":"", 337 | "tipList": "When setting a file destruction delay on backups, leave a reasonable amount of time for the scheduled deletion to be detected and cancelled.", 338 | "linksList": "" 339 | } 340 | ] 341 | }, 342 | { 343 | "type":"checkbox", 344 | "editable":true, 345 | "answerText":"something else", 346 | "storeAs":"[alt-backup-security]", 347 | "excludes":[], 348 | "policyEntry":"- [alt-backup-security]", 349 | "routineEntry":"", 350 | "appendixEntry":[ 351 | { 352 | "reviewList":"", 353 | "tipList": "", 354 | "linksList": "" 355 | } 356 | ] 357 | } 358 | ], 359 | "tips":[ 360 | {"relevance":""}, 361 | {"meaning":"Backups aren’t much use if they’ve been damaged or corrupted during unauthorised access, so encrypting the files, requiring extra verification for access and preventing instant file deletion are all useful ways of protecting your backed up data. While it’s common to back up files, it’s not common enough to test them regularly to ensure they restore correctly and aren’t corrupted. Checking the integrity of the files can help to avoid a nasty shock when they have to be relied upon during emergency data recovery."}, 362 | {"implementation":""}, 363 | {"more": ["automatic backups vs manual", "how to secure your stored data"]} 364 | ] 365 | }, 366 | { 367 | "isQuestion": true, 368 | "id":"q27", 369 | "q":"What steps should staff take when their devices are seized?", 370 | "required":false, 371 | "policyEntry":"... your device is seized: whether this is at a border, a protest or a raid, it is important to let someone know as soon about the situation as you are able, in order to begin countering any unauthorised access as early as possible. You are advised to follow these steps: [do-this-if-seized]", 372 | "appendixEntry": "Device seizures under any circumstances can be a traumatising event so be sure to offer support to your impacted colleague.", 373 | "routineEntry":"", 374 | "answers":[ 375 | { 376 | "type":"textarea", 377 | "answerText":"", 378 | "placeholder":"Here are a few examples: \n\n- write down exactly what happened, including any officer numbers, times, etc. \n- warn your coworkers and anyone who could now be at risk \n- contact the org’s lawyer", 379 | "storeAs":"[do-this-if-seized]", 380 | "excludes":[], 381 | "policyEntry":"", 382 | "routineEntry":"", 383 | "appendixEntry":[ 384 | { 385 | "reviewList":"", 386 | "tipList": "", 387 | "linksList": "" 388 | } 389 | ] 390 | } 391 | ], 392 | "tips":[ 393 | {"relevance":""}, 394 | {"meaning":"Device seizures involve your device being taken out of your possession by an authority, and it can be hard to tell whether anyone has broken into it when it's returned. Given this uncertainty and confusion, it can be helpful to have a clear process for staff to follow in order to regain some control over the situation."}, 395 | {"implementation":""}, 396 | {"more": ["what to do if your phone is seized by police", "how to secure sim card"]} 397 | ] 398 | } 399 | ] 400 | -------------------------------------------------------------------------------- /assets/js/policy.js: -------------------------------------------------------------------------------- 1 | // File for all the policy-related functions 2 | 3 | // function which takes two boolean values which determine which document is needed 4 | function compileDoc(p,a){ 5 | var doc = { 6 | plain: "", 7 | markdown: "", 8 | html: "" 9 | }; 10 | // create the temporary values 11 | var tempPolicy = []; 12 | var tempGeneralA = []; 13 | var tempReviewA = []; 14 | var tempTipsA = []; 15 | 16 | var contextP = []; 17 | var deviceP = []; 18 | var commsP = []; 19 | var acctsP = []; 20 | var incResP = []; 21 | var travelP = []; 22 | var envP = []; 23 | var networkP = []; 24 | var appContent = { 25 | general: [], 26 | review: [], 27 | tips: [], 28 | links: [] 29 | }; 30 | var routineDoc = []; 31 | 32 | // what is the first q in the answers array? 33 | var prevQ = 0; 34 | 35 | // for each of the answer references 36 | for (var i = 0; i < currentState.answers.length; i++){ 37 | // get quick ref for answers 38 | aRef = currentState.answers[i].a; 39 | // get quick ref for question number 40 | qRef = currentState.answers[i].q; 41 | // set up question name 42 | var thisQ = 'q'+qRef; 43 | // search for the relevant data using the answer reference 44 | for (var j = 0; j < sections.length; j++){ 45 | // store if the data is found 46 | var found = sections[j].find(ans => ans.id === thisQ); 47 | // if there's data 48 | if (found){ 49 | switch (true) { 50 | // questions 0-5 are for context 51 | case qRef < 6: 52 | contextP = getPolicyContent(qRef, prevQ, aRef, contextP, found); 53 | // if we need the appendix and routines too 54 | if (a) { 55 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); // TODO: instead of repeating this is should just be a function call each time 56 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 57 | } 58 | break; 59 | // add case for teaming name & pos @ 9 60 | // questions 6-8 are for devices 61 | case qRef < 9: 62 | deviceP = getPolicyContent(qRef, prevQ, aRef, deviceP, found); 63 | // if we need the appendix and routines too 64 | if (a) { 65 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 66 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 67 | } 68 | break; 69 | // questions 9-12 are for comms 70 | case qRef < 13: 71 | commsP = getPolicyContent(qRef, prevQ, aRef, commsP, found); 72 | // if we need the appendix and routines too 73 | if (a) { 74 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 75 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 76 | } 77 | break; 78 | // question 13 is inc resp 79 | case qRef < 14: 80 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 81 | break; 82 | // questions 14-19 are for accounts 83 | case qRef < 20: 84 | acctsP = getPolicyContent(qRef, prevQ, aRef, acctsP, found); 85 | // if we need the appendix and routines too 86 | if (a) { 87 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 88 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 89 | } 90 | break; 91 | // question 20 is for inc resp 92 | case qRef < 21: 93 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 94 | break; 95 | // add case for inserting Backups heading @ 22 96 | // questions 22-26 are for devices 97 | case qRef < 27: 98 | deviceP = getPolicyContent(qRef, prevQ, aRef, deviceP, found); 99 | // if we need the appendix and routines too 100 | if (a) { 101 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 102 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 103 | } 104 | break; 105 | // question 27 is for inc resp 106 | case qRef < 28: 107 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 108 | break; 109 | // add case for teaming name & pos @ 33 110 | // questions 28-33 are for travel 111 | case qRef < 34: 112 | travelP = getPolicyContent(qRef, prevQ, aRef, travelP, found); 113 | // if we need the appendix and routines too 114 | if (a) { 115 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 116 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 117 | } 118 | break; 119 | // question 34 is for inc resp 120 | case qRef < 35: 121 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 122 | break; 123 | // questions 35-41 are for environmental security 124 | case qRef < 42: 125 | envP = getPolicyContent(qRef, prevQ, aRef, envP, found); 126 | // if we need appendix and routines 127 | if (a){ 128 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 129 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 130 | } 131 | break; 132 | // question 42 is for inc resp 133 | case qRef < 43: 134 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 135 | break; 136 | // questions 43-47 are for network security 137 | case qRef < 48: 138 | networkP = getPolicyContent(qRef, prevQ, aRef, networkP, found); 139 | // if we need appendix and routines 140 | if (a){ 141 | appContent = getAppendixContent(qRef, prevQ, aRef, appContent, found); 142 | routineDoc = getRoutineEntry(qRef, prevQ, aRef, routineDoc, found); 143 | } 144 | break; 145 | // question 48 is for inc resp 146 | case qRef <49: 147 | incResP = getPolicyContent(qRef, prevQ, aRef, incResP, found); 148 | break; 149 | default: 150 | console.log(qRef + ' not found'); 151 | } 152 | } 153 | } 154 | // store this question's ID for comparison in the next loop 155 | prevQ = qRef; 156 | } 157 | 158 | doc.plain = 'Organizational Security Policy\n\nCreated '+dateStamp()+'\n\n'+contextP.join('\n'); 159 | doc.markdown = '# Organizational Security Policy \n#### Created '+dateStamp()+'\n\n'+contextP.join('\n'); 160 | doc.html = 'Organizational Security Policy '+dateStamp()+'

Organizational Security Policy

Created '+dateStamp()+'

'+contextP.join('

')+'

'; 161 | 162 | if (deviceP.length > 0){ 163 | doc.plain += '\n\nDevice Security\n' + deviceP.join('\n'); 164 | doc.markdown += '\n\n### Device Security \n' + deviceP.join('\n'); 165 | doc.html += '

Device Security

' + deviceP.join('

')+'

'; 166 | } 167 | if (commsP.length > 0){ 168 | doc.plain += '\n\nCommunications Security\n' + commsP.join('\n'); 169 | doc.markdown += '\n\n### Communications Security \n' + commsP.join('\n'); 170 | doc.html += '

Communications Security

' + commsP.join('

')+'

'; 171 | } 172 | if (acctsP.length > 0){ 173 | doc.plain += '\n\nAccounts Security\n' + acctsP.join('\n'); 174 | doc.markdown += '\n\n### Accounts Security \n' + acctsP.join('\n'); 175 | doc.html += '

Accounts Security

' + acctsP.join('

')+'

'; 176 | } 177 | if (travelP.length > 0){ 178 | doc.plain += '\n\nTravel Security\n' + travelP.join('\n'); 179 | doc.markdown += '\n\n### Travel Security \n' + travelP.join('\n'); 180 | doc.html += '

Travel Security

' + travelP.join('

')+'

'; 181 | } 182 | if (envP.length > 0){ 183 | doc.plain += '\n\nEnvironmental Security\n' + envP.join('\n'); 184 | doc.markdown += '\n\n### Environmental Security \n' + envP.join('\n'); 185 | doc.html += '

Environmental Security

' + envP.join('

')+'

'; 186 | } 187 | if (networkP.length > 0){ 188 | doc.plain += '\n\nNetwork Security\n' + networkP.join('\n'); 189 | doc.markdown += '\n\n### Network Security \n' + networkP.join('\n'); 190 | doc.html += '

Network Security

' + networkP.join('

')+'

'; 191 | } 192 | if (incResP.length > 0){ 193 | doc.plain += '\n\nWhat to do if...\n' + incResP.join('\n\n'); 194 | doc.markdown += '\n\n### What to do if...\n' + incResP.join('\n\n'); 195 | doc.html += '

What to do if...

' + incResP.join('

')+'

'; 196 | } 197 | 198 | // if appendix is requested, join the policy, appendix and routines arrays together, and add the team-specific policies 199 | if (a) { 200 | doc.plain += '\n\nAppendix\n'; 201 | doc.markdown += '\n\n## Appendix\n'; 202 | doc.html += '

Appendix

'; 203 | if (appContent.general.length > 0){ 204 | doc.plain += '\n\nGeneral Advice\n- ' + appContent.general.join('\n- '); 205 | doc.markdown += '\n\n### General Advice \n\n* ' + appContent.general.join('\n* '); 206 | doc.html += '

General Advice

  • ' + appContent.general.join('
  • ')+'
'; 207 | } 208 | if (appContent.review.length > 0){ 209 | doc.plain += '\n\nReview Checklist\n- ' + appContent.review.join('\n- '); 210 | doc.markdown += '\n\n### Review Checklist \n\n- [ ] ' + appContent.review.join('\n- [ ] '); 211 | doc.html += '

Review Checklist

  1. ' + appContent.review.join('
  2. ')+'
'; 212 | } 213 | if (appContent.tips.length > 0){ 214 | doc.plain += '\n\nImplementation Tips\n- ' + appContent.tips.join('\n- '); 215 | doc.markdown += '\n\n### Implementation Tips \n\n* ' + appContent.tips.join('\n* '); 216 | doc.html += '

Implementation Tips

  • ' + appContent.tips.join('
  • ')+'
'; 217 | } 218 | if (appContent.links.length > 0){ 219 | doc.plain += '\n\nUseful Links \n- ' + appContent.links.join('\n- '); 220 | doc.markdown += '\n\n### Useful Links \n\n* ' + appContent.links.join('\n* '); 221 | doc.html += '

Useful Links

  • ' + appContent.links.join('
  • ')+'
'; 222 | } 223 | if (routineDoc.length > 0){ 224 | doc.plain += '\n\nEveryday practices \n+ ' + routineDoc.join('\n+ '); 225 | doc.markdown += '\n\n## Everyday practices \n\n* ' + routineDoc.join('\n* '); 226 | doc.html += '

Everyday practices

  • ' + routineDoc.join('
  • ')+'
'; 227 | } 228 | } 229 | doc.plain += '\n\nPlease note: it is recommended that this policy undergoes a legal review prior to being implemented in your organization. \n\nBuilt with SOAP v.'+soapv; 230 | doc.markdown += '\n\n#### *Please note: it is recommended that this policy undergoes a legal review prior to being implemented in your organization.* \n\n##### Built with SOAP v.'+soapv; 231 | doc.html += '

Please note: it is recommended that this policy undergoes a legal review prior to being implemented in your organization.

Built with SOAP v. '+soapv+'
'; 232 | 233 | var teamPolicies = []; 234 | // for each of the teams in teamContent 235 | for (var t=0; t 0){ 238 | 239 | // get the area numbers 240 | areas = teamContent[t].areas; 241 | 242 | // add the contextP 243 | pText = '\n\n'+teamContent[t].name + '\n\nOrganizational Security Policy\n\nCreated '+dateStamp()+'\n\n'+contextP.join('\n'); 244 | mText = '\n\n# '+teamContent[t].name + '\n## Organizational Security Policy \n#### Created '+dateStamp()+'\n\n'+contextP.join('\n'); 245 | hText = 'Organizational Security Policy '+dateStamp()+'

'+teamContent[t].name+'

Organizational Security Policy

Created '+dateStamp()+'

'+contextP.join('

')+'

'; 246 | 247 | // for each of the optional areas 248 | for (var a = 0; a< areas.length; a++){ 249 | // add the area title 250 | // add the corresponding policy text in each format 251 | switch (areas[a]) { 252 | case 0: 253 | pText += '\n\nDevice Security\n' + deviceP.join('\n'); 254 | mText += '\n\n### Device Security \n' + deviceP.join('\n'); 255 | hText += '

Device Security

' + deviceP.join('

')+'

'; 256 | break; 257 | case 1: 258 | pText += '\n\nCommunications Security\n' + commsP.join('\n'); 259 | mText += '\n\n### Communications Security \n' + commsP.join('\n'); 260 | hText += '

Communications Security

' + commsP.join('

')+'

'; 261 | break; 262 | case 2: 263 | pText += '\n\nAccounts Security\n' + acctsP.join('\n'); 264 | mText += '\n\n### Accounts Security \n' + acctsP.join('\n'); 265 | hText += '

Accounts Security

' + acctsP.join('

')+'

'; 266 | break; 267 | case 3: 268 | pText += '\n\nTravel Security\n' + travelP.join('\n'); 269 | mText += '\n\n### Travel Security \n' + travelP.join('\n'); 270 | hText += '

Travel Security

' + travelP.join('

')+'

'; 271 | break; 272 | case 4: 273 | pText += '\n\nEnvironmental Security\n' + envP.join('\n'); 274 | mText += '\n\n### Environmental Security \n' + envP.join('\n'); 275 | hText += '

Environmental Security

' + envP.join('

')+'

'; 276 | break; 277 | case 5: 278 | pText += '\n\nNetwork Security\n' + networkP.join('\n'); 279 | mText += '\n\n### Network Security \n' + networkP.join('\n'); 280 | hText += '

Network Security

' + networkP.join('

')+'

'; 281 | break; 282 | default: 283 | console.log(areas[a]); 284 | } 285 | 286 | } 287 | // then add any other default content 288 | if (incResP.length > 0){ 289 | pText += '\n\nWhat to do if...\n' + incResP.join('\n\n'); 290 | mText += '\n\n### What to do if...\n' + incResP.join('\n\n'); 291 | hText += '

What to do if...

' + incResP.join('

')+'

'; 292 | } 293 | // and push the team policies to the array 294 | teamPolicies.push({ 295 | "team":teamContent[t].name, 296 | "plain": pText, 297 | "markdown": mText, 298 | "html": hText, 299 | }); 300 | } 301 | } 302 | for (var tm=0; tm 0){ 471 | thisContent = replaceStr(appEntry.reviewList); 472 | appDoc.review.push(thisContent); 473 | } 474 | if (appEntry.tipList.length > 0){ 475 | thisContent = replaceStr(appEntry.tipList); 476 | appDoc.tips.push(thisContent); 477 | } 478 | if (appEntry.linksList.length > 0){ 479 | thisContent = replaceStr(appEntry.linksList); 480 | appDoc.links.push(thisContent); 481 | } 482 | return appDoc; 483 | } 484 | 485 | function getRoutineEntry(question, previous, answer, routines, content){ 486 | // if it's a new question and there's a general routine entry 487 | if ((question !== previous) && (content.routineEntry !== "")) { 488 | // edit the entry and push it to the doc 489 | thisContent = replaceStr(content.routineEntry); 490 | routines.push(thisContent); 491 | } 492 | // if the answer has a specific routine entry 493 | if (content.answers[answer].routineEntry !== ""){ 494 | // edit that entry and push it to the doc 495 | thisContent = replaceStr(content.answers[answer].routineEntry); 496 | routines.push(thisContent); 497 | } 498 | return routines; 499 | } 500 | 501 | function resetChanges(){ 502 | // could also use textContent instead of output here 503 | document.querySelector('.policyHolder').value = output.plain; 504 | } 505 | -------------------------------------------------------------------------------- /assets/css/styles.css: -------------------------------------------------------------------------------- 1 | /* Colors 2 | Oxford blue 3 | #1b263b 4 | rgb(27,38,59) 5 | 6 | Sapphire blue 7 | #066493 8 | rgb(6,100,147) 9 | 10 | Alabaster 11 | #F2EEE2 12 | rgb(242,238,226) 13 | 14 | Light Steel Blue 15 | #A9BCD0 16 | rgb(169,188,208) 17 | 18 | Amaranth 19 | #893168 20 | rgb(137,49,104) 21 | */ 22 | 23 | /* Structural styles */ 24 | html, body { 25 | height:100%; 26 | } 27 | /* TODO: fix so it applies just to external links 28 | a[href]:visited { 29 | color:inherit; 30 | } */ 31 | a:hover { 32 | text-decoration: underline; 33 | } 34 | a.btn:hover { 35 | text-decoration: none; 36 | } 37 | 38 | :focus { 39 | border-style: solid; 40 | border-width: 0.5px; 41 | } 42 | 43 | 44 | @font-face { 45 | font-family: 'SinkinSansLight'; 46 | src: url('webfonts/SinkinSans-300Light.otf'); 47 | } 48 | @font-face { 49 | font-family: 'SinkinSansMedium'; 50 | src: url('webfonts/SinkinSans-500Medium.otf'); 51 | } 52 | @font-face { 53 | font-family: 'SinkinSansMediumItalic'; 54 | src: url('webfonts/SinkinSans-500MediumItalic.otf'); 55 | } 56 | @font-face { 57 | font-family: 'SinkinSansBold'; 58 | src: url('webfonts/SinkinSans-700Bold.otf'); 59 | } 60 | body { 61 | font-family: "SinkinSansMedium", "Helvetica", "OpenSans", sans-serif; 62 | margin: 0; 63 | font-size: 16px; 64 | color: #066493; 65 | line-height: 1.75em; 66 | display: flex; 67 | flex-direction: column; 68 | overflow: scroll; 69 | overflow-x:hidden; 70 | background: #1b263b; 71 | } 72 | 73 | p { 74 | font-family: "SinkinSansLight", "Helvetica", "OpenSans", sans-serif; 75 | } 76 | 77 | #head { 78 | position: fixed; 79 | width:100%; 80 | display: flex; 81 | padding: 5px 4em 0 4em; 82 | box-sizing: border-box; 83 | background: rgba(27,38,59,0.9); 84 | align-items: center; 85 | z-index: 1; 86 | } 87 | #head a { 88 | margin:0.5em 0; 89 | } 90 | 91 | #img-intro { 92 | width: 75%; 93 | border: 1px solid white; 94 | margin: 2em 4em 0; 95 | filter: drop-shadow(0px 0px 3px #eee); 96 | } 97 | .nav-scroll { 98 | color: #A9BCD0; 99 | } 100 | body.buildPage #head.nav-start { 101 | opacity:100; 102 | } 103 | body.buildPage #page { 104 | padding-top:4em; 105 | background: linear-gradient(to right bottom, #1b263b, #066493); 106 | } 107 | .nav-start { 108 | background-color: transparent; 109 | color: #f2eee2; 110 | } 111 | #head.nav-start { 112 | opacity:0; 113 | } 114 | .nav-scroll #logo{ 115 | filter:brightness(0.9); 116 | } 117 | 118 | #page { 119 | flex: 1 0 auto; 120 | } 121 | .editMode { 122 | border: 3em #a9bcd0 solid; 123 | border-top: 0; 124 | border-bottom: 0; 125 | } 126 | .editMode .questionContent { 127 | margin-bottom: 3em; 128 | } 129 | 130 | #editBtn.editMode { 131 | border-width: 0.5em; 132 | } 133 | #editBtn { 134 | color: #066493; 135 | cursor: pointer; 136 | } 137 | .prompt { 138 | cursor: pointer; 139 | } 140 | .prompt:hover, .prompt:focus { 141 | text-decoration: underline; 142 | padding:5px 20px; 143 | } 144 | 145 | .nav-scroll #editBtn { 146 | color:#fff; 147 | } 148 | 149 | #foot { 150 | flex-shrink: 0; 151 | background: #1b263b; 152 | } 153 | .wrapper { 154 | width: 75%; 155 | max-width:1000px; 156 | margin: 0 auto; 157 | } 158 | 159 | #head h1 { 160 | display:inline-block; 161 | } 162 | 163 | #head h2 { 164 | font-size: 2rem; 165 | display:inline-block; 166 | padding-left: 10px; 167 | } 168 | 169 | #logoBig { 170 | max-height: 110px; 171 | } 172 | #logoContainer { 173 | display: flex; 174 | justify-content: flex-start; 175 | align-items: center; 176 | } 177 | #logoContainer > div { 178 | margin-right: 4em; 179 | } 180 | .text-img-wrap { 181 | display:flex; 182 | flex-direction: row; 183 | } 184 | .text-buttons-wrap { 185 | margin: 2em 4em 2em 0; 186 | max-width: 25%; 187 | } 188 | #feedback .text-buttons-wrap { 189 | display: flex; 190 | flex-wrap: wrap; 191 | justify-content: space-evenly; 192 | max-width: unset; 193 | } 194 | .links { 195 | list-style: none; 196 | margin-left: auto; 197 | cursor: pointer; 198 | } 199 | 200 | .links a, .links a:visited, .prompt { 201 | padding: 5px; 202 | border:2px solid; 203 | border-radius: 5%; 204 | border-color: transparent; 205 | color:inherit; 206 | } 207 | 208 | .links a:hover, .links a.active, .prompt:hover, .prompt:active { 209 | border-color: #066493; 210 | } 211 | .nav-scroll .links a:hover, .nav-scroll .links a.active { 212 | border-color: #066493; 213 | } 214 | 215 | .links li { 216 | display:inline; 217 | padding: 0 20px; 218 | } 219 | 220 | .links li:first-child { 221 | padding-left: 0; 222 | } 223 | .links li:last-child { 224 | background-color: #f2eee2; 225 | font-weight: bold; 226 | padding:1em; 227 | white-space: nowrap; 228 | } 229 | .nav-start .links li:last-child a{ 230 | color:#066493; 231 | } 232 | 233 | .nav-scroll .links li:last-child { 234 | background-color: #066493; 235 | color: #fff; 236 | } 237 | 238 | #foot li { 239 | font-size: 0.8rem; 240 | } 241 | 242 | /* Button styles */ 243 | 244 | /* Actual checkboxes set to display:none. Styling the labels and then simply checking if the checkboxes are checked to show and hide the nav. */ 245 | #menu-check { 246 | display: none; 247 | } 248 | 249 | #menu-collapse { 250 | display: none; 251 | } 252 | 253 | /* Style the checkbox labels.*/ 254 | #expand { 255 | cursor: pointer; 256 | } 257 | 258 | #collapse { 259 | cursor: pointer; 260 | display: none; 261 | } 262 | 263 | /* If menu-check is checked, set the collapse icon to display... */ 264 | #menu-check:checked ~ #collapse { 265 | display: block; 266 | } 267 | 268 | /* ...and if menu-check is checked, then set this icon to disappear */ 269 | #menu-check:checked ~ #expand { 270 | display: none; 271 | } 272 | 273 | /* ...and if collapse is checked, set expand to display again */ 274 | #menu-collapse:checked ~ #expand { 275 | display: block; 276 | } 277 | 278 | /* Show and hide the nav */ 279 | #menu-check:checked ~ ul { 280 | display: flex; 281 | justify-content: space-around; 282 | } 283 | 284 | #menu-collapse:checked ~ ul { 285 | display: none; 286 | } 287 | 288 | /* ================== */ 289 | /* Content styles */ 290 | 291 | a { 292 | text-decoration: none; 293 | } 294 | 295 | .questionContent { 296 | display:none; 297 | } 298 | .questionContent.current { 299 | display:block; 300 | } 301 | 302 | .questionContent h2 { 303 | width:85%; 304 | } 305 | .questionContent > h1, .questionContent > p { 306 | color: #A9bcd0; 307 | } 308 | 309 | .questionContent > .btn-tert { 310 | min-width:3em; 311 | } 312 | 313 | .questionContent summary { 314 | width: auto; 315 | color: #f2eee2; 316 | background: #1b263b; 317 | padding: 1em; 318 | cursor: pointer; 319 | } 320 | 321 | #secretLink { 322 | color:#fff; 323 | } 324 | 325 | .qContainer { 326 | margin-top: -4em; 327 | padding: 2em 4em; 328 | } 329 | .buildPage .qContainer { 330 | margin-top:0; 331 | } 332 | 333 | #q0 .text-img-wrap { 334 | display:flex; 335 | flex-wrap: wrap; 336 | padding: 0; 337 | } 338 | 339 | #q0 img { 340 | width:100%; 341 | border-radius: 5%; 342 | } 343 | 344 | #q0 section { 345 | flex: 2 2 20%; 346 | margin: 3em 2em; 347 | background:#f2eee2; 348 | } 349 | 350 | #q0 section p { 351 | padding: 0.5em 2em; 352 | } 353 | 354 | #q0 h1 { 355 | font-size: 1.6em; 356 | } 357 | #q0 h2 { 358 | font-size: 1.2em; 359 | } 360 | 361 | /* ================== */ 362 | /* Info panel styles */ 363 | .info-display { 364 | border-top: 1px solid rgba(143,197,234,0.5); 365 | } 366 | 367 | .question-panel { 368 | border: 1px solid currentColor; 369 | padding: 1em; 370 | background-color: #f2eee2; 371 | display:flex; 372 | justify-content: space-between; 373 | flex-wrap: wrap; 374 | } 375 | 376 | .question-panel i { 377 | font-size: 2em; 378 | align-self: center; 379 | opacity: 0.5; 380 | } 381 | 382 | .question-panel i:hover, .question-panel i.highlight { 383 | opacity: 1; 384 | } 385 | 386 | .question-panel i.highlight{ 387 | transform: rotate(45deg); 388 | } 389 | 390 | .question-panel div{ 391 | display: flex; 392 | flex-direction: row; 393 | align-items: center; 394 | } 395 | 396 | .question-panel span{ 397 | font-size: 2em; 398 | padding-right: 0.5em; 399 | min-width: 30px; 400 | } 401 | 402 | /* ================== */ 403 | /* Preview styles */ 404 | 405 | #previewPolicy:disabled, #editBtn.disabled { 406 | display:none; 407 | } 408 | #submitAnswers { 409 | margin-right:0; 410 | } 411 | 412 | #submitAnswers:disabled { 413 | cursor: not-allowed; 414 | opacity:75%; 415 | background-color: #888685; 416 | border:#ccc; 417 | } 418 | 419 | .modal { 420 | position: fixed; 421 | top: 50%; 422 | left: 50%; 423 | transform: translate(-50%, -50%); 424 | width: 75%; 425 | max-width: 95%; 426 | height: 75%; 427 | max-height: 95%; 428 | z-index: 10; 429 | display: flex; 430 | } 431 | .closed { 432 | display: none; 433 | } 434 | 435 | .feature ul { 436 | padding: 0 0 0 10px; 437 | } 438 | 439 | .xIcon { 440 | z-index: 1; 441 | position: absolute; 442 | right: -1.6em; 443 | background-color: initial; 444 | color:#668EAA; 445 | border: none; 446 | padding: 0.5em 0.75em; 447 | top: -1.3em; 448 | cursor: pointer; 449 | font-size:1.2em; 450 | opacity: 0.8; 451 | } 452 | .xIcon .fas { 453 | transform: rotate(45deg); 454 | font-size: 2em; 455 | background-color: #fff; 456 | border-radius: 50%; 457 | } 458 | .xIcon:focus, .xIcon:hover { 459 | border: 0; 460 | box-shadow: none; 461 | opacity: 1; 462 | } 463 | 464 | #preview h4 { 465 | border-top:1px solid; 466 | padding-top:0.5em; 467 | } 468 | 469 | .modalScrollbox { 470 | position: absolute; 471 | top: 0; 472 | left: 0; 473 | width: 100%; 474 | height: 100%; 475 | padding: 20px 50px 20px 20px; 476 | overflow: auto; 477 | background-color: #f2eee2; 478 | box-sizing: border-box; 479 | text-align: justify; 480 | } 481 | 482 | .modalOverlay { 483 | z-index: 9; 484 | position: fixed; 485 | top: 0; 486 | left: 0; 487 | width: 100%; 488 | height: 100%; 489 | background-color: rgba(0,0,0,0.5); 490 | } 491 | 492 | .homeSectionOverview{ 493 | padding: 0 0 2em 3em; 494 | font-size: 1em; 495 | font-family: "SinkinSansLight", "Helvetica", "OpenSans", sans-serif; 496 | width:100%; 497 | } 498 | .homeSectionOverview li { 499 | padding: 0.5em 0; 500 | } 501 | .homeSectionOverview ol, .homeSectionOverview ul { 502 | margin-left: 1em; 503 | padding: 0; 504 | } 505 | /* .window details { 506 | margin-top: 1em; 507 | font-size: initial; 508 | } */ 509 | .window summary { 510 | background: rgba(27,38,59,0.75); 511 | color: #f2eee2; 512 | padding: 1em; 513 | cursor: pointer; 514 | } 515 | details[open=""] summary { 516 | opacity: 0.5; 517 | } 518 | details > h3, details > p { 519 | padding: 0 1em; 520 | } 521 | details a { 522 | color:#b93168; 523 | } 524 | .questionContent details h3{ 525 | text-transform: uppercase; 526 | background: #1b263b; 527 | color: #f2eee2; 528 | padding: 1em; 529 | } 530 | #overview { 531 | display: flex; 532 | justify-content: space-evenly; 533 | } 534 | 535 | #overview section { 536 | width: 20%; 537 | display: flex; 538 | flex-direction: column; 539 | justify-content: space-between; 540 | } 541 | 542 | #overview img { 543 | width:100%; 544 | filter:drop-shadow(0px 0px 3px #eee); 545 | } 546 | 547 | 548 | .nav-start p a { 549 | color: #fff; 550 | font-weight: bold; 551 | } 552 | 553 | #intro { 554 | background: linear-gradient(to right bottom, #1b263b, #066493, #b93168); 555 | padding: 5em 4em 8em; 556 | } 557 | 558 | #intro h1, #intro h2 { 559 | text-align: left; 560 | } 561 | 562 | #intro h1 { 563 | font-size: 2.4em; 564 | line-height: 1.5em; 565 | margin: 2em 0; 566 | width:80%; 567 | } 568 | 569 | #intro h2 { 570 | font-weight: lighter; 571 | font-size: 1em; 572 | text-align: center; 573 | padding-bottom: 1em; 574 | } 575 | 576 | #intro input { 577 | background: #f2eee2; 578 | background:transparent; 579 | margin: 0 auto; 580 | } 581 | 582 | #intro .btn { 583 | margin-top:3em; 584 | } 585 | 586 | #intro .wrap-c { 587 | align-items: center; 588 | } 589 | 590 | .window { 591 | margin:4em 0; 592 | padding:2em; 593 | background: #f2eee2; 594 | display:flex; 595 | flex-direction: row; 596 | } 597 | 598 | .window:first-child { 599 | margin-top: 0; 600 | } 601 | #page > .window ~ .window { 602 | margin-top: 4em; 603 | } 604 | 605 | .window h3 { 606 | font-family: 'SinkinSansBold'; 607 | text-transform: uppercase; 608 | font-size: 5em; 609 | line-height: 1.5em; 610 | margin: 0; 611 | background: #1b263b; 612 | color: #f2eee2; 613 | flex: 0 0 40%; 614 | } 615 | .dlBtnWrapper { 616 | display: flex; 617 | flex-direction: column; 618 | } 619 | .dlBtnWrapper > div { 620 | justify-content: space-evenly; 621 | display: flex; 622 | margin-top: 1em; 623 | } 624 | .buildPage .window { 625 | margin:4em auto; 626 | padding: 2em; 627 | } 628 | .buildPage .window > :not(h3) { 629 | padding: 0 2em; 630 | } 631 | 632 | #policy-dl i{ 633 | padding-left: 5px; 634 | } 635 | .buildPage .window div { 636 | display: flex; 637 | flex-direction: column; 638 | width:100% 639 | } 640 | .feature { 641 | width: 30%; 642 | border: 1px solid #8FC5EA; 643 | color: #066493; 644 | box-shadow: 0px 4px 5px #eee; 645 | margin: 1em; 646 | } 647 | 648 | .feat-wrap { 649 | padding: 2em; 650 | } 651 | 652 | .feature i { 653 | font-size: 6em; 654 | text-align: center; 655 | color: #8FC5EA; 656 | display: block; 657 | background-color: #066392; 658 | padding: 10px; 659 | } 660 | .btn { 661 | border:2px solid; 662 | padding: 0.75em; 663 | font-family: SinkinSansMedium; 664 | margin: 10px; 665 | min-width:10em; 666 | text-align: center; 667 | cursor: pointer; 668 | font-size: 1em; 669 | } 670 | 671 | .btn-prim { 672 | background-color: #b93168; 673 | border-color: #b93168; 674 | color:#f2eee2; 675 | font-weight: bold; 676 | } 677 | .btn-prim:hover, .btn-seco:hover, #intro .btn-seco:hover{ 678 | background-color: #A9BCD0; 679 | color: #1b263b; 680 | } 681 | .btn-seco { 682 | color: #066493; 683 | border-color: #066493; 684 | background: #f2eee2; 685 | } 686 | .btn-tert { 687 | border: 0; 688 | color: #fff; 689 | background-color: #066493; 690 | min-width: 0; 691 | border:1px solid #fff; 692 | } 693 | 694 | .btn-tert:hover { 695 | background-color: #8FC5EA; 696 | filter: drop-shadow(0px 0px 3px #fff); 697 | } 698 | 699 | .btn-tert.highlight { 700 | background-color: #8FC5EA; 701 | filter: drop-shadow(0px 0px 3px #fff); 702 | } 703 | 704 | .btn-wrap { 705 | display:flex; 706 | padding: 2em 0 0 1em; 707 | } 708 | 709 | .wrap-c { 710 | flex-direction: column; 711 | align-items: flex-start; 712 | } 713 | 714 | .wrap-r { 715 | justify-content: space-between; 716 | margin-bottom:1em; 717 | } 718 | progress { 719 | width:100%; 720 | opacity: 0.5; 721 | border: 1px solid #B93168; 722 | background: #1b263b; 723 | border: 0; 724 | display: block; 725 | } 726 | progress[value="0"] { 727 | display:none; 728 | } 729 | .policyHolder { 730 | width:100%; 731 | display:inline-block; 732 | min-height:300px; 733 | border: none; 734 | padding: 1em 2em; 735 | box-sizing: border-box; 736 | text-align: left; 737 | line-height: 1.8em; 738 | color:#1b263b; 739 | } 740 | small a { 741 | color:#a9bcd0; 742 | text-decoration: underline; 743 | } 744 | small { 745 | padding: 1em 0; 746 | color:#f2eee2; 747 | display:block; 748 | } 749 | .incidentBox { 750 | text-align: left; 751 | padding:2em; 752 | width: 95%; 753 | min-height: 300px; 754 | } 755 | 756 | .answers-container { 757 | display:flex; 758 | justify-content: space-evenly; 759 | flex-wrap: wrap; 760 | } 761 | 762 | .answers-container > * { 763 | flex: 1 28%; 764 | } 765 | 766 | .form-el { 767 | line-height: 3em; 768 | min-width: 200px; 769 | text-align: center; 770 | margin:2em; 771 | } 772 | 773 | .form-el label { 774 | display:inline; 775 | font-size: 1.1em; 776 | line-height: 2em; 777 | color: #f2eee2; 778 | } 779 | .form-el label[contenteditable="true"] { 780 | border-style: dotted; 781 | border-width:4px; 782 | cursor: text; 783 | } 784 | 785 | .btn-edit { 786 | cursor: text; 787 | } 788 | 789 | input[type=text], textarea{ 790 | display:block; 791 | padding: 2em; 792 | text-align: center; 793 | color: #A9BCD0; 794 | background: transparent; 795 | border:none; 796 | border-bottom-style: dotted; 797 | font-family: SinkinSansMedium; 798 | font-size: 1em; 799 | } 800 | 801 | input[type=text]{ 802 | margin: 0 auto; 803 | } 804 | 805 | form input[type='radio'], 806 | form input[type='checkbox'] { 807 | /* Hide the input, but have it still be clickable */ 808 | opacity: 0; 809 | position: absolute; 810 | z-index: 10; 811 | } 812 | 813 | form input[type='radio'] + label, 814 | form input[type='checkbox'] + label { 815 | /* Make look clickable because they are */ 816 | cursor: pointer; 817 | padding:1em; 818 | display:block; 819 | border: 2px solid #b93168; 820 | color: #a9bcd0; 821 | font-family: SinkinSansMedium; 822 | } 823 | 824 | form input[type='radio'] + label{ 825 | border-radius: 2em; 826 | } 827 | form input[type='checkbox'] + label { 828 | border-radius: 0.5em; 829 | } 830 | 831 | form input:checked + label { 832 | background-color: #a9bcd0; 833 | color: #1b263b; 834 | box-shadow: 0px 0px 1em #b93168; 835 | border-color: #b93168; 836 | } 837 | .form-el input + label:hover { 838 | opacity:0.6; 839 | } 840 | 841 | 842 | .q0-only { 843 | font-size: 1em; 844 | color: #fff; 845 | background-color: #7cadcf; 846 | padding: 1em 0; 847 | text-align: center; 848 | margin:0 10px; 849 | } 850 | span.q0-only { 851 | background-color: initial; 852 | color: inherit; 853 | } 854 | .q0-only h4, .q0-only p{ 855 | margin:0; 856 | } 857 | 858 | #bgContainer { 859 | padding:0; 860 | } 861 | #bgContainer section { 862 | padding:2em 6em; 863 | display:flex; 864 | align-items: center; 865 | } 866 | 867 | #bgContainer .text-img-wrap { 868 | flex-direction: column; 869 | flex: 1 1 90%; 870 | } 871 | #bgContainer section > div > p { 872 | flex: 1 2 52%; 873 | } 874 | #bgContainer img { 875 | border-radius: 5px; 876 | height:100%; 877 | max-width:500px; 878 | } 879 | #bgContainer .nav-start img { 880 | filter:drop-shadow(0px 0px 3px #eee); 881 | } 882 | #bgContainer .nav-scroll img { 883 | filter:drop-shadow(0px 0px 3px currentColor); 884 | } 885 | /* #bgContainer .trio img { 886 | margin:1em; 887 | width:100%; 888 | } */ 889 | .border-white-round { 890 | border: 2px solid #fff; 891 | padding: 1em 2em; 892 | border-radius: 1.5em; 893 | } 894 | .trio-wrap { 895 | display:flex; 896 | justify-content: space-evenly; 897 | } 898 | .dbl { 899 | flex-direction: row; 900 | } 901 | .dbl.reverse { 902 | flex-direction: row-reverse; 903 | } 904 | 905 | 906 | .grid { 907 | display:flex; 908 | flex-direction: column; 909 | align-items: center; 910 | flex:2 0 15%; 911 | margin-left:4em; 912 | } 913 | .grid img { 914 | margin: 0 915 | flex: 1 2 50%; 916 | } 917 | 918 | .trio p { 919 | flex: 2 0 60%; 920 | } 921 | .trio h1 { 922 | flex: 1 0 15%; 923 | } 924 | .dbl img { 925 | margin-right: 2em; 926 | margin-left: 0; 927 | flex: 1 1 40%; 928 | } 929 | .dbl.reverse img { 930 | margin-left: 2em; 931 | margin-right: 0; 932 | } 933 | 934 | .spacer { 935 | flex-grow:100; 936 | } 937 | .showAllQs { 938 | display:block; 939 | } 940 | #home-pic { 941 | flex: 1 1 60%; 942 | margin: 1em; 943 | } 944 | #img-intro { 945 | width:100%; 946 | margin:0; 947 | } 948 | 949 | .nav-start .btn.btn-seco { 950 | color: #fff; 951 | background-color: #066493; 952 | } 953 | 954 | .nav-start .btn.btn-seco:hover, .nav-start .btn.btn-tert:hover { 955 | color: #066392; 956 | background-color: #fff; 957 | } 958 | 959 | textarea { 960 | border-style: dotted; 961 | } 962 | textarea[placeholder] { 963 | font-size: 1em; 964 | line-height: 1.4em; 965 | } 966 | 967 | /* styles for highlighting current element in focus */ 968 | .pink-border-glow, 969 | :focus, 970 | form input[type="radio"]:focus + label, 971 | form input[type="checkbox"]:focus + label, 972 | input[type="text"]:focus, 973 | textarea:focus, 974 | #homeLink:focus > img, 975 | .modalScrollbox { 976 | box-shadow: 0px 0px 1em #b93168; 977 | border-color: #b93168; 978 | } 979 | 980 | /* reset style on logo link */ 981 | #homeLink:focus { 982 | box-shadow: initial; 983 | border: initial; 984 | outline: none; 985 | } 986 | button::-moz-focus-inner { 987 | outline: none; 988 | border: none; 989 | } 990 | 991 | summary h2 { 992 | display: inline; 993 | line-height: 2em; 994 | } 995 | summary::marker { 996 | font-size: 1.5em; 997 | } 998 | #no-js.window { 999 | flex-direction: column; 1000 | max-width: 1600px; 1001 | margin-left: auto; 1002 | margin-right: auto; 1003 | } 1004 | .contain > * { 1005 | max-width: 1600px; 1006 | margin-left: auto; 1007 | margin-right: auto; 1008 | } 1009 | .header-menu{ 1010 | margin:0; 1011 | } 1012 | #spinner { 1013 | display: none; 1014 | position: fixed; 1015 | bottom: 0; 1016 | left: 50%; 1017 | z-index: 10; 1018 | background: #893168; 1019 | padding:1em; 1020 | } 1021 | #spinner img { 1022 | max-width: 100px; 1023 | } 1024 | #spinner p{ 1025 | text-align: center; 1026 | color:#f2eee2; 1027 | } 1028 | #spinner.loading{ 1029 | display: block; 1030 | } 1031 | #snapshotPolicy:disabled, #snapshotPolicy:disabled + div { 1032 | display: none; 1033 | } 1034 | #snapshotGroup.hidden{ 1035 | display:none !important; 1036 | } 1037 | #snapshotLink { 1038 | padding:1em; 1039 | margin: 10px; 1040 | border-style: dotted; 1041 | text-align: left; 1042 | margin-left: 0; 1043 | } 1044 | .fa-copy{ 1045 | color:#f2eee2; 1046 | font-size: 2em; 1047 | cursor: pointer; 1048 | opacity: 0.8; 1049 | } 1050 | .fa-copy:hover { 1051 | opacity: 1; 1052 | } 1053 | #snapshotGroup{ 1054 | display: flex; 1055 | flex-direction: column; 1056 | color: #f2eee2; 1057 | font-size:0.8em; 1058 | } 1059 | #snapshotGroup div { 1060 | display: flex; 1061 | align-items: center; 1062 | } 1063 | .btn-wrap.wrap-r p { 1064 | color:#a9bcd0; 1065 | } 1066 | #teamContentCols{ 1067 | display:flex; 1068 | justify-content: space-around; 1069 | } 1070 | #teams { 1071 | text-align: center; 1072 | } 1073 | #expectedOutput{ 1074 | color: #F2EEE2; 1075 | margin-top: 4em; 1076 | } 1077 | label span { 1078 | display: block; 1079 | font-size: 0.8em; 1080 | font-family: "SinkinSansMediumItalic"; 1081 | } 1082 | --------------------------------------------------------------------------------