├── frontend ├── src │ ├── App.css │ ├── react-app-env.d.ts │ ├── constants.ts │ ├── Assessment.tsx │ ├── App.test.tsx │ ├── index.css │ ├── utils │ │ ├── prepareProjectMeta.ts │ │ ├── getSelectedRisks.ts │ │ ├── handleRiskAnswer.ts │ │ ├── index.ts │ │ ├── pickCategoriesWithResponse.ts │ │ └── moduleHelpers.ts │ ├── index.tsx │ ├── components │ │ ├── RiskAssessment.tsx │ │ ├── HelpDialog.tsx │ │ ├── PaginationButtons.tsx │ │ ├── Header.tsx │ │ ├── steps │ │ │ ├── RiskAssessment.tsx │ │ │ ├── ProjectMetaGathering.tsx │ │ │ ├── ModuleSelection.tsx │ │ │ └── Summary.tsx │ │ ├── FormSteps.tsx │ │ ├── RiskCriteria.tsx │ │ ├── Tooling.tsx │ │ ├── Module.tsx │ │ └── Checklists.tsx │ ├── help.ts │ ├── context │ │ ├── index.ts │ │ └── StepContext.tsx │ ├── SearchChecklists.tsx │ ├── Home.tsx │ ├── types │ │ └── index.ts │ ├── Faq.tsx │ ├── styles │ │ └── index.ts │ ├── listo_pink.svg │ ├── QuickChecklist.tsx │ ├── Project.tsx │ └── App.tsx ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── Makefile ├── tsconfig.json ├── generate-schema.ts └── package.json ├── logo.png ├── Makefile ├── server ├── src │ ├── config.ts │ ├── types.ts │ ├── app.ts │ ├── slack.ts │ ├── diskdb.ts │ ├── dynamodb.ts │ ├── data.ts │ ├── tests.ts │ ├── appFactory.ts │ └── trello.ts ├── tsconfig.json ├── tsconfig.local.json ├── Makefile └── package.json ├── .prettierrc ├── data ├── projectTypes.yml ├── modules │ ├── service_provider │ │ ├── datacentre.yml │ │ └── aws.yml │ ├── data │ │ ├── object_store.yml │ │ ├── rds.yml │ │ ├── nosql.yml │ │ └── general.yml │ ├── general │ │ ├── abuse.yml │ │ ├── services.yml │ │ ├── telemetry.yml │ │ ├── general.yml │ │ └── threat_modeling.yml │ ├── code │ │ ├── internal_libs.yml │ │ ├── open_source.yml │ │ ├── xml.yml │ │ ├── authorisation.yml │ │ ├── authentication.yml │ │ ├── third_party_libs.yml │ │ ├── xss.yml │ │ ├── urls.yml │ │ └── csrf.yml │ ├── software_env │ │ ├── servers.yml │ │ └── containers.yml │ └── test │ │ └── test_long_checklist.yml ├── projectMeta.yml ├── risks.yml └── tooling.yml ├── Dockerfile.web ├── examples ├── deploy │ ├── Dockerfile │ ├── ecr.yaml │ ├── heroku_deploy.yml │ ├── Makefile │ ├── README.md │ └── stack.yaml ├── marketing.md ├── TEMPLATE_env.sh ├── pitch.md └── rfc_listo.md ├── .gitignore ├── .dockerignore ├── docker-compose.yml ├── Dockerfile ├── LICENSE.txt ├── .github └── workflows │ └── prod.yml └── README.md /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seek-oss/listo/HEAD/logo.png -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | serve: 2 | source env.sh; docker-compose up --build 3 | 4 | .PHONY: serve 5 | -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | export const region = process.env.AWS_REGION || 'ap-southeast-2'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seek-oss/listo/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /data/projectTypes.yml: -------------------------------------------------------------------------------- 1 | - name: api 2 | modules: 3 | - "code/authentication" 4 | - name: frontend 5 | - name: worker -------------------------------------------------------------------------------- /frontend/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = 2 | process.env.REACT_APP_API_URL || 'http://localhost:8000/api'; 3 | -------------------------------------------------------------------------------- /Dockerfile.web: -------------------------------------------------------------------------------- 1 | FROM listoproject/listo:latest 2 | ADD data /etc/listo/data 3 | ENV DATA_DIR=/etc/listo/data 4 | ENV DISK_PATH=/opt/listo/db.json 5 | -------------------------------------------------------------------------------- /examples/deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM listoproject/listo:latest 2 | ADD data /etc/listo/data 3 | ENV DATA_DIR=/etc/listo/data 4 | ENV DISK_PATH=/opt/listo/db.json 5 | -------------------------------------------------------------------------------- /frontend/src/Assessment.tsx: -------------------------------------------------------------------------------- 1 | import FormSteps from './components/FormSteps'; 2 | import { RouteComponentProps } from '@reach/router'; 3 | import React from 'react'; 4 | 5 | export const Assessment = (_: RouteComponentProps) => ; 6 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "outDir": "build", 6 | "sourceMap": true 7 | }, 8 | "include": [ 9 | "src/**/*.ts" 10 | ], 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env.sh 2 | db.json 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # production 8 | /build 9 | server/build 10 | frontend/build 11 | frontend/data-schema.json 12 | 13 | # misc 14 | .vscode 15 | .DS_Store 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /data/modules/service_provider/datacentre.yml: -------------------------------------------------------------------------------- 1 | category: Service Provider 2 | assessmentQuestion: Will you be deploying your application outside of our AWS Org (i.e. Datacentre)? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We thought about it, and decided to deploy into AWS instead. 7 | tags: WebApp 8 | title: Datacentre -------------------------------------------------------------------------------- /data/modules/data/object_store.yml: -------------------------------------------------------------------------------- 1 | category: Data 2 | assessmentQuestion: We will be using data in simple object store (i.e. S3)? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | S3: 6 | - question: If our bucket is publicly accessible, we have tagged it according to the [Public S3 Bucket Tagging Policy RFC](). 7 | tags: WebApp, API, Worker 8 | title: Object Storage 9 | -------------------------------------------------------------------------------- /server/tsconfig.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017"], 4 | "moduleResolution": "node", 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "sourceMap": true, 8 | "target": "es2017", 9 | "outDir": "build", 10 | "resolveJsonModule": true, 11 | }, 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | yarn install 3 | 4 | prettier: 5 | yarn run format 6 | 7 | build: install 8 | REACT_APP_API_URL=/_lambda NODE_ENV=production yarn run build 9 | 10 | serve: install 11 | source ../env.sh ; yarn run start 12 | 13 | data-schema.json: install 14 | yarn run build:schema 15 | 16 | .PHONY: install build deploy serve prettier 17 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Listo", 3 | "name": "Listo", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /data/modules/general/abuse.yml: -------------------------------------------------------------------------------- 1 | category: General 2 | assessmentQuestion: We have users or customers that could abuse the functionallity of our project? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We assume customer accounts will be broken into and have solutions in place to minimise the user impact. 7 | tags: WebApp, API, Worker, iOS, Android 8 | title: Abuse 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/charts 16 | **/docker-compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | README.md -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | listo: 5 | image: listo 6 | build: . 7 | environment: 8 | - DATA_DIR=/etc/listo/data 9 | - DISK_PATH=/opt/listo/db.json 10 | - TRELLO_API_KEY 11 | - TRELLO_TOKEN 12 | - TRELLO_TEAM 13 | - TRELLO_BOARD_LINK 14 | - SLACK_CHANNEL_LINK 15 | - SLACK_TARGET_CHANNEL 16 | - SLACK_WEB_HOOK 17 | volumes: 18 | - ./data:/etc/listo/data:ro 19 | ports: 20 | - 8000:8000 -------------------------------------------------------------------------------- /examples/marketing.md: -------------------------------------------------------------------------------- 1 | ## Listo Announcement Message 2 | 3 | Starting a new project or product? Making a significant change to an existing feature? Want to find out the latest security, reliability and architecture requirements to consider? Why not give Listo a go! 4 | 5 | The average time to complete a #listo assessment is 10 minutes that results in a Trello board containing cards specific to your project to triage and prioritise at your leisure. 6 | 7 | We are very eager to help teams through the #listo process and collect any feedback from your experience :) The RFC can be found [here](rfc_listo.md). 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | "types": ["node"], 18 | "typeRoots": ["./node_modules/@types"] 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/utils/prepareProjectMeta.ts: -------------------------------------------------------------------------------- 1 | import { getRiskLevel } from './index'; 2 | import { ProjectMeta, Risk, ProjectMetaResponses } from './../types/index'; 3 | 4 | export const prepareProjectMeta = ( 5 | projectMeta: ProjectMeta[], 6 | risks: Risk[], 7 | ) => { 8 | const preparedProjectMeta = projectMeta.reduce((map, metaItem) => { 9 | return { ...map, [metaItem.name]: metaItem.userResponse }; 10 | }, {} as ProjectMetaResponses); 11 | 12 | // Adding risk level here... feels a bit strange to force it in to the project meta though 13 | preparedProjectMeta.riskLevel = getRiskLevel(risks); 14 | return preparedProjectMeta; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/utils/getSelectedRisks.ts: -------------------------------------------------------------------------------- 1 | import { Risk, RiskOption, RiskSelection } from './../types'; 2 | 3 | const findRiskOption = (options: RiskOption[]) => 4 | options.find(option => option.selected); 5 | 6 | const getSelectedRisks = (risks: Risk[]) => 7 | risks.reduce((selectedRisks, risk) => { 8 | const option = findRiskOption(risk.options); 9 | if (option) { 10 | return [ 11 | ...selectedRisks, 12 | { 13 | text: risk.text, 14 | selection: option.text, 15 | }, 16 | ]; 17 | } 18 | return selectedRisks; 19 | }, []); 20 | 21 | export default getSelectedRisks; 22 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { ThemeProvider } from '@material-ui/styles'; 6 | import { createMuiTheme } from '@material-ui/core'; 7 | 8 | const theme = createMuiTheme({ 9 | palette: { 10 | primary: { 11 | main: '#e60278', 12 | }, 13 | secondary: { 14 | main: '#0d3880', 15 | }, 16 | error: { 17 | main: '#9556b7', 18 | }, 19 | }, 20 | }); 21 | 22 | ReactDOM.render( 23 | 24 | {' '} 25 | 26 | , 27 | document.getElementById('root'), 28 | ); 29 | -------------------------------------------------------------------------------- /data/modules/general/services.yml: -------------------------------------------------------------------------------- 1 | category: General 2 | assessmentQuestion: Are you implementing or modifying a service? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: > 7 | We have looked through [RFC010 Domains]() 8 | to determine the most appropriate domain for our service 9 | - question: Our service is accepting traffic as per the guidance in the [Ingress RFC](). 10 | - question: > 11 | We have implemented DoS and runtime blocking (WAF), in compliance 12 | with the [RFC Runtime Protection and Monitoring](). 13 | - question: We have added authentication to our service as per [RFC008 - Application Authentication](). 14 | tags: WebApp, API, Worker 15 | title: Services 16 | -------------------------------------------------------------------------------- /data/modules/data/rds.yml: -------------------------------------------------------------------------------- 1 | category: Data 2 | assessmentQuestion: We will make use of Relation Database Systems for persistent storage? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We have taken steps to prevent [SQL injection vulnerabilities](https://www.acunetix.com/websitesecurity/sql-injection/) 7 | Amazon RDS: 8 | - question: We have considered using Amazon Aurora. 9 | - question: We have setup read-replicas and backups. 10 | - question: We ensure our replicas are properly spread across AZs. 11 | resources: 12 | - "[SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)." 13 | tags: WebApp, API, Worker 14 | title: Relational Database 15 | -------------------------------------------------------------------------------- /data/modules/code/internal_libs.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Concerns the consumption or publication of internal shared libraries? 3 | minimumRisk: Medium Risk 4 | checkLists: 5 | General: 6 | - question: We have a process or tool for updating our external and internal dependencies to the latest stable version (i.e. [Renovate](https://github.com/apps/renovate) and [Upkeep]()). 7 | tools: 8 | - Renovate 9 | - Upkeep 10 | - question: We have contacted the owners of internal libraries to ensure our use case is appropriate. 11 | NPM: 12 | - question: > 13 | We have read [RFC009 - npm]() concerning 14 | proper usage of our NPM organisation. 15 | tags: WebApp, API, Worker, iOS, Android 16 | title: Internal Libraries 17 | -------------------------------------------------------------------------------- /data/projectMeta.yml: -------------------------------------------------------------------------------- 1 | - name: boardName 2 | placeholder: 'Feature X or Product Y' 3 | required: true 4 | label: The feature or product you want to assess 5 | type: input 6 | - name: slackTeam 7 | placeholder: '#myteam' 8 | required: true 9 | label: Your teams Slack channel 10 | type: input 11 | - name: slackUserName 12 | placeholder: '@name' 13 | required: true 14 | label: Your Slack Username 15 | type: input 16 | - name: codeLocation 17 | placeholder: https:// (link to code or documentation) 18 | required: false 19 | label: A link to find out more (code or documentation) 20 | type: input 21 | - name: trelloEmail 22 | placeholder: 'mytrello@email.com' 23 | required: true 24 | label: The email address of your Trello account 25 | type: input 26 | -------------------------------------------------------------------------------- /data/modules/code/open_source.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Do you intend for this library or project to be open sourced? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We have read and are compliance with [RFC016 - Open-Source](). 7 | - question: We have selected an appropriate licence ([RFC016 - Open-Source]())? 8 | - question: Have you ensured no secrets are stored in the project source code and history as per ([RFC016 - Open-Source]())? 9 | - question: Have you ensured no sensitive corporate information is stored in the project source code? 10 | - question: Have you let the community know and are they comfortable about your intention within the '#open-source' channel? 11 | tags: WebApp, API, Workers, iOS, Android 12 | title: Open Source Compliance 13 | -------------------------------------------------------------------------------- /examples/deploy/ecr.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Resources: 3 | ContainerRepo: 4 | Type: AWS::ECR::Repository 5 | Properties: 6 | RepositoryName: listo 7 | LifecyclePolicy: 8 | LifecyclePolicyText: > 9 | { 10 | "rules": [ 11 | { 12 | "rulePriority": 1, 13 | "description": "Keep only 10 images", 14 | "selection": { 15 | "tagStatus": "any", 16 | "countType": "imageCountMoreThan", 17 | "countNumber": 10 18 | }, 19 | "action": { 20 | "type": "expire" 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /frontend/generate-schema.ts: -------------------------------------------------------------------------------- 1 | import * as TJS from 'typescript-json-schema'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const { promisify } = require('util'); 6 | 7 | const writeFile = promisify(fs.writeFile); 8 | 9 | (async () => { 10 | try { 11 | const settings: TJS.PartialArgs = { 12 | required: true, 13 | noExtraProps: true, 14 | }; 15 | 16 | const program = TJS.getProgramFromFiles([ 17 | path.join(__dirname, 'src/types/index.ts'), 18 | ]); 19 | const schema = TJS.generateSchema(program, 'DirectoryData', settings); 20 | await writeFile( 21 | path.join(__dirname, 'data-schema.json'), 22 | JSON.stringify(schema), 23 | ); 24 | } catch (ex) { 25 | console.error('BAD CODE', ex); 26 | process.exit(1); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /examples/deploy/heroku_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Listo to Heroku 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | IMAGE_NAME: listoproject/listo 10 | HEROKU_APP_NAME: listo-demo 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Login to Heroku Container registry 18 | env: 19 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 20 | run: heroku container:login 21 | - name: Build and push 22 | env: 23 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 24 | run: heroku container:push -a $HEROKU_APP_NAME web --recursive 25 | - name: Release 26 | env: 27 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 28 | run: heroku container:release -a $HEROKU_APP_NAME web 29 | -------------------------------------------------------------------------------- /data/modules/software_env/servers.yml: -------------------------------------------------------------------------------- 1 | category: Software Environment 2 | assessmentQuestion: Will your team need to manage servers (EC2/VM/Physical) to deploy your application? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We have hardened our servers using the [Runtime Protection – Infrastructure Hardening and Patching]() guidance. 7 | tools: 8 | - Gantry 9 | - EBB 10 | - GAMI 11 | - question: We have patched our servers (except for valid and approved exceptions) using the [Runtime Protection – Infrastructure Hardening and Patching]() guidance. 12 | - question: We are logging security relevent OS server events. Using [GAMI]() will enable the appropriate logging for you. 13 | resources: 14 | - "[Runtime Protection – Infrastructure Hardening and Patching]()" 15 | tags: WebApp, API, Worker 16 | title: Servers 17 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | yarn install 3 | 4 | prettier: 5 | yarn run format 6 | 7 | serve: install prettier ../frontend/data-schema.json 8 | source ../env.sh ; yarn run start 9 | 10 | ../frontend/data-schema.json: 11 | pushd ../frontend && make data-schema.json ; popd 12 | 13 | validate_data: ../frontend/data-schema.json 14 | yarn run validate:data ../frontend/data-schema.json ../data 15 | 16 | start_db: 17 | docker run -d -p 9000:8000 --name="listo-db" amazon/dynamodb-local 18 | 19 | stop_db: 20 | docker stop "listo-db"; docker rm listo-db 21 | 22 | delete_board: 23 | yarn run test -- -d $(boardId) 24 | 25 | create_board: 26 | yarn run test -- -c 27 | 28 | list_boards: 29 | yarn run test -- -l 30 | 31 | get_project: 32 | yarn run test -- -p $(projectId) 33 | 34 | slack_message: 35 | yarn run test -- -s 36 | 37 | .PHONY: deploy serve install prettier test 38 | -------------------------------------------------------------------------------- /frontend/src/components/RiskAssessment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { useStyles } from '../styles'; 3 | import { AppContext } from '../context'; 4 | import { Grid, Paper, Typography } from '@material-ui/core'; 5 | import { getRiskLevel } from '../utils'; 6 | 7 | const RiskAssessment = () => { 8 | const classes = useStyles({}); 9 | const { risks } = useContext(AppContext); 10 | 11 | const riskLevel = getRiskLevel(risks); 12 | 13 | return riskLevel ? ( 14 | 15 | 16 | 17 | 18 | 19 | Your project is {riskLevel}! 20 | 21 | 22 | 23 | 24 | 25 | ) : null; 26 | }; 27 | 28 | export default RiskAssessment; 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 as builder 2 | 3 | WORKDIR /usr/src/listo 4 | ADD server/package.json server/yarn.lock ./server/ 5 | ADD frontend/package.json frontend/yarn.lock ./frontend/ 6 | RUN (cd frontend && yarn install) && (cd server && yarn install) 7 | 8 | ADD server server 9 | ADD frontend frontend 10 | 11 | RUN cd server && yarn run build ; cd .. 12 | RUN cd frontend && REACT_APP_API_URL=/api yarn run build ; yarn run build:schema ; cd .. 13 | 14 | FROM node:10-slim 15 | 16 | RUN mkdir /opt/listo 17 | WORKDIR /opt/listo 18 | COPY --from=builder /usr/src/listo/frontend/build frontend 19 | COPY --from=builder /usr/src/listo/server/build/server/src server 20 | COPY --from=builder /usr/src/listo/server/node_modules server/node_modules 21 | COPY --from=builder /usr/src/listo/frontend/data-schema.json . 22 | 23 | ENV SCHEMA_PATH=/opt/listo/data-schema.json 24 | ENV FRONTEND_ASSETS_PATH=/opt/listo/frontend 25 | CMD [ "node", "server/app.js"] -------------------------------------------------------------------------------- /data/modules/code/xml.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Does your code work with, write or read XML (including file formats like Word and PDF)? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We are validating user-supplied XML with a schema doc before working on it. 7 | - question: > 8 | We have disabled all routes to [XML External Entity processing](https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing) 9 | at the parser level. 10 | - question: If using a third party XML parser (including parsing XML based documents like Word). 11 | resources: 12 | - "[XML external entity (XXE) injection](https://portswigger.net/web-security/xxe)." 13 | - "[What Are XXE Attacks](https://www.acunetix.com/blog/articles/xml-external-entity-xxe-vulnerabilities/)." 14 | - "[OWASP XXE Prevention](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html)." 15 | tags: WebApp, API, iOS, Android, Worker 16 | title: XML 17 | -------------------------------------------------------------------------------- /data/modules/code/authorisation.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Does your code need to control access between different users or roles? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | Access Control: 6 | - question: > 7 | We have ensured that difference user roles (regular, admin, etc) 8 | can only access what is intended. 9 | - question: > 10 | We have checked that customers can only access data that is intended for them and 11 | have checked our code for [IDOR](https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html) vulnerabilities. 12 | resources: 13 | - "[ASVS - Access Control](https://github.com/OWASP/ASVS/blob/master/4.0/en/0x12-V4-Access-Control.md)." 14 | - "[OWASP IDOR Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html)." 15 | - "[Access Control Verification Requirements](https://cheatsheetseries.owasp.org/cheatsheets/IndexASVS.html#v41-general-access-control-design)." 16 | tags: WebApp, API, iOS, Android, Worker 17 | title: Authorisation 18 | -------------------------------------------------------------------------------- /data/modules/software_env/containers.yml: -------------------------------------------------------------------------------- 1 | category: Software Environment 2 | assessmentQuestion: Will you be deploying your application inside a container? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | AWS ECR: 6 | - question: We are storing our images within ECR as per the [RFC004 - AWS ECR]() 7 | tools: 8 | - Gantry 9 | - question: We have scanned our currently deployed images for vulnerabilities using [AWS ECR Image Scanning](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html). 10 | - question: We have triaged and created a remediation plan for all high vulnerabilities identified by AWS ECR Image Scanning. 11 | General: 12 | - question: We use a minimal Docker image for our containers to reduce the attack surface (e.g. Alpine vs Ubuntu). 13 | - question: We have reduced the attack surface further by installing only the packages we need or have considered [Distroless Docker Images](https://github.com/GoogleContainerTools/distroless). 14 | - question: We have run [Docker Bench for Security](https://github.com/docker/docker-bench-security) to scan for and harden our containers. 15 | tags: WebApp, API 16 | title: Containers 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2019 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /server/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ProjectModel, QuickChecklistModel } from '../../frontend/src/types'; 2 | 3 | export interface Repository { 4 | init: () => Promise; 5 | create: (project: ProjectModel) => Promise; 6 | update: (projectId: string, boardLink: string) => Promise; 7 | get: (projectId: string) => Promise; 8 | getQuickChecklist: (id: string) => Promise; 9 | upsertQuickChecklist: ( 10 | quickchecklist: QuickChecklistModel, 11 | ) => Promise; 12 | } 13 | 14 | export const isValidProject = ( 15 | projectOrChecklist?: ProjectModel | QuickChecklistModel, 16 | ): projectOrChecklist is ProjectModel => { 17 | 18 | if(!projectOrChecklist) return false; 19 | 20 | if ((projectOrChecklist as ProjectModel).metaData) { 21 | return true; 22 | } 23 | 24 | return false; 25 | }; 26 | 27 | export const isValidQuickChecklist = ( 28 | projectOrChecklist?: ProjectModel | QuickChecklistModel, 29 | ): projectOrChecklist is QuickChecklistModel => { 30 | 31 | if(!projectOrChecklist) return false; 32 | 33 | if ((projectOrChecklist as QuickChecklistModel).checkList) { 34 | return true; 35 | } 36 | 37 | return false; 38 | }; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "ts-node src/tests.ts", 6 | "start": "ts-node --project ./tsconfig.local.json src/app.ts", 7 | "format": "prettier --write \"./**/*.{ts,tsx}\"", 8 | "tsc": "tsc", 9 | "build": "tsc --project ./tsconfig.json", 10 | "validate:data": "ts-node src/data.ts" 11 | }, 12 | "dependencies": { 13 | "@types/proper-lockfile": "^4.1.1", 14 | "ajv": "^6.10.2", 15 | "aws-sdk": "^2.814.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.0.0", 18 | "express": "^4.17.1", 19 | "js-yaml": "^3.13.1", 20 | "node-fetch": "^2.6.1", 21 | "object-path-immutable": "^3.1.1", 22 | "p-limit": "^2.3.0", 23 | "proper-lockfile": "^4.1.1" 24 | }, 25 | "devDependencies": { 26 | "@types/cors": "^2.8.6", 27 | "@types/express": "^4.17.0", 28 | "@types/js-yaml": "^3.12.1", 29 | "@types/node": "^12.11.7", 30 | "@types/node-fetch": "^2.5.0", 31 | "@types/uuid": "^3.4.5", 32 | "commander": "^2.20.0", 33 | "prettier": "^1.19.1", 34 | "ts-loader": "^5.3.3", 35 | "ts-node": "^8.3.0", 36 | "typescript": "^3.9.3" 37 | }, 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /data/modules/general/telemetry.yml: -------------------------------------------------------------------------------- 1 | category: General 2 | assessmentQuestion: We need to monitor the behaviour (security or product) and health (reliability or perf) of our running service? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | Logging: 6 | - question: We log important system events at appropriate log levels to help with debugging code. 7 | - question: > 8 | We log to stdout and use [our central log forwarding service]() 9 | to ship our logs as per [RFC Logging Standard](). 10 | tools: 11 | - Gantry 12 | - question: We use a structured logging format that is machine parsable. As per [RFC Logging Standard](). 13 | - question: We do not log sensitive customer or system information (like auth headers). 14 | Monitoring: 15 | - question: > 16 | We have configured monitoring tools mentioned in the [Application Monitoring and Alerting RFC](). 17 | - question: > 18 | We have configured [Pager Duty](https://pagerduty.com) or a similar 19 | on-call tool to be triggered from actionable monitoring alerts. 20 | Tracing: 21 | - question: > 22 | We have read [RFC002 - Request Ids]() 23 | and understand how to link requests for tracing purposes. 24 | tags: WebApp, API, Worker, iOS, Android 25 | title: Monitoring 26 | -------------------------------------------------------------------------------- /data/modules/service_provider/aws.yml: -------------------------------------------------------------------------------- 1 | category: Service Provider 2 | assessmentQuestion: Will you be deploying your application within AWS? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: Our AWS account has been properly provisioned via our account creation process. 7 | - question: > 8 | We have used our internal [VPC Template]() 9 | to create a VPC with sensible defaults. 10 | - question: > 11 | We are using either [Trusted Advisor](https://console.aws.amazon.com/trustedadvisor/) to identify issues (cost, security, 12 | performance, etc) within our AWS account. 13 | - question: We have triaged and created a remediation plan for all high and critical vulnerabilities. 14 | - question: We have completed the [Well Architected Framework](https://console.aws.amazon.com/wellarchitected/home) 15 | IAM Roles: 16 | - question: We have read and comply with [RFC005 - AWS IAM Users Standard]() 17 | - question: We employ the [principle of least privilege](https://searchsecurity.techtarget.com/definition/principle-of-least-privilege-POLP) when deciding what access to give to roles and policies within our account. 18 | resources: 19 | - "[Well Architected Framework](https://console.aws.amazon.com/wellarchitected/home)." 20 | tags: WebApp, API, Worker 21 | title: AWS 22 | -------------------------------------------------------------------------------- /data/modules/code/authentication.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Does your code authenticate or authorize users, tokens, sessions, or other? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | Authentication: 6 | - question: We are only using preferred auth protocols and patterns stated within [RFC018 - Authentication Protocols](). 7 | tools: 8 | - Auth Sidecar 9 | - Auth0 10 | - question: We are managing authentication secrets as per [RFC007 - Secrets Management - Securing Sensitive Secrets](). 11 | Cookies: 12 | - question: We have forced the use of the `secure` and `HttpOnly` flags if possible. 13 | tools: 14 | - Auth0 15 | - question: We have ensured that the `domain` being set on this cookie is tightly-scoped. 16 | - question: We are not using cookies to store customer PII. 17 | tools: 18 | - Auth0 19 | Login Flows: 20 | - question: We are blocking and monitoring for authentication specific attacks such as brute-force and credential stuffing. 21 | tools: 22 | - Auth0 23 | resources: 24 | - "[ASVS - Authentication Verification Requirements](https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md)." 25 | - "[OWASP Auth Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)." 26 | tags: WebApp, API, iOS, Android, Worker 27 | title: Authentication 28 | -------------------------------------------------------------------------------- /frontend/src/components/HelpDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import DialogTitle from '@material-ui/core/DialogTitle'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import { DialogContent, DialogContentText } from '@material-ui/core'; 6 | 7 | interface Props { 8 | title: string; 9 | helpText: string; 10 | } 11 | 12 | const HelpDialog = ({ helpText, title }: Props) => { 13 | const [open, setOpen] = React.useState(false); 14 | 15 | const handleClickOpen = () => { 16 | setOpen(true); 17 | }; 18 | 19 | const handleClose = () => { 20 | setOpen(false); 21 | }; 22 | 23 | return ( 24 |
25 | 28 | 34 | {title} 35 | 36 | 40 | 41 | 42 |
43 | ); 44 | }; 45 | 46 | export default HelpDialog; 47 | -------------------------------------------------------------------------------- /frontend/src/components/PaginationButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, MouseEvent } from 'react'; 2 | import { Button, CircularProgress } from '@material-ui/core'; 3 | import { useStyles } from '../styles'; 4 | import { STEPS, StepContext } from '../context/StepContext'; 5 | 6 | const PaginationButtons = () => { 7 | const classes = useStyles(); 8 | const { 9 | activeStep, 10 | handleBack, 11 | handleNext, 12 | checkStepValid, 13 | loading, 14 | setLoading, 15 | } = useContext(StepContext); 16 | 17 | const stepValid = checkStepValid(activeStep); 18 | 19 | return ( 20 |
21 | {activeStep !== 0 && ( 22 | 25 | )} 26 | {loading ? ( 27 | 28 | ) : ( 29 | 41 | )} 42 |
43 | ); 44 | }; 45 | 46 | export default PaginationButtons; 47 | -------------------------------------------------------------------------------- /data/modules/code/third_party_libs.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Does your project introduce or utilize a third-party library or service? 3 | minimumRisk: Medium Risk 4 | checkLists: 5 | General: 6 | - question: We are utilizing the latest stable version of our libraries by using [Renovate](https://github.com/apps/renovate) to keep them updated (reach out in '#github'). 7 | tools: 8 | - Renovate 9 | Library: 10 | - question: We have enabled [Snyk]() for all supported services. 11 | tools: 12 | - Snyk 13 | - question: We have followed secure vetting practices before using open source components [as per the guidance here](). 14 | - question: We have remediated all fixable High severity vulnerabilities [as per the guidance here](). 15 | - question: We have read and comply with the [Open Source Dependency Management RFC](). 16 | Service: 17 | - question: We have reached out in '#security' to request a third party vendor risk review for commercial services that have access to or store sensitive data. 18 | resources: 19 | - "[OWASP Vulnerable Dependency Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html)." 20 | - "[Vendor Risk Management](https://searchcio.techtarget.com/definition/Vendor-risk-management)." 21 | tags: WebApp, API, iOS, Android, Worker 22 | title: Third-Party Libraries & Services 23 | -------------------------------------------------------------------------------- /examples/deploy/Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_REPO_URL = .dkr.ecr.ap-southeast-2.amazonaws.com/listo 2 | 3 | deploy: 4 | docker build github.com/seek-oss/listo -t seek/listo:latest 5 | docker build --pull=false -t "$(DOCKER_REPO_URL):${BUILD_NUMBER}" . 6 | docker push "$(DOCKER_REPO_URL):${BUILD_NUMBER}" 7 | aws cloudformation deploy \ 8 | --template-file stack.yaml \ 9 | --stack-name listo \ 10 | --no-fail-on-empty-changeset \ 11 | --parameter-overrides \ 12 | 'ImageUrl=$(DOCKER_REPO_URL):${BUILD_NUMBER}' \ 13 | 'VpcID=' \ 14 | 'PublicSubnetIds=' \ 15 | 'PrivateSubnetIds=' \ 16 | 'TrelloTeam=' \ 17 | 'CertificateArn=' \ 18 | 'OidcAuthorizationEndpoint=' \ 19 | 'OidcClientId=' \ 20 | 'OidcClientSecret=' \ 21 | 'OidcIssuer=' \ 22 | 'OidcTokenEndpoint=' \ 23 | 'OidcUserInfoEndpoint=' \ 24 | 'AlbHostedZoneId=' \ 25 | 'AlbDnsName=' 26 | 27 | deploy_heroku: 28 | heroku container:login 29 | heroku container:push --recursive --app [YOUR_APP_NAME] web 30 | heroku container:release --app [YOUR_APP_NAME] web 31 | 32 | 33 | .PHONY: deploy 34 | -------------------------------------------------------------------------------- /data/modules/code/xss.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: We are rendering user-originated content via the DOM or reflecting it from the server? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: > 7 | We are using standardized formatting libraries for all output of user-provided 8 | data. 9 | - question: > 10 | 'We are using proper `Content-Type` headers when rendering content. e.g. `JSON` 11 | should be served with Content-Type: application/json'. 12 | - question: User input is not echoed or output unencoded on the page. 13 | - question: > 14 | We have tested user input with some XSS test payloads to ensure there is no XSS 15 | present. 16 | DOM: 17 | - question: > 18 | We are using the javascript formatting library to properly encode all user-provided 19 | data to protect against [DOM based XSS](https://www.owasp.org/index.php/DOM_Based_XSS). 20 | resources: 21 | - "[OWASP ASVS V5 - Validation, Sanitization and Encoding Verification Requirements](https://github.com/OWASP/ASVS/blob/master/4.0/en/0x13-V5-Validation-Sanitization-Encoding.md)." 22 | - "[OWASP Cross Site Scripting Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)." 23 | - "[OWASP DOM Based XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html)." 24 | tags: WebApp, API 25 | title: XSS 26 | -------------------------------------------------------------------------------- /frontend/src/utils/handleRiskAnswer.ts: -------------------------------------------------------------------------------- 1 | import { Risk, RiskOption } from '../types'; 2 | 3 | export const handleRiskAnswer = (risks: Risk[], setRisks: any) => ( 4 | selectedIndex: number, 5 | ) => (_: React.ChangeEvent<{}>, value: string) => { 6 | const text = value; 7 | const updatedRisks = risks.map( 8 | (risk, index): Risk => { 9 | if (index === selectedIndex) { 10 | return { 11 | ...risk, 12 | options: risk.options.map( 13 | (option): RiskOption => ({ 14 | ...option, 15 | selected: option.text === text, 16 | }), 17 | ), 18 | }; 19 | } 20 | if (index > selectedIndex && isFinalStep(risks, selectedIndex, text)) { 21 | return { 22 | ...risk, 23 | options: risk.options.map( 24 | (option): RiskOption => ({ 25 | ...option, 26 | selected: false, 27 | }), 28 | ), 29 | }; 30 | } 31 | return risk; 32 | }, 33 | ); 34 | 35 | setRisks(updatedRisks); 36 | }; 37 | 38 | export const isFinalStep = ( 39 | risks: Risk[], 40 | selectedIndex: number, 41 | text: string, 42 | ) => { 43 | if (selectedIndex >= risks.length) { 44 | return false; 45 | } 46 | 47 | const foundAnswer = risks[selectedIndex].options.find(o => o.text === text); 48 | 49 | return foundAnswer ? Boolean(foundAnswer.risk) : false; 50 | }; 51 | -------------------------------------------------------------------------------- /data/modules/data/nosql.yml: -------------------------------------------------------------------------------- 1 | category: Data 2 | assessmentQuestion: We will be using a NoSQL database to store data (e.g Dynamo or Mongo)? 3 | minimumRisk: Low Risk 4 | checkLists: 5 | General: 6 | - question: We have taken steps to prevent [NoSQL injection attacks](http://www.petecorey.com/blog/2017/07/03/what-is-nosql-injection/). 7 | DynamoDB: 8 | - question: We have configured a sensible provisioning strategy based on our expected throughput. 9 | - question: We have [designed our partition keys](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html) to ensure workloads are evenly distributed. 10 | - question: We have carefully considered our expected [access patterns](https://youtu.be/HaEPXoXVf2k) and have incorporated this into our key design. 11 | - question: We have a sensible [back-up](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Backup.Tutorial.html) strategy in place. 12 | resources: 13 | - "[DynamoDB Injection](https://medium.com/appsecengineer/dynamodb-injection-1db99c2454ac)." 14 | - "[MongoDB Injection Attacks](https://blog.sqreen.com/mongodb-will-not-prevent-nosql-injections-in-your-node-js-app/)." 15 | - "[MongoDB Injection Prevention](https://blog.sqreen.com/prevent-nosql-injections-mongodb-node-js/)." 16 | - "[Best Practices for DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html)." 17 | tags: WebApp, API, Worker 18 | title: NoSQL Databases 19 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'; 2 | import appFactory from './appFactory'; 3 | import { combineData } from './data'; 4 | import { Disk } from './diskdb'; 5 | import { Dynamo } from './dynamodb'; 6 | import { Repository } from './types'; 7 | import { region } from './config'; 8 | 9 | const DATA_DIR = process.env.DATA_DIR || '../data'; 10 | const SCHEMA_PATH = process.env.SCHEMA_PATH || '../frontend/data-schema.json'; 11 | const PORT = process.env.PORT || 8000; 12 | 13 | (async function() { 14 | try { 15 | const listoData = await combineData(SCHEMA_PATH, DATA_DIR); 16 | if (!listoData) { 17 | console.log('Unable to read listo data'); 18 | process.exit(1); 19 | } 20 | 21 | // "Disk" is the default database. 22 | let db: Repository; 23 | 24 | if (process.env.LISTO_DATABASE === 'Dynamo') { 25 | const dynamoConfigOptions: ServiceConfigurationOptions = { 26 | region: region, 27 | endpoint: process.env.DYNAMO_DB_ENDPOINT, 28 | }; 29 | 30 | db = new Dynamo(dynamoConfigOptions, process.env.DYNAMODB_TABLE); 31 | } else { 32 | db = new Disk(process.env.DISK_PATH); 33 | } 34 | 35 | await db.init(); 36 | 37 | const server = await appFactory(db, listoData); 38 | server.listen(PORT); 39 | console.log(`listening on http://localhost:${PORT}`); 40 | } catch (err) { 41 | console.error('Error starting program', err); 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Risk } from '../types'; 2 | 3 | export const getIndexOfLatestAnswer = (risks: Risk[]) => { 4 | const index = risks 5 | .slice() 6 | .reverse() 7 | .findIndex(risk => risk.options.some(o => o.selected)); 8 | 9 | return index >= 0 ? risks.length - 1 - index : index; 10 | }; 11 | 12 | export const getRisksToDisplay = (risks: Risk[]) => { 13 | const lastAnswerIndex = getIndexOfLatestAnswer(risks); 14 | 15 | return risks.filter((_, index, array) => { 16 | if (index === 0 || index <= lastAnswerIndex) { 17 | return true; 18 | } 19 | if (array[index - 1].options.find(o => o.selected && !o.risk)) { 20 | return true; 21 | } 22 | return false; 23 | }); 24 | }; 25 | 26 | export const getRiskLevel = (risks: Risk[]) => { 27 | const lastAnswerIndex = getIndexOfLatestAnswer(risks); 28 | 29 | if (lastAnswerIndex > -1) { 30 | const lastSelectedRisk = risks[lastAnswerIndex]; 31 | 32 | const selectedAnswer = lastSelectedRisk.options.find(o => o.selected); 33 | 34 | return selectedAnswer ? selectedAnswer.risk : undefined; 35 | } 36 | }; 37 | 38 | export const isFinalStep = ( 39 | risks: Risk[], 40 | selectedIndex: number, 41 | text: string, 42 | ) => { 43 | if (selectedIndex >= risks.length) { 44 | return false; 45 | } 46 | 47 | const foundAnswer = risks[selectedIndex].options.find(o => o.text === text); 48 | 49 | return foundAnswer ? Boolean(foundAnswer.risk) : false; 50 | }; 51 | -------------------------------------------------------------------------------- /data/modules/code/urls.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Does your component build, parse, read or write URL's or redirect users to another URL? 3 | minimumRisk: Medium Risk 4 | checkLists: 5 | General: 6 | - question: We are using a well supported url library to load and verify every url before operating on it. 7 | - question: > 8 | We have looked into ways to avoid directing users to arbitrary urls, especially 9 | those that are user-controlled (Open Redirect). 10 | - question: > 11 | We know what type of scheme (http/https/ftp) we are expecting, so we are guaranteeing 12 | that the scheme is being parsed, and validated correctly. 13 | SSRF: 14 | - question: We have configured a whitelist of resources or systems (i.e. URL's, files, etc) to which the server can send requests or load data/files from. 15 | resources: 16 | - "[OWASP Unvalidated Redirects and Forwards](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)." 17 | - "[OWASP ASVS V5 - Validation, Sanitization and Encoding Verification Requirements](https://github.com/OWASP/ASVS/blob/master/4.0/en/0x13-V5-Validation-Sanitization-Encoding.md)." 18 | - "[Server Side Request Forgery(SSRF)](https://www.acunetix.com/blog/articles/server-side-request-forgery-vulnerability/)." 19 | - "[OWASP SSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)." 20 | tags: WebApp, API, iOS, Android 21 | title: URL Parsing & Redirect 22 | -------------------------------------------------------------------------------- /data/modules/general/general.yml: -------------------------------------------------------------------------------- 1 | category: General 2 | assessmentQuestion: Guidance that all projects should follow. 3 | minimumRisk: Mandatory 4 | checkLists: 5 | Upgradability: 6 | - question: There is a commitment and plan for upgrading, patching, and ongoing maintenance for this project. 7 | - question: We have the ability to upgrade this functionality and it's libraries within 3 days if a security issue is identified. 8 | Build & Deploy: 9 | - question: We are using Buildkite to build and deploy our systems [as per our CI/CD guidance](). 10 | tools: 11 | - Buildkite 12 | - question: We are storing and managing our source code as per the guidance within [RFC003 - Source Control - GitHub](). 13 | Security: 14 | - question: We have discussed with the security team if a pen test, code or security architecture review should be performed on this component or project. 15 | - question: We understand our commitments of being in scope of our [Bug Bounty Program](https://bugcrowd.com/seek). 16 | - question: We have reviewed and remediated our open security vulnerabilities [as per the open Jira issues here](). 17 | - question: We have checked for dead code, and deleted it. Less code = fewer bugs! 18 | - question: Error messages (debug, callstack, etc) don't [leak sensitive information]() to a user. 19 | - question: We are managing our secrets (app, deploy, infra, etc) as per [RFC007 - Secrets Management - Securing Sensitive Secrets](). 20 | tags: WebApp, API, Workers, iOS, Android 21 | title: General Guidance 22 | -------------------------------------------------------------------------------- /frontend/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CssBaseline from '@material-ui/core/CssBaseline'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import { useStyles } from '../styles'; 7 | import { Link } from '@reach/router'; 8 | import { ReactComponent as Logo } from '../listo_pink.svg'; 9 | 10 | const Header = () => { 11 | const classes = useStyles({}); 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | FAQ 28 | 29 | 30 | 31 | 37 | CHECKLISTS 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default Header; 47 | -------------------------------------------------------------------------------- /frontend/src/components/steps/RiskAssessment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Typography, Grid } from '@material-ui/core'; 3 | import RiskCriteria from '../RiskCriteria'; 4 | import RiskAssessment from '../RiskAssessment'; 5 | import { AppContext } from '../../context'; 6 | import { getRisksToDisplay } from '../../utils'; 7 | import { riskHelp } from '../../help'; 8 | import HelpDialog from '../HelpDialog'; 9 | 10 | const RiskAssessmentContainer = () => { 11 | const { handleRiskAnswer, risks } = useContext(AppContext); 12 | 13 | if (!risks) { 14 | return
Loading risk questions....
; 15 | } 16 | 17 | const visibleRisks = getRisksToDisplay(risks); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | Risk Assessment for your project or feature 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {visibleRisks.map(({ text, options, description }, index) => ( 33 | 40 | ))} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default RiskAssessmentContainer; 47 | -------------------------------------------------------------------------------- /data/modules/code/csrf.yml: -------------------------------------------------------------------------------- 1 | category: Code 2 | assessmentQuestion: Is your functionality exposed within a web browser and implementing requests? 3 | that make sensitive user changes (e.g. password change, CV upload, send message, 4 | etc)? 5 | minimumRisk: Low Risk 6 | checkLists: 7 | General: 8 | - question: We are using the built-in anti-CSRF framework for our form and action functionality. 9 | - question: Every action in our functionality that makes sensitive user changes (e.g. password change, CV upload, send message, etc), is protected by an anti-CSRF token. 10 | Client: 11 | - question: If not already built-in to your anti-CSRF framework, authenticated XHR requests contain a [custom header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests) (ie. Authorization). 12 | API: 13 | - question: "[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) policy either not required or restricted to UI origin and minimum required methods, headers and credentials." 14 | - question: Authenticated requests not containing a custom header (ie. Authorization) [respond with 400 Bad Request](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers) . 15 | resources: 16 | - "[OWASP CSRF Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)." 17 | - "[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)." 18 | tags: WebApp, API 19 | title: CSRF 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "MIT", 6 | "dependencies": { 7 | "@material-ui/core": "^4.10.2", 8 | "@material-ui/icons": "^4.2.1", 9 | "@material-ui/lab": "^4.0.0-alpha.33", 10 | "@reach/router": "^1.2.1", 11 | "@types/node": "^12.12.44", 12 | "global": "^4.4.0", 13 | "lodash.clonedeep": "^4.5.0", 14 | "react": "^16.14.0", 15 | "react-dom": "^16.14.0", 16 | "react-markdown": "^4.2.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "format": "prettier --write \"./**/*.{ts,tsx}\"", 24 | "build:schema": "ts-node --project ./tsconfig.json -O '{\"module\":\"commonjs\"}' generate-schema.ts" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^24.0.18", 43 | "@types/lodash.clonedeep": "^4.5.6", 44 | "@types/reach__router": "^1.2.6", 45 | "@types/react": "^16.9.2", 46 | "@types/react-dom": "^16.9.0", 47 | "prettier": "^1.19.1", 48 | "react-scripts": "3.0.1", 49 | "ts-node": "^8.4.1", 50 | "typescript": "^3.9.3", 51 | "typescript-json-schema": "^0.40.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/components/steps/ProjectMetaGathering.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import TextField from '@material-ui/core/TextField'; 5 | import { AppContext } from '../../context'; 6 | 7 | const ProjectMetaGathering = () => { 8 | const { projectMeta, handleUpdateProjectMeta } = useContext(AppContext); 9 | 10 | const onChangeHandler = (event: React.ChangeEvent) => { 11 | const { name, value } = event.target; 12 | handleUpdateProjectMeta(name, value); 13 | }; 14 | 15 | return ( 16 | 17 | 18 | Details about your project or feature 19 | 20 | 21 | {projectMeta && 22 | projectMeta.map( 23 | ({ label, name, placeholder, userResponse = '', required }) => ( 24 | 25 | 37 | 38 | ), 39 | )} 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default ProjectMetaGathering; 46 | -------------------------------------------------------------------------------- /frontend/src/utils/pickCategoriesWithResponse.ts: -------------------------------------------------------------------------------- 1 | import { ModuleCategories } from '../types/index'; 2 | 3 | const pickCategoriesWithResponse = (categories: ModuleCategories) => { 4 | return Object.keys(categories).reduce<{ [category: string]: string[] }>( 5 | (pickedCategories, categoryKey) => { 6 | const submodules = Object.keys(categories[categoryKey]).reduce( 7 | (pickedModules, moduleKey) => { 8 | return categories[categoryKey][moduleKey]['response'] 9 | ? [...pickedModules, moduleKey] 10 | : pickedModules; 11 | }, 12 | [], 13 | ); 14 | return submodules.length 15 | ? { ...pickedCategories, ...{ [categoryKey]: submodules } } 16 | : pickedCategories; 17 | }, 18 | {}, 19 | ); 20 | }; 21 | 22 | export const selectedCategories = (categories: ModuleCategories) => { 23 | return Object.entries(categories).reduce( 24 | (pickedCategories, [categoryKey, category]) => { 25 | const submodules = Object.entries(categories[categoryKey]).reduce( 26 | (pickedModules, [moduleKey, mod]) => { 27 | return categories[categoryKey][moduleKey]['response'] 28 | ? { 29 | ...pickedModules, 30 | [moduleKey]: mod, 31 | } 32 | : pickedModules; 33 | }, 34 | {}, 35 | ); 36 | return Object.keys(submodules).length > 0 37 | ? { ...pickedCategories, ...{ [categoryKey]: submodules } } 38 | : pickedCategories; 39 | }, 40 | {} as ModuleCategories, 41 | ); 42 | }; 43 | 44 | export default pickCategoriesWithResponse; 45 | -------------------------------------------------------------------------------- /examples/TEMPLATE_env.sh: -------------------------------------------------------------------------------- 1 | # Template ENV file (Place in root directory and rename to env.sh) 2 | 3 | # Trello - Get your API Key here -> https://trello.com/app-key/ 4 | export TRELLO_API_KEY=e94947...00a92 5 | 6 | # Trello - Click on the "Generate a Token" link here -> https://trello.com/app-key/ 7 | export TRELLO_TOKEN=fda876d8af87d6fa876adfa....8516dcf715 8 | 9 | # AWS - Fake creds for local DynamoDB development 10 | export AWS_ACCESS_KEY_ID=leavelikethis 11 | export AWS_SECRET_ACCESS_KEY=leavelikethis 12 | 13 | # OPTIONAL - An invite link to a test board. 14 | # export TRELLO_BOARD_LINK='https://trello.com/invite/b/....' 15 | 16 | # OPTIONAL - Trello `idOrganization` 17 | # export TRELLO_TEAM= 18 | 19 | # OPTIONAL - If you would like to test the Slack Notification locally. 20 | # If not set, The Slack message will print to the console. 21 | # https://api.slack.com/messaging/webhooks 22 | # export SLACK_WEB_HOOK='https://hooks.slack.com/services/....' 23 | 24 | # OPTIONAL - The Slack channel you would like to send notifications 25 | export SLACK_TARGET_CHANNEL='#listo' 26 | 27 | # OPTIONAL - Slack Channel Deep link - https://api.slack.com/reference/deep-linking 28 | # export SLACK_CHANNEL_LINK='https://slack.com/app_redirect?channel=listo&team=T02P5698' 29 | 30 | # OPTIONAL - Server URL, e.g. if the server hosting listo is fronted by a load balancer or reverse proxy 31 | # export SERVER_URL='https://example.com' 32 | 33 | # OPTIONAL - Uncomment you would like to run the local Dynamodb database instead of the Diskdb. 34 | # export LISTO_DATABASE='Dynamo' 35 | # export CREATE_DYNAMO_TABLES=true 36 | # export DYNAMO_DB_ENDPOINT=http://localhost:9000 -------------------------------------------------------------------------------- /examples/deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deploying Listo 2 | 3 | This folder serves as an **example** for how Listo can be deployed to AWS and Heroku. 4 | 5 | ## AWS 6 | - `ecr.yaml` - CloudFormation stack for the ECR repository 7 | - `stack.yaml` - CloudFormation stack that: 8 | 1. Creates an ALB to authenticate users with the OIDC-compatible IdP of your choice 9 | 2. Launches Listo within Fargate 10 | 3. Creates some AWS Secrets Manager secrets for a Slack Webhook and your Trello Credentials 11 | 4. Creates a DynamoDB table for storing Listo projects 12 | 5. Sets up a AWS CloudWatch log group called `/listo` 13 | - `Makefile` - Sample Makefile for building and deploying Listo into AWS using the above stack 14 | - `Dockerfile` - Sample Dockerfile that embeds custom data, as volumes 15 | 16 | ### To use this example 17 | 18 | 1. Deploy `ecr.yaml` to your AWS account via AWS CloudFormation (this can be done in the UI) 19 | 2. Update the `Makefile` with your own settings (OIDC details, VPC settings, DNS settings, etc) 20 | 3. Deploy Listo `make deploy` 21 | 4. Update the secret values in the AWS Secrets Manager secrets created in deployment (Slack WebHook and Trello Credentials) 22 | 23 | ## Heroku 24 | 25 | - Create a container application within your account and add the relevant environment variables to the Config Vars (Trello creds is the only mandatory vars to add). 26 | - Create a new repo with your customised data directory and the Dockerfile.web. 27 | - [manually] Add your application details to the Makefile and then run `make deploy_heroku`. 28 | - [via CI] Checkout how we have deployed listo to Heroku using [Github Actions](heroku_deploy.yml). 29 | -------------------------------------------------------------------------------- /frontend/src/help.ts: -------------------------------------------------------------------------------- 1 | export const riskHelp = `The risk assessment is designed to help product and enablement teams become aware of the risks their project has on the business. For example processing, accessing or storing sensitive customer PII data will have a bigger risk if that personal information was breached due to a vulnerability within the product. 2 |
3 |
4 | “High Risk” projects have significant impact to the business (e.g. service was unavailable, a vulnerability was found or the project is strategically important to business success). Enablement teams will likely need to get involved or prioritise projects rated as "High Risk". 5 |
6 |
7 | “Medium Risk” projects will require some engagement but this would be in an ad-hoc capacity. 8 |
9 |
10 | “Low Risk” projects will generally not require much engagement from enablement teams.`; 11 | 12 | export const toolsHelp = `A list of tools to help product teams build projects and features faster. 13 |
14 | Selecting the tools below will automatically tick off the requirements aka checklist items on the following page and in the Trello board that are already completed by using the selected tool. `; 15 | 16 | export const modulesHelp = `The questions below are to help narrow down what requirements are applicable to your project so that the resulting Trello board only contains cards that you will need to triage, prioritise and action. 17 | 18 |
19 |
20 | 21 | If you are unsure whether a question or module is applicable, click on the arrow to find out more. If in doubt we recommend you tick the module and ask your questions on the resulting card in Trello.`; 22 | -------------------------------------------------------------------------------- /data/modules/data/general.yml: -------------------------------------------------------------------------------- 1 | category: Data 2 | assessmentQuestion: We are storing data to the filesystem or a database? 3 | minimumRisk: High Risk 4 | checkLists: 5 | General: 6 | - question: All data stores are not accessible to the public internet, unless this is intended. 7 | - question: We only store our Data for as long as it's required (i.e. we have a data retention policy setup). 8 | - question: All data is encrypted at rest, where possible. 9 | - question: We have considered how to provide sensible access control to customers, database users and systems, [via the principle of least privilege](https://searchsecurity.techtarget.com/definition/principle-of-least-privilege-POLP). 10 | - question: We have setup [audit logs](https://logz.io/blog/audit-logs-security-compliance/) and are forwarding them via our central log forwarder. 11 | - question: We have read the [Data Classification Policy]() to determine if our data is classified as sensitive. 12 | Sensitive Data: 13 | - question: Data is encrypted at rest and in flight. 14 | - question: Data is anonymised before it's sent to consuming services that do not require it. 15 | - question: Not stored in any non-production environments. 16 | - question: Tagged according to the [Data Tagging RFC](). 17 | - question: Complies with the [Data Handling Standard](). 18 | resources: 19 | - "[Using Audit Logs for Security and Compliance](https://logz.io/blog/audit-logs-security-compliance/)." 20 | - "[Data Retention Policies](https://searchdatabackup.techtarget.com/definition/data-retention-policy)." 21 | tags: WebApp, API, iOS, Android, Worker 22 | title: General Data Storage Guidance 23 | -------------------------------------------------------------------------------- /.github/workflows/prod.yml: -------------------------------------------------------------------------------- 1 | name: Build Listo, Push to Dockerhub and Deploy to Heroku 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v/*' 9 | 10 | env: 11 | IMAGE_NAME: listoproject/listo 12 | HEROKU_APP_NAME: listo-demo 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Build image 20 | run: docker build . --file Dockerfile --tag $IMAGE_NAME 21 | - name: Log into registry 22 | run: docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin <<< "${{ secrets.DOCKER_API_KEY }}" 23 | - name: Push image 24 | run: | 25 | if [[ "${{ github.ref }}" =~ ^refs/tags/v/(.*) ]] ; then 26 | VERSION="${BASH_REMATCH[1]}" 27 | elif [[ "${{ github.ref }}" =~ ^refs/heads/master ]] ; then 28 | VERSION="latest" 29 | else 30 | VERSION="unknown" 31 | fi 32 | 33 | docker tag $IMAGE_NAME $IMAGE_NAME:$VERSION 34 | docker push $IMAGE_NAME:$VERSION 35 | deploy: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v1 40 | - name: Login to Heroku Container registry 41 | env: 42 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 43 | run: heroku container:login 44 | - name: Build and push 45 | env: 46 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 47 | run: heroku container:push -a $HEROKU_APP_NAME web --recursive 48 | - name: Release 49 | env: 50 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 51 | run: heroku container:release -a $HEROKU_APP_NAME web 52 | -------------------------------------------------------------------------------- /frontend/src/components/FormSteps.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { useStyles } from '../styles'; 4 | import ProjectMetaGathering from './steps/ProjectMetaGathering'; 5 | import RiskAssessment from './steps/RiskAssessment'; 6 | import ModuleSelection from './steps/ModuleSelection'; 7 | import Summary from './steps/Summary'; 8 | import PaginationButtons from './PaginationButtons'; 9 | import { AppContext } from '../context'; 10 | import { Stepper, Step, StepLabel } from '@material-ui/core'; 11 | import { StepContext, STEPS } from '../context/StepContext'; 12 | import ToolingComponent from './Tooling'; 13 | 14 | const FormSteps = () => { 15 | const classes = useStyles({}); 16 | const { projectMeta, risks, categories } = useContext(AppContext); 17 | const { activeStep, handleGoToStep } = useContext(StepContext); 18 | 19 | if (!projectMeta || !risks || !categories) { 20 | // TODO: better empty state handling 21 | return

Loading!

; 22 | } 23 | 24 | return ( 25 | 26 | 27 | {STEPS.map(label => ( 28 | handleGoToStep(label)}> 29 | {label} 30 | 31 | ))} 32 | 33 | {activeStep === 0 && } 34 | {activeStep === 1 && } 35 | {activeStep === 2 && } 36 | {activeStep === 3 && } 37 | {activeStep === 4 && } 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default FormSteps; 44 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Listo 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/context/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleCategories, Checklists } from './../types/index'; 2 | import React from 'react'; 3 | import { Risk, ProjectMeta, AssessmentResult, Tools } from '../types'; 4 | 5 | export type HandleClickCheckbox = ( 6 | moduleKey: string, 7 | subModuleKey: string, 8 | value: boolean, 9 | ) => void; 10 | 11 | export type HandleUpdateProjectMeta = (name: string, response: string) => void; 12 | export type HandleRiskAnswer = ( 13 | event: React.ChangeEvent<{}>, 14 | value: string, 15 | ) => void; 16 | export type HandleSelectModule = ( 17 | categoryKey: string, 18 | moduleKey: string, 19 | value: boolean, 20 | ) => void; 21 | 22 | const initialContext = { 23 | projectMeta: [] as ProjectMeta[], 24 | handleUpdateProjectMeta: (name: string, response: string) => {}, 25 | categories: {} as ModuleCategories, 26 | risks: [] as Risk[], 27 | tools: {} as Tools, 28 | quickChecklist: {} as Checklists, 29 | initQuickChecklist: (checklists: Checklists) => {}, 30 | handleSelectChecklistItem: ( 31 | checklistName: string, 32 | checklistItemIndex: number, 33 | checked: boolean) => { }, 34 | handleSelectModule: ( 35 | categoryKey: string, 36 | moduleKey: string, 37 | value: boolean, 38 | ) => { }, 39 | handleRiskAnswer: (index: number) => ( 40 | _: React.ChangeEvent<{}>, 41 | value: string, 42 | ) => { }, 43 | handleSelectTool: (tool: string, category: string, value: boolean) => { }, 44 | prepareResult: (): AssessmentResult => ({ 45 | selectedModulesByCategory: {}, 46 | selectedRisks: [], 47 | projectMetaResponses: {}, 48 | selectedTools: [], 49 | }), 50 | }; 51 | 52 | export const AppContext = React.createContext(initialContext); 53 | -------------------------------------------------------------------------------- /data/risks.yml: -------------------------------------------------------------------------------- 1 | questions: 2 | - text: Do you want supporting teams such as Security and Architecture to reach out throughout the project? 3 | description: 'If you think the project is risky and want help from Security from the 4 | beginning: trust your gut. We''ll be involved and help however we can.' 5 | options: 6 | - text: 'Yes' 7 | risk: High Risk 8 | - text: 'No' 9 | 10 | - text: Does this project make any major security related changes or implement new authentication controls? 11 | description: Say "yes" if this project adds new ways for people to authenticate, 12 | adds to or changes existing security controls, or otherwise explicitly implements 13 | security/privacy features. 14 | options: 15 | - text: 'Yes' 16 | risk: High Risk 17 | - text: 'No' 18 | 19 | - text: Is the target for this project a prototype, internal-only, or a full, productionized, public launch? 20 | description: '' 21 | options: 22 | - text: Prototype or internal-only 23 | risk: Low Risk 24 | - text: Public launch 25 | 26 | - text: Will this project store, access or process sensitive customer data (PII) 27 | description: Customer Data is the most sensitive type of data, such as the contents of CV's, messages, etc. 28 | options: 29 | - text: 'No' 30 | risk: Low Risk 31 | - text: Yes, this project will store, access or process sensitive customer data 32 | 33 | - text: Is this project highly important strategically? 34 | description: Is this project one of our top priorities, one with major implications for our business, or otherwise highly strategic? 35 | options: 36 | - text: 'Yes' 37 | risk: Medium Risk 38 | - text: 'No' 39 | 40 | - text: Is this a change/update to an existing component, or a completely new project? 41 | description: '' 42 | options: 43 | - risk: High Risk 44 | text: New Component 45 | - risk: Medium Risk 46 | text: A change to an existing component 47 | -------------------------------------------------------------------------------- /data/tooling.yml: -------------------------------------------------------------------------------- 1 | Execution: 2 | Gantry: 3 | description: > 4 | Gantry is the tool for managing containerised workloads on AWS. It allows development teams to build and deploy services in a way that is secure, reliable, well-monitored, consistent, and in accordance with our RFCs. More info [here](). 5 | EBB: 6 | description: > 7 | EBB is a way of deploying your dockerised web apps to AWS using Elasticbeanstalk. 8 | warning: "Should not be used for new projects" 9 | GAMI: 10 | description: > 11 | Our Golden AMI's are a set of patched and security hardened AWS AMI's that are distributed into all AWS account (built weekly) within all deployable regions. 12 | Deployment: 13 | Buildkite: 14 | description: > 15 | We use [Buildkite]() as our preferred CI/CD platform. 16 | Teamcity: 17 | description: Teamcity is an outdated CI/CD process, it is now mostly used for legacy Windows builds. 18 | warning: "Should not be used for new projects" 19 | Security: 20 | Snyk: 21 | description: > 22 | Snyk is a tool for finding and fixing vulnerabilities in open source third party dependencies (libraries). 23 | Renovate: 24 | description: > 25 | [Renovate](https://github.com/renovatebot/renovate) is a Github tool for automatic dependency update, and is multi-platform and multi-language. It opens PRs when your dependencies become out of date. Ask in the #github channel for access. 26 | Upkeep: 27 | description: > 28 | Upkeep is a webapp for keeping track of maintenance of your Github projects. Ask in the #upkeep channel for questions. 29 | Authentication: 30 | Auth Sidecar: 31 | description: > 32 | A Docker sidecar for handling authentication patterns that comply with the Authentication Protocols RFC. More info here. 33 | Auth0: 34 | description: > 35 | Auth0 is the identity platform that we have chosen for our customers. It provides several features that allow for a more secure authentication experience. -------------------------------------------------------------------------------- /frontend/src/components/steps/ModuleSelection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import Typography from '@material-ui/core/Typography'; 4 | 5 | import { AppContext } from '../../context'; 6 | import HelpDialog from '../HelpDialog'; 7 | import { modulesHelp } from '../../help'; 8 | import { Paper } from '@material-ui/core'; 9 | import { useStyles } from '../../styles'; 10 | import Module from '../Module'; 11 | import { getCategoryName } from '../../utils/moduleHelpers'; 12 | 13 | const ModuleSelection = () => { 14 | const { categories, handleSelectModule } = useContext(AppContext); 15 | const classes = useStyles({}); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | Select your modules 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {Object.entries(categories).map(([categoryName, categoryModules]) => { 32 | return ( 33 | 34 | 35 | 36 | {getCategoryName(categoryModules)} 37 | 38 | {Object.entries(categoryModules).map( 39 | ([moduleKey, moduleObject]) => { 40 | if(moduleObject.minimumRisk !== 'Mandatory'){ 41 | return ( 48 | ) 49 | } 50 | return null; 51 | }, 52 | )} 53 | 54 | 55 | ); 56 | })} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default ModuleSelection; 63 | -------------------------------------------------------------------------------- /server/src/slack.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import * as AWS from 'aws-sdk'; 3 | import { region } from './config'; 4 | 5 | const WEBHOOK_SECRET_ID = process.env.WEBHOOK_SECRET_ID; 6 | 7 | const sm = new AWS.SecretsManager({ region }); 8 | const getSecretParams: AWS.SecretsManager.GetSecretValueRequest = { 9 | SecretId: WEBHOOK_SECRET_ID, 10 | }; 11 | 12 | let cachedSecretResponse: AWS.SecretsManager.GetSecretValueResponse | undefined; 13 | 14 | async function getSlackWebHook() { 15 | if (process.env.SLACK_WEB_HOOK) { 16 | return process.env.SLACK_WEB_HOOK; 17 | } 18 | 19 | // avoid looking up the secret every time 20 | cachedSecretResponse = 21 | cachedSecretResponse || 22 | (await sm.getSecretValue(getSecretParams).promise()); 23 | 24 | const parsedSecrets = JSON.parse(cachedSecretResponse.SecretString); 25 | return parsedSecrets.slack; 26 | } 27 | 28 | export async function sendMessage(message: string): Promise { 29 | const { 30 | SLACK_WEB_HOOK, 31 | WEBHOOK_SECRET_ID, 32 | SLACK_TARGET_CHANNEL, 33 | } = process.env; 34 | if (!SLACK_WEB_HOOK && !WEBHOOK_SECRET_ID) { 35 | console.log(`Slack alert ${prepareSlackMessage(message)}`); 36 | return; 37 | } 38 | 39 | const slackRequest = { 40 | channel: SLACK_TARGET_CHANNEL, 41 | username: 'Listo Bot', 42 | text: prepareSlackMessage(message), 43 | }; 44 | 45 | const slackWebHook = await getSlackWebHook(); 46 | 47 | return await fetch(slackWebHook, { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify(slackRequest), 53 | }); 54 | } 55 | 56 | /** 57 | * Converts a listo notification message into a string that displays nicely in Slack 58 | */ 59 | function prepareSlackMessage(message: string): string { 60 | function flatten(messageToParse: object, indent: number): string { 61 | let text = '\n'; 62 | for (const prop in messageToParse) { 63 | if (typeof messageToParse[prop] === 'object') { 64 | text += flatten(messageToParse[prop], indent + 1); 65 | } else { 66 | text += `${' '.repeat(indent)}${prop} : ${messageToParse[prop]} \n`; 67 | } 68 | } 69 | return text; 70 | } 71 | 72 | // recursively flatten to cover nested objects 73 | return flatten(JSON.parse(message), 0); 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/utils/moduleHelpers.ts: -------------------------------------------------------------------------------- 1 | import { ModuleCategory, ModuleCategories } from './../types/index'; 2 | import { Tools, ChecklistItem, Module } from '../types'; 3 | 4 | const getAllChecklistItems = (m: Module) => { 5 | return Object.values(m.checkLists).flatMap(checklist => Object.values(checklist)).flat(); 6 | }; 7 | 8 | export const getSelectedTools = (tools: Tools) => { 9 | return Object.keys(tools).flatMap(toolCategory => 10 | Object.keys(tools[toolCategory]).filter( 11 | tool => tools[toolCategory][tool].response, 12 | ), 13 | ); 14 | }; 15 | 16 | export const getSupportedTools = ( 17 | checkListItem: ChecklistItem, 18 | selectedTools: string[], 19 | ): string[] => { 20 | if (!checkListItem.tools) { 21 | return []; 22 | } 23 | return checkListItem.tools.filter(tool => selectedTools.includes(tool)); 24 | }; 25 | 26 | export const getNumberOfAnsweredQuestions = (m: Module, tools: Tools) => { 27 | if (!m.checkLists) { 28 | return 0; 29 | } 30 | 31 | const selectedTools = getSelectedTools(tools); 32 | const allChecklistItems = getAllChecklistItems(m); 33 | 34 | return allChecklistItems.map(checklistItem => { 35 | return getSupportedTools(checklistItem, selectedTools).length > 0; 36 | }) 37 | .filter(isAnswered => isAnswered).length; 38 | }; 39 | 40 | 41 | export const getNumberOfCheckListItems = (m: Module) => { 42 | if (!m.checkLists) { 43 | return 0; 44 | } 45 | return getAllChecklistItems(m).length; 46 | }; 47 | 48 | export const getCategoryName = (categoryData: ModuleCategory) => 49 | Object.values(categoryData)[0].category; 50 | 51 | export const getModuleDescription = (m: Module) => { 52 | if(!m) return ""; 53 | const description = [m.assessmentQuestion]; 54 | const resources = m.resources; 55 | const moduleDescription = m.guidance; 56 | 57 | if(moduleDescription) description.push('', '#### Guidance:', '', moduleDescription); 58 | 59 | if (resources) { 60 | description.push('', '#### Resources:', ''); 61 | description.push(...resources.map(resource => `+ ${resource}`)); 62 | } 63 | 64 | return description.join('\n'); 65 | } 66 | export const getModule = (categories: ModuleCategories, categoryName: string, moduleName: string) => { 67 | if(!categories || !Object.entries(categories).length) return undefined; 68 | const moduleCategory = categories[categoryName]; 69 | return moduleCategory ? moduleCategory[moduleName] : undefined; 70 | } -------------------------------------------------------------------------------- /frontend/src/SearchChecklists.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from '@reach/router'; 2 | import React, { useContext } from 'react'; 3 | import { Typography, Grid, Button, Paper, List, ListItem } from '@material-ui/core'; 4 | import { Meta, ModuleCategory } from './types'; 5 | import { AppContext } from './context'; 6 | import { getCategoryName } from './utils/moduleHelpers'; 7 | import { useStyles } from './styles'; 8 | 9 | interface ModuleListProps extends RouteComponentProps { 10 | categoryName: string; 11 | categoryModules: ModuleCategory; 12 | } 13 | 14 | const ModuleList = (props: ModuleListProps) => { 15 | const classes = useStyles({}); 16 | return ( 17 | 18 | 19 | 20 | {getCategoryName(props.categoryModules)} 21 | 22 | 23 | {Object.entries(props.categoryModules).map( 24 | ([moduleKey, moduleObject]) => { 25 | return ( 26 | 27 | 28 | 29 | {moduleObject.title} 30 | 31 | 32 | ); 33 | }, 34 | )} 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | } 42 | 43 | interface SearchChecklistsProps extends RouteComponentProps {}; 44 | 45 | export const SearchChecklists = (props: SearchChecklistsProps) => { 46 | const { categories } = useContext(AppContext); 47 | return ( 48 | 49 | 50 | 51 | 52 | Listo Checklists 53 | 54 | 55 | 56 | 57 | 58 | 59 | Search for and view individual checklists within Listo. 60 | 61 | 62 | {Object.entries(categories).map(([categoryName, categoryModules]) => { 63 | return ( 64 | 65 | ); 66 | })} 67 | 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /examples/pitch.md: -------------------------------------------------------------------------------- 1 | ## Situation [Given] 2 | 3 | “Move fast and break things”, the famous Facebook motto that Mark Zuckerberg gave to his developers in 2009. In order for SEEK to move quickly in a competitive landscape, we must accept failure. The question then becomes how do we fail without exposing SEEK or our customers to unnecessary risks. We call this “failing safely.” 4 | 5 | ## Complication [But] 6 | 7 | At SEEK we offer this safety through our collective expertise, which is concentrated in the security and architecture teams and distributed among senior developers across the organisation. 8 | 9 | We have collated this knowledge and expertise into a set of internally documented and community approved standards, known as RFCs (Request for Comment). 10 | 11 | However, both the architecture and security teams are vastly outnumbered by delivery teams and it is not possible to embed one of each into every team. Thus, engagement is ad hoc and often late in the software development lifecycle. RFCs are a dense collection of requirements, 80% might not apply to a project but the 20% that do are essential to failing safely. 12 | 13 | So how do we enable developers to move fast and fail safely? More importantly, how do we achieve this as we scale our product development capabilities? 14 | 15 | ## Resolution [How] 16 | 17 | To answer this question, we looked at the aviation and medical professions who face severe consequences as a result of failure. Both employed a very simple process to improve safety in a complex environment: checklists. 18 | 19 | Despite the huge increase in air travel, rates of incidents and related deaths have steadily decreased since checklists were introduced in the late 1930s. Surgeon, and author of “The Checklist Manifesto,” Atul Gawande devised a simple checklist for operating theatres and introduced them across 8 hospitals. The checklists cut death rates in half and reduced related complications by 36%. Looking closer to our own industry Slack and Salesforce have published articles describing their success using checklists to improve the quality and security of their products. 20 | 21 | Checklists, are at the heart of Listo. Listo empowers developers to perform a self-assessment that will quickly determine the relative risk of a project. The developer then selects from the full list of possible requirements, only those that apply to their project. Listo uses this information to create a Trello board with cards containing checklists for security and architecture concerns relevant to their assessment. 22 | 23 | The current focus of Listo is security and architecture but new assessments and checklists are easily added to cover other parts of the business like compliance and governance. 24 | 25 | So without further ado, let’s move onto a demo! 26 | -------------------------------------------------------------------------------- /frontend/src/Home.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from '@reach/router'; 2 | import React from 'react'; 3 | import { navigate } from '@reach/router'; 4 | import { Typography, Grid, Button } from '@material-ui/core'; 5 | import { Meta } from './types'; 6 | 7 | interface HomeProps extends RouteComponentProps { 8 | listoMeta: Meta; 9 | } 10 | 11 | export const Home = (props: HomeProps) => { 12 | const isSlack = props.listoMeta 13 | ? Boolean(props.listoMeta.slackChannelLink && props.listoMeta.slackChannel) 14 | : false; 15 | const isTrelloBoard = props.listoMeta 16 | ? Boolean(props.listoMeta.exampleTrelloBoardLink) 17 | : false; 18 | return ( 19 | 20 | 21 | 22 | 23 | Welcome to Listo 24 | 25 | 26 | 27 | 28 | 29 | 30 | This tool provides advice on security, reliability and architecture 31 | requirements when developing products. Think of it as insight into 32 | the collective technical knowledge of an engineering community. 33 | 34 | 35 | 36 | 37 | At the end of the review we’ll provide a link to a Trello board 38 | which contains the checklists for your project.{' '} 39 | {isTrelloBoard && ( 40 | 45 | Here is an example of a full Trello board. 46 | 47 | )} 48 | 49 | 50 | 51 | 52 | Got questions? Check out the FAQ's or message 53 | us on Slack{' '} 54 | {isSlack && ( 55 | 60 | {props.listoMeta.slackChannel} 61 | 62 | )} 63 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /frontend/src/components/steps/Summary.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { 3 | Box, 4 | Typography, 5 | List, 6 | ListItemText, 7 | ListItem, 8 | Grid, 9 | Paper, 10 | } from '@material-ui/core'; 11 | import { AppContext } from '../../context'; 12 | import { selectedCategories } from '../../utils/pickCategoriesWithResponse'; 13 | import { ModuleCategory } from '../../types'; 14 | import { getCategoryName } from '../../utils/moduleHelpers'; 15 | import Module from '../Module'; 16 | import { useStyles } from '../../styles'; 17 | 18 | const Summary = () => { 19 | const classes = useStyles({}); 20 | const { projectMeta, categories } = useContext(AppContext); 21 | 22 | const filteredCategories = selectedCategories(categories); 23 | 24 | return ( 25 | 26 | 27 | Listo Summary 28 | 29 | 30 | 31 | 32 | Please review that the information below is correct before we create 33 | your Trello Board! 34 | 35 | 36 | Project Information 37 | 38 | {projectMeta.map(meta => ( 39 | 40 | 44 | 45 | ))} 46 | 47 | 48 | 49 | Modules Selected 50 | 51 | 52 | {Object.entries(filteredCategories).map( 53 | ([categoryKey, categoryData]) => ( 54 | 55 | 56 | 61 | {getCategoryName(categoryData)} 62 | 63 | {Object.entries(categoryData).map( 64 | ([moduleKey, moduleObject]) => ( 65 | {}} 69 | categoryKey={categoryKey} 70 | moduleKey={moduleKey} 71 | readOnlyMode={true} 72 | /> 73 | ), 74 | )} 75 | 76 | 77 | ), 78 | )} 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default Summary; 86 | -------------------------------------------------------------------------------- /frontend/src/components/RiskCriteria.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Grid, 4 | Paper, 5 | Typography, 6 | RadioGroup, 7 | FormControlLabel, 8 | Radio, 9 | FormControl, 10 | ExpansionPanel, 11 | ExpansionPanelSummary, 12 | withStyles, 13 | } from '@material-ui/core'; 14 | import MuiExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; 15 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 16 | import { useStyles } from '../styles'; 17 | import { RiskOption } from '../types'; 18 | 19 | const UNSELECTED_KEY = 'unselected'; 20 | 21 | interface Props { 22 | text: string; 23 | description?: string; 24 | options: RiskOption[]; 25 | handleRiskOption: (event: React.ChangeEvent<{}>, value: string) => void; 26 | } 27 | 28 | const RiskCriteria = ({ 29 | text, 30 | options, 31 | handleRiskOption, 32 | description, 33 | }: Props) => { 34 | const classes = useStyles({}); 35 | 36 | if (!options) { 37 | // TODO: this should be moved to pre-validation 38 | return null; 39 | } 40 | 41 | const selectedOption = options.find(o => o.selected); 42 | const value = selectedOption ? selectedOption.text : UNSELECTED_KEY; 43 | 44 | const ExpansionPanelDetails = withStyles(theme => ({ 45 | root: { 46 | padding: theme.spacing(2), 47 | backgroundColor: '#f5f9fe', 48 | }, 49 | }))(MuiExpansionPanelDetails); 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | } 59 | aria-controls="panel1a-content" 60 | > 61 | {text} 62 | 63 | 64 | {description} 65 | 66 | 67 | 68 | 69 | 70 | 71 | {options.map(option => ( 72 | } 76 | label={option.text} 77 | /> 78 | ))} 79 | } 82 | style={{ display: 'none' }} 83 | label="Hidden" 84 | /> 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | export default RiskCriteria; 94 | -------------------------------------------------------------------------------- /frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectMeta { 2 | name: string; 3 | placeholder?: string; 4 | required?: boolean; 5 | label: string; 6 | type: 'input'; 7 | userResponse?: string; 8 | } 9 | 10 | export type ProjectMetaResponses = Record; 11 | 12 | export interface Meta { 13 | exampleTrelloBoardLink?: string; 14 | slackChannelLink?: string; 15 | slackChannel?: string; 16 | } 17 | 18 | export interface PickedCategories { 19 | [category: string]: string[]; 20 | } 21 | 22 | export interface AssessmentResult { 23 | selectedModulesByCategory: PickedCategories; 24 | selectedRisks: RiskSelection[]; 25 | projectMetaResponses: ProjectMetaResponses; 26 | selectedTools: string[]; 27 | } 28 | 29 | // Risks: 30 | export interface RiskOption { 31 | text: string; 32 | risk?: string; 33 | selected?: boolean; 34 | } 35 | 36 | export interface Risk { 37 | text: string; 38 | description?: string; 39 | options: RiskOption[]; 40 | } 41 | 42 | export interface RiskSelection { 43 | text: string; 44 | selection: string; 45 | } 46 | 47 | // Modules 48 | 49 | export interface ModuleCategories { 50 | [category: string]: ModuleCategory; 51 | } 52 | 53 | export interface ModuleCategory { 54 | [module: string]: Module; 55 | } 56 | 57 | export interface Module { 58 | title: string; 59 | category: string; 60 | assessmentQuestion: string; 61 | guidance?: string; 62 | response?: boolean; // User's response 63 | minimumRisk?: string; 64 | checkLists: Checklists; 65 | tags?: string; 66 | resources?: string[]; 67 | } 68 | 69 | export interface Checklists { 70 | [checklistName: string]: ChecklistItem[]; 71 | } 72 | 73 | export interface ChecklistItem { 74 | question: string; 75 | key?: string; 76 | tools?: string[]; 77 | checked?: boolean; // User's response 78 | } 79 | 80 | interface ProjectTypes { 81 | name: string; 82 | modules?: string[]; 83 | } 84 | 85 | export interface Tools { 86 | [category: string]: { [key: string]: Tool }; 87 | } 88 | 89 | export interface Tool { 90 | warning?: string; 91 | description?: string; 92 | response?: boolean; // User's response 93 | } 94 | 95 | // Data export 96 | export interface DirectoryData { 97 | data: { 98 | modules: ModuleCategories; 99 | projectMeta: ProjectMeta[]; 100 | risks: { 101 | questions: Risk[]; 102 | }; 103 | projectTypes: ProjectTypes[]; 104 | tooling: Tools; 105 | }; 106 | } 107 | 108 | export interface DatabaseModel { 109 | id?: string; 110 | createdAt?: string; 111 | updatedAt?: string; 112 | } 113 | 114 | export interface ProjectModel extends DatabaseModel { 115 | metaData: AssessmentResult; 116 | boardLink?: string; // Trello Board URL 117 | } 118 | 119 | export interface QuickChecklistModel extends DatabaseModel{ 120 | checkList: Checklists; 121 | projectId?: string; // We might use this in the future to link quick checklists to projects. 122 | } -------------------------------------------------------------------------------- /data/modules/test/test_long_checklist.yml: -------------------------------------------------------------------------------- 1 | category: Test 2 | assessmentQuestion: Test for modules that contain lots of checklists. 3 | guidance: > 4 | Hello this is a test description. 5 | minimumRisk: Low Risk 6 | checkLists: 7 | Diagram: 8 | - question: asdfadsfas 9 | - question: asdfadsfas 10 | - question: asdfadsfas 11 | Threats: 12 | - question: asdfadsfas 13 | - question: asdfadsfas 14 | - question: asdfadsfas 15 | - question: asdfadsfas 16 | Spoofing: 17 | - question: asdfadsfas 18 | - question: asdfadsfas 19 | - question: asdfadsfas 20 | - question: asdfadsfas 21 | - question: asdfadsfas 22 | - question: asdfadsfas 23 | - question: asdfadsfas 24 | - question: asdfadsfas 25 | - question: asdfadsfas 26 | - question: asdfadsfas 27 | - question: asdfadsfas 28 | Tampering: 29 | - question: asdfadsfas 30 | - question: asdfadsfas 31 | - question: asdfadsfas 32 | - question: asdfadsfas 33 | - question: asdfadsfas 34 | - question: asdfadsfas 35 | - question: asdfadsfas 36 | - question: asdfadsfas 37 | - question: asdfadsfas 38 | - question: asdfadsfas 39 | - question: asdfadsfas 40 | Repudiation: 41 | - question: asdfadsfas 42 | - question: asdfadsfas 43 | - question: asdfadsfas 44 | - question: asdfadsfas 45 | - question: asdfadsfas 46 | - question: asdfadsfas 47 | - question: asdfadsfas 48 | - question: asdfadsfas 49 | - question: asdfadsfas 50 | - question: asdfadsfas 51 | - question: asdfadsfas 52 | - question: asdfadsfas 53 | Information Disclosure: 54 | - question: asdfadsfas 55 | - question: asdfadsfas 56 | - question: asdfadsfas 57 | - question: asdfadsfas 58 | - question: asdfadsfas 59 | - question: asdfadsfas 60 | - question: asdfadsfas 61 | - question: asdfadsfas 62 | - question: asdfadsfas 63 | - question: asdfadsfas 64 | - question: asdfadsfas 65 | - question: asdfadsfas 66 | Denial of Service: 67 | - question: asdfadsfas 68 | - question: asdfadsfas 69 | - question: asdfadsfas 70 | - question: asdfadsfas 71 | - question: asdfadsfas 72 | - question: asdfadsfas 73 | - question: asdfadsfas 74 | - question: asdfadsfas 75 | - question: asdfadsfas 76 | - question: asdfadsfas 77 | - question: asdfadsfas 78 | - question: asdfadsfas 79 | Elevation of Privilege: 80 | - question: asdfadsfas 81 | - question: asdfadsfas 82 | - question: asdfadsfas 83 | - question: asdfadsfas 84 | - question: asdfadsfas 85 | - question: asdfadsfas 86 | - question: asdfadsfas 87 | - question: asdfadsfas 88 | - question: asdfadsfas 89 | resources: 90 | - test test 91 | - test test test 92 | tags: WebApp, API, Worker, iOS, Android 93 | title: Test Threat Modeling -------------------------------------------------------------------------------- /frontend/src/Faq.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from '@reach/router'; 2 | import React from 'react'; 3 | import { Typography, Grid } from '@material-ui/core'; 4 | import { Meta } from './types'; 5 | 6 | interface FaqProps extends RouteComponentProps { 7 | listoMeta: Meta; 8 | } 9 | 10 | export const Faq = (props: FaqProps) => ( 11 | 12 | 13 | 14 | 15 | Frequently asked questions 16 | 17 | 18 | 19 | 20 | 21 | 22 | How do I change my project details after I’ve submitted a request? 23 | 24 | 25 | Unfortunately, it isn’t possible to modify project details yourself. 26 | However, all the information collected during the process will be 27 | reflected on the generated Trello board. You should be granted edit 28 | access to your Trello board which means the project information can be 29 | updated there. 30 | 31 | 32 | 33 | 34 | How do I find the link to my board? 35 | 36 | 37 | The easiest way to find the link to your Trello board is by visiting 38 | the {props.listoMeta.slackChannel} Slack channel if you created it 39 | recently. If you can’t find your board there, reach out in{' '} 40 | {props.listoMeta.slackChannel} and the team can help you find the 41 | link. 42 | 43 | 44 | 45 | 46 | I’m building a new service, what are the fundamentals I need to be 47 | aware of? 48 | 49 | 50 | Listo will guide you through the process of selecting tools and 51 | modules that would apply to your initiative and ultimately determine 52 | the risk 53 | 54 | 55 | 56 | 57 | What are you basing the decisions on? 58 | 59 | 60 | The content exposed by Listo is open for anyone to contribute to by 61 | opening Pull Requests against the Listo GitHub project. By using Pull 62 | Requests it’s up to the community of Listo users to define the 63 | content. Data can come from other places like AWS Best Practices, OWAP 64 | Top 10 and other patterns and practices. 65 | 66 | 67 | 68 | 69 | How do I provide feedback about Listo? 70 | 71 | 72 | If you are reporting a bug or would like to request a feature, please 73 | raise a GitHub issue. 74 | 75 | 76 | 77 | 78 | ); 79 | -------------------------------------------------------------------------------- /frontend/src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import MuiExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; 2 | import MuiExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'; 3 | import MuiExpansionPanel from '@material-ui/core/ExpansionPanel'; 4 | import { makeStyles, withStyles } from '@material-ui/core'; 5 | 6 | export const useStyles = makeStyles(theme => ({ 7 | root: { 8 | padding: theme.spacing(3, 2), 9 | }, 10 | logo: { 11 | textDecoration: 'none', 12 | color: theme.palette.text.primary, 13 | }, 14 | menuItem: { 15 | paddingLeft: theme.spacing(3), 16 | paddingRight: theme.spacing(3), 17 | }, 18 | asessment: { 19 | padding: theme.spacing(3, 2), 20 | textAlign: 'center', 21 | }, 22 | appBar: { 23 | position: 'relative', 24 | }, 25 | layout: { 26 | width: 'auto', 27 | marginLeft: theme.spacing(2), 28 | marginRight: theme.spacing(2), 29 | [theme.breakpoints.up(800 + theme.spacing(4))]: { 30 | width: 800, 31 | marginLeft: 'auto', 32 | marginRight: 'auto', 33 | }, 34 | }, 35 | paper: { 36 | marginTop: theme.spacing(3), 37 | marginBottom: theme.spacing(3), 38 | padding: theme.spacing(2), 39 | [theme.breakpoints.up(600 + theme.spacing(6))]: { 40 | marginTop: theme.spacing(6), 41 | marginBottom: theme.spacing(6), 42 | padding: theme.spacing(3), 43 | }, 44 | }, 45 | stepper: { 46 | padding: `${theme.spacing(3)}px 0 ${theme.spacing(5)}px`, 47 | }, 48 | buttons: { 49 | display: 'flex', 50 | justifyContent: 'flex-end', 51 | }, 52 | button: { 53 | marginTop: theme.spacing(3), 54 | marginLeft: theme.spacing(1), 55 | }, 56 | stepLabels: { 57 | cursor: 'pointer', 58 | }, 59 | checklistSummary: { 60 | display: 'flex', 61 | width: '100%', 62 | justifyContent: 'space-between', 63 | }, 64 | checklistQuestion: { 65 | alignSelf: 'start', 66 | paddingTop: '14px', 67 | }, 68 | checkboxIcon: { 69 | alignSelf: 'baseline', 70 | paddingTop: '14px', 71 | }, 72 | questionIcon: { 73 | paddingRight: '10px', 74 | }, 75 | toolsWrapper: { 76 | display: 'flex', 77 | }, 78 | toolChip: { 79 | margin: '5px', 80 | }, 81 | column: { 82 | width: '100%', 83 | }, 84 | answeredQuestionsChip: { 85 | backgroundColor: '#d1f9d0', 86 | border: 'none', 87 | color: '#657764', 88 | }, 89 | })); 90 | 91 | export const ExpansionPanelSummary = withStyles({ 92 | root: { 93 | borderBottom: '1px solid rgba(0, 0, 0, .125)', 94 | minHeight: 56, 95 | '&$expanded': { 96 | minHeight: 56, 97 | boxShadow: '1px 3px 3px rgba(0, 0, 0, 0.2)', 98 | }, 99 | }, 100 | content: { 101 | '&$expanded': { 102 | margin: '12px 0', 103 | boxShadow: 'none', 104 | }, 105 | }, 106 | expanded: {}, 107 | })(MuiExpansionPanelSummary); 108 | 109 | export const ExpansionPanel = withStyles({ 110 | root: { 111 | border: 'none', 112 | '&:not(:last-child)': { 113 | borderBottom: 0, 114 | }, 115 | '&:before': { 116 | display: 'none', 117 | }, 118 | '&$expanded': { 119 | margin: 'auto', 120 | }, 121 | }, 122 | expanded: {}, 123 | })(MuiExpansionPanel); 124 | 125 | export const ExpansionPanelDetails = withStyles(theme => ({ 126 | root: { 127 | padding: theme.spacing(2), 128 | backgroundColor: '#f5f9fe', 129 | }, 130 | }))(MuiExpansionPanelDetails); 131 | -------------------------------------------------------------------------------- /server/src/diskdb.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as uuid from 'uuid'; 3 | import { Repository, isValidProject, isValidQuickChecklist } from './types'; 4 | import { 5 | ProjectModel, 6 | QuickChecklistModel, 7 | } from '../../frontend/src/types'; 8 | import * as lockfile from 'proper-lockfile'; 9 | 10 | export class Disk implements Repository { 11 | db: Map; 12 | diskPath: string; 13 | 14 | constructor(diskPath: string) { 15 | this.diskPath = diskPath || './db.json'; 16 | } 17 | 18 | async saveDB() { 19 | const options = { stale: 5000, retries: 2 }; 20 | const release = await lockfile.lock(this.diskPath, options); 21 | const serialiseDB = JSON.stringify(Array.from(this.db.entries())); 22 | await fs.writeFile(this.diskPath, serialiseDB); 23 | await release(); 24 | } 25 | 26 | async fetchDB() { 27 | const file = await fs.readFile(this.diskPath, 'utf-8'); 28 | this.db = new Map(JSON.parse(file)); 29 | } 30 | 31 | public async init() { 32 | try { 33 | await this.fetchDB(); 34 | console.log( 35 | `A Disk database was found here: ${this.diskPath}. Loading it now...`, 36 | ); 37 | } catch (err) { 38 | this.db = new Map(); 39 | console.debug(`${err}`); 40 | console.log( 41 | `No Disk database found. Creating one here: ${this.diskPath}`, 42 | ); 43 | await fs.writeFile(this.diskPath, ''); 44 | } 45 | } 46 | 47 | public async create(project: ProjectModel): Promise { 48 | const date = new Date().toISOString(); 49 | const projectId = uuid.v4(); 50 | 51 | project.id = projectId; 52 | project.boardLink = ''; 53 | project.createdAt = date; 54 | project.updatedAt = date; 55 | 56 | this.db.set(projectId, project); 57 | await this.saveDB(); 58 | 59 | return projectId; 60 | } 61 | 62 | public async update(projectId: string, boardLink: string): Promise { 63 | const project = this.db.get(projectId); 64 | 65 | if (!isValidProject(project)) { 66 | throw `Can't find project with id: ${projectId}`; 67 | } 68 | 69 | project.boardLink = boardLink; 70 | project.updatedAt = new Date().toISOString(); 71 | 72 | this.db.set(projectId, project); 73 | await this.saveDB(); 74 | 75 | return projectId; 76 | } 77 | 78 | public async get(projectId: string): Promise { 79 | const project = this.db.get(projectId); 80 | 81 | if (!isValidProject(project)) { 82 | throw `Can't find project with id: ${projectId}`; 83 | } 84 | 85 | return project; 86 | } 87 | 88 | public async getQuickChecklist(id: string): Promise { 89 | const quickChecklist = this.db.get(id); 90 | 91 | if (!isValidQuickChecklist(quickChecklist)) { 92 | throw `Can't find QuickChecklist with id: ${id}`; 93 | } 94 | 95 | return quickChecklist; 96 | } 97 | 98 | public async upsertQuickChecklist( 99 | quickChecklist: QuickChecklistModel, 100 | ): Promise { 101 | const date = new Date().toISOString(); 102 | 103 | try { 104 | await this.getQuickChecklist(quickChecklist.id); 105 | } catch (err) { 106 | // Create a new QuickChecklist 107 | const id = uuid.v4(); 108 | quickChecklist.id = id; 109 | quickChecklist.createdAt = date; 110 | } 111 | 112 | quickChecklist.updatedAt = date; 113 | this.db.set(quickChecklist.id, quickChecklist); 114 | await this.saveDB(); 115 | 116 | return quickChecklist.id; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/listo_pink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](logo.png) 2 | 3 | [![Known Vulnerabilities Frontend](https://snyk.io/test/github/seek-oss/listo/badge.svg?targetFile=frontend/package.json)](https://snyk.io/test/github/seek-oss/listo?targetFile=frontend/package.json) [![Known Vulnerabilities Server](https://snyk.io/test/github/seek-oss/listo/badge.svg?targetFile=server/package.json)](https://snyk.io/test/github/seek-oss/listo?targetFile=server/package.json) 4 | 5 | Use questionnaires and checklists to make it easy to do the right thing, regarding the software you build. 6 | 7 | ## About 8 | 9 | Checklists are at the heart of Listo, empowering engineering teams to perform a web-based self-assessment, which results in a Trello board containing the essential security, reliability and architecture requirements from our RFCs, tailored to a project's objectives. 10 | 11 | A more detailed blog post can be found [on SEEK's Tech Blog](https://medium.com/seek-blog/listo-failing-safely-with-checklists-and-rfc-s-d14b6fa34b2f). 12 | 13 | ## Getting Listo Running Locally 14 | 15 | The quickest way to get Listo running locally is to launch it via Docker Compose. 16 | 17 | 1. Install Docker and Docker Compose (Mac users can install both [here](https://docs.docker.com/docker-for-mac/install/)). 18 | 2. Create an [env.sh](examples/TEMPLATE_env.sh) file in the root directory: 19 | 20 | ```bash 21 | # Get your API Key here -> https://trello.com/app-key/ 22 | export TRELLO_API_KEY=e94947...00a92 23 | 24 | # Click on the "Generate a Token" link here -> https://trello.com/app-key/. 25 | export TRELLO_TOKEN=fda876d8af87d6fa876adfa....8516dcf715 26 | ``` 27 | 28 | 3. In the root directory, start the Listo service (server and UI): 29 | 30 | ```bash 31 | $ make serve 32 | ``` 33 | 4. [OPTIONAL] Once you have Listo running locally you can now customise the checklists and questions for your own requirements [here](data/). 34 | 35 | 36 | ## Setting up Listo for Development 37 | 38 | If you want to modify or debug Listo's code, it's often easier without using Docker or Docker Compose. 39 | 40 | ### Requirements 41 | 42 | Listo requires [Yarn](https://yarnpkg.com/). 43 | 44 | > Note you will still need to have the `env.sh` configured as per [Getting Listo Running Locally](#getting-listo-running-locally). 45 | 46 | ### Server 47 | In the `server` directory: 48 | 49 | ```bash 50 | $ make serve 51 | ``` 52 | 53 | > See the [Makefile](./server/Makefile) for more options. 54 | 55 | ### UI 56 | 57 | In the `frontend` directory: 58 | 59 | ```bash 60 | $ make serve 61 | ``` 62 | 63 | > See the [Makefile](./frontend/Makefile) for more options. 64 | 65 | The browser should auto open, if not you can navigate to: 66 | 67 | [http://localhost:3000/](http://localhost:3000/) 68 | 69 | ### Running Local DynamoDB 70 | If you would like to run the local DynamoDB (instead of the default DiskDB) to test changes to that integration: 71 | 72 | 1. Within the server directory and before starting the server run the following command (make sure to uncomment the necessary exports within your env file first). 73 | 74 | ```bash 75 | $ make start_db 76 | ``` 77 | 78 | ## Deploying Listo into Production 79 | 80 | Deploying Listo for production use requires an AWS account with access to a DynamoDB Table. However, we plan to support other DB's in the future to remove the AWS requirement. 81 | 82 | We have a separate build repo internally that picks up this repo and deploys Listo internally with a separate set of custom questions and checklists. We add authentication (using an internal service) and use AWS Secret's Manager for storing credentials). An example of our build process can be found [here](examples/deploy). 83 | 84 | ## References 85 | 86 | + Sample questions and checklists can be found in the [data directory](data/). 87 | + Listo was influenced by [goSDL](https://github.com/slackhq/goSDL). 88 | 89 | ## License 90 | 91 | MIT. 92 | -------------------------------------------------------------------------------- /frontend/src/components/Tooling.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, Fragment } from 'react'; 2 | import { AppContext } from '../context'; 3 | import { Tool } from '../types'; 4 | import { 5 | Checkbox, 6 | FormControlLabel, 7 | Typography, 8 | Box, 9 | Grid, 10 | Paper, 11 | } from '@material-ui/core'; 12 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 13 | import { 14 | useStyles, 15 | ExpansionPanel, 16 | ExpansionPanelSummary, 17 | ExpansionPanelDetails, 18 | } from '../styles'; 19 | import ReactMarkdown from 'react-markdown'; 20 | import { toolsHelp } from '../help'; 21 | import HelpDialog from './HelpDialog'; 22 | 23 | const ToolingComponent = () => { 24 | const { tools } = useContext(AppContext); 25 | return ( 26 | 27 | 28 | 29 | 30 | Select your tools 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {Object.keys(tools).map((category, index) => ( 39 | 44 | ))} 45 | 46 | 47 | ); 48 | }; 49 | 50 | interface ToolsCategoryComponentProps { 51 | category: string; 52 | tools: { [key: string]: Tool }; 53 | } 54 | 55 | const ToolCategoryComponent = ({ 56 | category, 57 | tools, 58 | }: ToolsCategoryComponentProps) => { 59 | const classes = useStyles({}); 60 | return ( 61 | 62 | 63 | 64 | {category} 65 | 66 | {Object.keys(tools).map(tool => ( 67 | 73 | ))} 74 | 75 | 76 | ); 77 | }; 78 | 79 | interface ToolComponentProps extends Tool { 80 | name: string; 81 | category: string; 82 | } 83 | 84 | const ToolComponent = ({ 85 | category, 86 | name, 87 | description, 88 | warning, 89 | response, 90 | }: ToolComponentProps) => { 91 | const classes = useStyles(); 92 | const { handleSelectTool } = useContext(AppContext); 93 | 94 | return ( 95 | 96 | } 98 | aria-controls="panel1a-content" 99 | id="panel1a-header" 100 | > 101 | 102 | 103 | event.stopPropagation()} 105 | control={ 106 | { 110 | handleSelectTool(name, category, event.target.checked); 111 | }} 112 | /> 113 | } 114 | label={name} 115 | /> 116 | 117 | 118 | {warning} 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | export default ToolingComponent; 130 | -------------------------------------------------------------------------------- /frontend/src/context/StepContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { AppContext } from '.'; 3 | import { API_URL } from '../constants'; 4 | import { getRiskLevel } from '../utils'; 5 | 6 | export type STEP_TYPES = 7 | | 'Project Details' 8 | | 'Risk Assessment' 9 | | 'Tools' 10 | | 'Modules' 11 | | 'Summary'; 12 | 13 | export const INFO_STEP: STEP_TYPES = 'Project Details'; 14 | export const RISK_STEP: STEP_TYPES = 'Risk Assessment'; 15 | export const TOOLS_STEP: STEP_TYPES = 'Tools'; 16 | export const MODULES_STEP: STEP_TYPES = 'Modules'; 17 | export const SUMMARY_STEP: STEP_TYPES = 'Summary'; 18 | 19 | export const STEPS = [ 20 | INFO_STEP, 21 | RISK_STEP, 22 | TOOLS_STEP, 23 | MODULES_STEP, 24 | SUMMARY_STEP, 25 | ] as STEP_TYPES[]; 26 | 27 | const initContextNoop = (...args: any[]) => { 28 | console.error('not implemented'); 29 | void undefined; 30 | }; 31 | 32 | const initialStepContext = { 33 | handleBack: initContextNoop, 34 | handleNext: initContextNoop, 35 | handleGoToStep: initContextNoop, 36 | checkStepValid: (stepIndex: number) => true, 37 | activeStep: 0, 38 | loading: false, 39 | setLoading: (value: boolean) => {}, 40 | }; 41 | 42 | export const StepContext = React.createContext(initialStepContext); 43 | 44 | export const StepProvider: React.FC = ({ children }) => { 45 | const [activeStep, setStep] = useState(0); 46 | const { prepareResult, risks } = useContext(AppContext); 47 | const [loading, setLoading] = useState(false); 48 | 49 | const handleBack = () => { 50 | if (activeStep > 0) { 51 | setStep(activeStep - 1); 52 | } 53 | }; 54 | 55 | const handleGoToStep = (stepName: STEP_TYPES) => { 56 | const index = STEPS.findIndex(step => step === stepName); 57 | 58 | setStep(index); 59 | }; 60 | 61 | const isStep = (currentStepIndex: number, stepName: STEP_TYPES) => { 62 | return currentStepIndex === STEPS.findIndex(step => step === stepName); 63 | }; 64 | 65 | const checkStepValid = (stepIndex: number) => { 66 | if (process.env.NODE_ENV === 'development') { 67 | return true; 68 | } 69 | if (isStep(activeStep, INFO_STEP)) { 70 | // TODO: need to check which ones are required and only check those 71 | return true; 72 | } 73 | if (isStep(activeStep, RISK_STEP)) { 74 | const riskLevel = getRiskLevel(risks); 75 | return Boolean(riskLevel); 76 | } 77 | if (isStep(activeStep, TOOLS_STEP)) { 78 | return true; 79 | } 80 | if (isStep(activeStep, MODULES_STEP)) { 81 | const selectedModulesByCategory = prepareResult() 82 | .selectedModulesByCategory; 83 | 84 | return Object.keys(selectedModulesByCategory).length > 0; 85 | } 86 | return true; 87 | }; 88 | 89 | const handleNext = async () => { 90 | if (!checkStepValid(activeStep)) { 91 | return; 92 | } 93 | 94 | if (isStep(activeStep, SUMMARY_STEP)) { 95 | try { 96 | const res = await fetch(`${API_URL}/project`, { 97 | method: 'POST', 98 | headers: { 99 | 'Content-Type': 'application/json', 100 | }, 101 | body: JSON.stringify(prepareResult()), 102 | }); 103 | const data = await res.json(); 104 | window.location.href = `/project/${data.id}`; 105 | return; 106 | } catch (err) { 107 | console.error(err); 108 | } 109 | } 110 | setStep(activeStep + 1); 111 | window.scrollTo(0, 0); 112 | setLoading(false); 113 | }; 114 | 115 | return ( 116 | 127 | {children} 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /frontend/src/components/Module.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { 3 | Checkbox, 4 | FormControlLabel, 5 | Typography, 6 | Box, 7 | Chip, 8 | ExpansionPanelSummary, 9 | ExpansionPanel, 10 | ExpansionPanelDetails, 11 | Grid, 12 | } from '@material-ui/core'; 13 | import { Module } from '../types'; 14 | import { HandleClickCheckbox, AppContext } from '../context'; 15 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 16 | import ReactMarkdown from 'react-markdown'; 17 | import { 18 | getNumberOfAnsweredQuestions, 19 | getNumberOfCheckListItems, 20 | } from '../utils/moduleHelpers'; 21 | import Checklists from './Checklists'; 22 | import { useStyles } from '../styles'; 23 | 24 | interface Props { 25 | moduleObject: Module; 26 | handleSelectModule: HandleClickCheckbox; 27 | categoryKey: string; 28 | moduleKey: string; 29 | readOnlyMode?: boolean; 30 | } 31 | 32 | const ModuleComponent = ({ 33 | moduleObject, 34 | handleSelectModule, 35 | categoryKey, 36 | moduleKey, 37 | readOnlyMode = false, 38 | }: Props) => { 39 | const { tools } = useContext(AppContext); 40 | const classes = useStyles(); 41 | 42 | const numberOfAnsweredQuestions = getNumberOfAnsweredQuestions( 43 | moduleObject, 44 | tools, 45 | ); 46 | 47 | const numberOfCheckListItems = getNumberOfCheckListItems(moduleObject); 48 | 49 | return ( 50 | 51 | } 53 | aria-controls="panel1a-content" 54 | id="panel1a-header" 55 | > 56 | 57 | 58 | {readOnlyMode && ( 59 | 60 | {moduleObject.title} 61 | 62 | )} 63 | {!readOnlyMode && ( 64 | event.stopPropagation()} 66 | control={ 67 | { 70 | handleSelectModule( 71 | categoryKey, 72 | moduleKey, 73 | event.target.checked, 74 | ); 75 | }} 76 | color="primary" 77 | /> 78 | } 79 | label={moduleObject.title} 80 | /> 81 | )} 82 | {readOnlyMode || Boolean(moduleObject.response) ? ( 83 | 0 89 | ? classes.answeredQuestionsChip 90 | : '' 91 | } 92 | /> 93 | ) : null} 94 | 95 | 96 | {moduleObject.assessmentQuestion} 97 | 98 | 99 | 100 | 101 | 107 | {moduleObject.guidance ? 108 | ( 109 | 110 | 111 | 112 | ) : null 113 | } 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | export default ModuleComponent; 124 | -------------------------------------------------------------------------------- /frontend/src/components/Checklists.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from 'react'; 2 | import { 3 | Typography, 4 | List, 5 | ListItem, 6 | Box, 7 | Chip, 8 | Checkbox, 9 | } from '@material-ui/core'; 10 | import { AppContext } from '../context'; 11 | import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; 12 | import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye'; 13 | import { useStyles, } from '../styles'; 14 | import ReactMarkdown from 'react-markdown'; 15 | import { 16 | getSupportedTools, 17 | getSelectedTools, 18 | } from '../utils/moduleHelpers'; 19 | import { Checklists, ChecklistItem } from '../types'; 20 | 21 | interface Props { 22 | readOnlyMode: boolean; 23 | checklists: Checklists; 24 | } 25 | interface ChecklistProps { 26 | toolsSupported: string[]; 27 | readOnlyMode: boolean; 28 | checklistItem: ChecklistItem; 29 | checklistName: string; 30 | checklistItemIndex: number; 31 | } 32 | 33 | const ListoCheckbox = (props: ChecklistProps) => { 34 | const classes = useStyles(); 35 | const { handleSelectChecklistItem } = useContext(AppContext); 36 | if (props.toolsSupported.length) { 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } else if (!props.readOnlyMode) { 43 | return ( { 46 | handleSelectChecklistItem( 47 | props.checklistName, 48 | props.checklistItemIndex, 49 | event.target.checked 50 | ); 51 | }} 52 | color="primary" className={classes.checkboxIcon} />); 53 | } 54 | return ( 55 |
56 | 57 |
58 | ); 59 | }; 60 | 61 | const ChecklistsContainer = ({ 62 | readOnlyMode, 63 | checklists 64 | }: Props) => { 65 | const { tools } = useContext(AppContext); 66 | const classes = useStyles(); 67 | 68 | const selectedTools = getSelectedTools(tools); 69 | 70 | return ( 71 | 72 | {Object.entries(checklists).map( 73 | ([checklistName, checklistItems]) => { 74 | return ( 75 | 76 | {checklistName} 77 | {checklistItems.map((checklistItem, checklistItemIndex) => { 78 | const toolsSupported = getSupportedTools( 79 | checklistItem, 80 | selectedTools, 81 | ); 82 | return ( 83 | 84 | 90 | 91 | 92 |
93 | {toolsSupported.map((tool, index) => ( 94 | 99 | )) 100 | } 101 | {!readOnlyMode && checklistItem.tools ? checklistItem.tools.map((tool, index) => ( 102 | 107 | )) : null 108 | } 109 |
110 |
111 |
112 | ); 113 | })} 114 |
115 | ); 116 | }, 117 | )} 118 |
119 | ); 120 | }; 121 | 122 | export default ChecklistsContainer; -------------------------------------------------------------------------------- /frontend/src/QuickChecklist.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from '@reach/router'; 2 | import React, { useContext, useEffect, useState } from 'react'; 3 | import { Typography, CircularProgress, Button } from '@material-ui/core'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import { getModuleDescription, getModule } from './utils/moduleHelpers'; 6 | import { AppContext } from './context'; 7 | import { API_URL } from './constants'; 8 | import { QuickChecklistModel } from './types'; 9 | import ChecklistsContainer from './components/Checklists'; 10 | import { useStyles } from './styles'; 11 | 12 | interface QuickChecklistProps extends RouteComponentProps { 13 | moduleName?: string; 14 | categoryName?: string; 15 | id?: string; 16 | } 17 | 18 | export const QuickChecklist = (props: QuickChecklistProps) => { 19 | const { categories, quickChecklist, initQuickChecklist } = useContext(AppContext); 20 | const [loading, setLoading] = useState(false); 21 | const [errorState, setErrorState] = useState(false); 22 | const classes = useStyles(); 23 | const categoryName = props.categoryName ?? ""; 24 | const moduleName = props.moduleName ?? ""; 25 | let id = props.id; 26 | const module = (categoryName && moduleName) ? getModule(categories, categoryName, moduleName) : undefined; 27 | 28 | const prepareQuickChecklist = () => { 29 | const quickChecklistData: QuickChecklistModel = { checkList: quickChecklist }; 30 | if(id) quickChecklistData.id = id; 31 | return quickChecklistData; 32 | } 33 | 34 | const save = async () => { 35 | try { 36 | const res = await fetch(`${API_URL}/quick-checklist`, { 37 | method: 'PUT', 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | }, 41 | body: JSON.stringify(prepareQuickChecklist()), 42 | }); 43 | if (res.status !== 200) throw new Error(`Quick Checklist with id ${id} can't be saved`); 44 | const data = await res.json(); 45 | window.location.href = `/checklist/${categoryName}/${moduleName}/${data.id}`; 46 | } catch (err) { 47 | console.log(`Error saving the QuickChecklist: ${err}`); 48 | } 49 | 50 | } 51 | 52 | useEffect(() => { 53 | const fetchData = async () => { 54 | try { 55 | if(!id) return; 56 | const res = await fetch(`${API_URL}/quick-checklist/${id}`, { 57 | method: 'GET', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | } 61 | }); 62 | 63 | if (res.status !== 200) throw new Error(`Quick Checklist with id ${id} not found`); 64 | const data = await res.json(); 65 | const quickChecklistRes: QuickChecklistModel = data.quickChecklist; 66 | initQuickChecklist(quickChecklistRes.checkList); 67 | 68 | } catch (err) { 69 | setErrorState(true); 70 | console.log(`Error fetching the QuickChecklist: ${err}`); 71 | } 72 | }; 73 | fetchData(); 74 | }, [id]); 75 | 76 | if (!module || errorState) { 77 | return ( 78 | 79 | 80 | Listo Module Not Found 81 | 82 | 83 | 84 | We can't seem to find your module with category and title of: {`${categoryName} -> ${moduleName}`}. 85 | 86 | 87 | ); 88 | } else { 89 | 90 | if(Object.keys(quickChecklist).length === 0){ 91 | initQuickChecklist(module.checkLists); 92 | } 93 | 94 | return ( 95 | 96 | 97 | Module - {module.title} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 | {loading ? ( 107 | 108 | ) : ( 109 | 117 | )} 118 |
119 | ); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /server/src/dynamodb.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import * as uuid from 'uuid'; 3 | import { Repository, isValidProject, isValidQuickChecklist } from './types'; 4 | import { 5 | ProjectModel, 6 | QuickChecklistModel, 7 | } from '../../frontend/src/types'; 8 | import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'; 9 | 10 | export class Dynamo implements Repository { 11 | db: AWS.DynamoDB.DocumentClient; 12 | tableName: string; 13 | dynamoConfigOptions: ServiceConfigurationOptions; 14 | 15 | constructor( 16 | dynamoConfigOptions: ServiceConfigurationOptions, 17 | tableName: string, 18 | ) { 19 | this.dynamoConfigOptions = dynamoConfigOptions; 20 | this.dynamoConfigOptions.region = 21 | dynamoConfigOptions.region || 'ap-southeast-2'; 22 | this.tableName = tableName || 'Projects'; 23 | this.db = new AWS.DynamoDB.DocumentClient(this.dynamoConfigOptions); 24 | } 25 | 26 | public async init() { 27 | if ( 28 | process.env.LISTO_DATABASE === 'Dynamo' && 29 | process.env.CREATE_DYNAMO_TABLES 30 | ) { 31 | const params = { 32 | TableName: this.tableName, 33 | KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }], 34 | AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }], 35 | ProvisionedThroughput: { 36 | ReadCapacityUnits: 5, 37 | WriteCapacityUnits: 5, 38 | }, 39 | }; 40 | 41 | const dynamoClient = new AWS.DynamoDB(this.dynamoConfigOptions); 42 | console.debug('dynamo config: ', this.dynamoConfigOptions); 43 | await dynamoClient.createTable(params).promise(); 44 | const data = await dynamoClient.listTables().promise(); 45 | console.log('Created table.', JSON.stringify(data, null, 2)); 46 | } 47 | } 48 | 49 | public async create(project: ProjectModel): Promise { 50 | const date = new Date().toISOString(); 51 | const projectId = uuid.v4(); 52 | 53 | project.id = projectId; 54 | project.boardLink = null; 55 | project.createdAt = date; 56 | project.updatedAt = date; 57 | 58 | const params = { 59 | TableName: this.tableName, 60 | Item: project, 61 | }; 62 | 63 | await this.db.put(params).promise(); 64 | return projectId; 65 | } 66 | 67 | public async update(projectId: string, boardLink: string): Promise { 68 | const params = { 69 | TableName: this.tableName, 70 | Key: { 71 | id: projectId, 72 | }, 73 | UpdateExpression: 'set boardLink = :link, updatedAt = :time', 74 | ExpressionAttributeValues: { 75 | ':link': boardLink, 76 | ':time': new Date().toISOString(), 77 | }, 78 | ReturnValues: 'UPDATED_NEW', 79 | }; 80 | 81 | const resp = await this.db.update(params).promise(); 82 | console.log('Successfully updated item', JSON.stringify(resp)); 83 | return projectId; 84 | } 85 | 86 | public async get(projectId: string): Promise { 87 | const params = { 88 | TableName: this.tableName, 89 | Key: { 90 | id: projectId, 91 | }, 92 | }; 93 | 94 | const data = await this.db.get(params).promise(); 95 | const project = data.Item; 96 | 97 | if (!isValidProject(project)) { 98 | throw 'Project not found'; 99 | } 100 | 101 | return project; 102 | } 103 | 104 | public async getQuickChecklist(id: string): Promise { 105 | const params = { 106 | TableName: this.tableName, 107 | Key: { 108 | id: id, 109 | }, 110 | }; 111 | 112 | const data = await this.db.get(params).promise(); 113 | const quickChecklist = data.Item; 114 | 115 | if (!isValidQuickChecklist(quickChecklist)) { 116 | throw 'Checklist not found'; 117 | } 118 | 119 | return quickChecklist; 120 | } 121 | 122 | public async upsertQuickChecklist( 123 | quickChecklist: QuickChecklistModel, 124 | ): Promise { 125 | const date = new Date().toISOString(); 126 | const params = { 127 | TableName: this.tableName, 128 | Item: quickChecklist, 129 | }; 130 | 131 | try { 132 | await this.getQuickChecklist(quickChecklist.id); 133 | } catch (err) { 134 | // Create a new QuickChecklist 135 | const id = uuid.v4(); 136 | quickChecklist.id = id; 137 | quickChecklist.createdAt = date; 138 | } 139 | 140 | quickChecklist.updatedAt = date; 141 | await this.db.put(params).promise(); 142 | 143 | return quickChecklist.id; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /server/src/data.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | const immutable = require('object-path-immutable'); 5 | const yaml = require('js-yaml'); 6 | import { DirectoryData } from '../../frontend/src/types'; 7 | 8 | const Ajv = require('ajv'); 9 | 10 | const readdir = promisify(fs.readdir); 11 | const readFile = promisify(fs.readFile); 12 | 13 | async function yamlFileToJson(filePath: string) { 14 | try { 15 | const { name } = path.parse(filePath); 16 | // Note: the safeLoad function is delightfully robust and will correctly 17 | // parse JSON, and plain text files into the resulting structure. 18 | // This may not be desired, if so a possible work around would be to filter 19 | // by file extension and only process .yml or .yaml files 20 | return { 21 | [name]: await yaml.safeLoad(await readFile(filePath)), 22 | }; 23 | } catch (ex) { 24 | console.error(`Unable to process ${filePath}`, ex); 25 | } 26 | return {}; 27 | } 28 | 29 | async function reduceDirectory(directory: string) { 30 | const dirEntries = await readdir(directory, { withFileTypes: true }); 31 | let retVal = {}; 32 | 33 | for (const entry of dirEntries) { 34 | const filePath = `${directory}/${entry.name}`; 35 | const namespace = directory.split('/').pop(); 36 | retVal = immutable.merge( 37 | retVal, 38 | namespace, 39 | entry.isDirectory() 40 | ? await reduceDirectory(filePath) 41 | : await yamlFileToJson(filePath), 42 | ); 43 | } 44 | 45 | return retVal; 46 | } 47 | 48 | async function validate(schema: object, json: string): Promise { 49 | var ajv = new Ajv(); 50 | const obj = JSON.parse(json); 51 | var valid = ajv.validate(schema, obj); 52 | if (!valid) { 53 | console.error(ajv.errors); 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | function validateToolMapping(json: string): boolean { 60 | const obj: any = JSON.parse(json); 61 | 62 | const { tooling, modules } = obj.data; 63 | 64 | const flattenedTools: any = Object.values(tooling).reduce( 65 | (list: any, current: any) => { 66 | return list.concat(...Object.keys(current)); 67 | }, 68 | [] as string[], 69 | ); 70 | 71 | const nonMatches: string[] = []; 72 | 73 | const result = Object.values(modules).every(category => { 74 | return Object.values(category).every(mod => { 75 | if (!mod.checkLists) { 76 | return true; 77 | } 78 | return Object.values(mod.checkLists).every((checklist: any) => { 79 | return checklist.every(ch => { 80 | if (ch.tools) { 81 | const results = ch.tools.map((tool: any) => { 82 | const result = flattenedTools.includes(tool); 83 | if (!result) { 84 | nonMatches.push(tool); 85 | } 86 | return result; 87 | }); 88 | 89 | return results.every(val => val); 90 | } 91 | // If no tools are listed then just return true 92 | return true; 93 | }); 94 | }); 95 | }); 96 | }); 97 | 98 | if (!result) { 99 | console.error( 100 | `TOOLING ERROR: The following keys could not be found as tools: \n\n${nonMatches.join( 101 | '\n', 102 | )}\n\n`, 103 | ); 104 | } 105 | 106 | return result; 107 | } 108 | 109 | export async function combineData( 110 | schemaPath: string, 111 | dataDirectory: string, 112 | ): Promise { 113 | const data = JSON.stringify(await reduceDirectory(dataDirectory)); 114 | const schema = JSON.parse(await readFile(schemaPath)); 115 | const validSchema = await validate(schema, data); 116 | if (!validSchema) { 117 | throw new Error('invalid schema'); 118 | } 119 | const validToolMapping = validateToolMapping(data); 120 | if (!validToolMapping) { 121 | throw new Error('invalid tooling'); 122 | } 123 | return JSON.parse(data); 124 | } 125 | 126 | if (process.argv[1] === __filename) { 127 | (async () => { 128 | try { 129 | const [schemaPath, dataDirectory] = process.argv.slice(2); 130 | 131 | if (!schemaPath || !dataDirectory) { 132 | console.error( 133 | `Usage: ${process.argv[0]} `, 134 | ); 135 | process.exit(1); 136 | } 137 | console.log(`Validating ${dataDirectory} against ${schemaPath}`); 138 | await combineData(schemaPath, dataDirectory); 139 | } catch (ex) { 140 | console.error('BAD CODE', ex); 141 | process.exit(1); 142 | } 143 | })(); 144 | } 145 | -------------------------------------------------------------------------------- /examples/rfc_listo.md: -------------------------------------------------------------------------------- 1 | 2 | > Inspired by Riot's RFC process [here](https://technology.riotgames.com/news/tech-design-riot) 3 | 4 | # Example RFC For - Listo Assessments (Based on SEEK's internal RFC process) 5 | 6 | Status: DRAFT 7 | 8 | Owner(s): Security, Architecture, Paved Road 9 | 10 | Scope: All product development 11 | 12 | ## Problem Statement 13 | 14 | As we expand the number of product teams at SEEK, it is difficult for product teams to keep track of all the recommended technical health practices, guidance and knowledge in the areas of Security, Performance, Recovery, Efficiency, Availability and Deployability. 15 | 16 | Also, as an organisation we move quickly to deliver products for our customers. However, moving too quickly without considering the risks could lead to introducing security, quality or reliability issues within our products. We need to be able to move fast without introducing unnecessary or preventable risks. 17 | 18 | ### Current Challenges 19 | 20 | + Teams find it difficult to stay up to date with large amounts of documentation (RFCs and GitHub Wikis in our case). 21 | + It's hard for small enablement teams (Security, Architecture, etc) to spread their specialist knowledge as SEEK grows. 22 | + Product teams have many considerations to think about while building a product for our customers. This can be overwhelming and confusing. 23 | + Recommended information is scattered and sometimes hard to follow. 24 | + Guidance in all areas changes rapidly. Finding a consolidated view is hard and can get confusing when contradictory advice is given or documentation is out of date. 25 | 26 | ## Listo 27 | 28 | Listo provides advice on security, reliability and architecture requirements when developing products at SEEK. Backed by our SEEK RFC process, this is a product team's one-stop shop to ensure its technical designs are in line with SEEK’s best practices. It also provides visibility of major product changes to enablement teams such as security and architecture, allowing them to better advise and prioritise the projects with the biggest risks to SEEK. 29 | 30 | ## Details 31 | 32 | + Teams *MUST* fill out a Listo assessment and [Triage the Trello board](#Triage-Trello-Board) at a minimum cadence of: 33 | * For every green field project [SEEK builds](#SEEK-Built-Products) before it goes live. 34 | * For every [major change](#major-changes) that will be added to existing [SEEK built products](#SEEK-Built-Products) before it goes live. 35 | + Teams *SHOULD* prioritise and create tasks based on the outcomes of the [Trello board triage process](#Triage-Trello-Board). 36 | + Projects rated as High Risk *SHOULD* reach out to the security team to run them through the outcomes of the [Trello board triage process](#Triage-Trello-Board). 37 | 38 | ## Definitions 39 | 40 | ### SEEK Built Products 41 | 42 | SEEK Built Products include any web or mobile applications or services built by SEEK Group staff for external customers or SEEK employees. 43 | 44 | ### Triage Trello Board 45 | 46 | Triaging the board is complete when: 47 | 48 | 1. The team has read through all checklists and ticked the items that have been completed for the [scope of the Listo assessment](#listo-assessment-scope). 49 | 2. Checklists that can't be completed have comments describing the considered and agreed actions. Examples of comments include: 50 | * The card or checklist is not relevant to our project. 51 | * We are working to action this card or checklist in x months. 52 | * We have deferred or deprioritised this task due to y and are comfortable with the risk. 53 | * We have completed this task by 70% and are planning to complete the rest by x. 54 | 55 | The triaged board should have: 56 | 57 | 1. Cards fully complete moved to the `Done` list. 58 | 2. Cards that are not applicable moved to the `Not Applicable` list, with comments describing the reasoning. 59 | 3. Cards that have outstanding actions moved to the `To Do` list 60 | 61 | ### Major Changes 62 | 63 | Introduces a significant product change or modifies significant amounts of code within an application. Examples include but are not limited to the following: 64 | 65 | + Adding new API endpoints and / or data stores. 66 | + Changing or adding infrastructure. 67 | + Product feature that collects new types of customer data from the frontend. 68 | + Adding integrations with third party businesses. 69 | 70 | ### Listo Assessment Scope 71 | 72 | The scope of a Listo assessment and corresponding Trello Board is determined by the Project Information entered at the start of the assessment. Examples of the scope of an assessment could be: 73 | 74 | + A repository including the products and services deployed within it. 75 | + A full product that comprises of multiple repositories and systems. 76 | + A feature within a product that comprises of several files within a repository. 77 | + All products owned by a team or stream that would comprise of several repositories, systems and environments. 78 | 79 | ## References 80 | 81 | + [The Checklist Manifesto: How to Get Things Right](https://www.goodreads.com/book/show/6667514-the-checklist-manifesto) 82 | + 83 | -------------------------------------------------------------------------------- /frontend/src/Project.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from '@reach/router'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { API_URL } from './constants'; 4 | import { 5 | Box, 6 | Typography, 7 | List, 8 | ListItemText, 9 | ListItem, 10 | } from '@material-ui/core'; 11 | import { 12 | AssessmentResult, 13 | PickedCategories, 14 | ProjectMetaResponses, 15 | Meta, 16 | ProjectModel 17 | } from './types/index'; 18 | 19 | interface ProjectProps extends RouteComponentProps { 20 | projectId?: string; 21 | listoMeta: Meta; 22 | } 23 | 24 | export const Project = (props: ProjectProps) => { 25 | const [projectMetaResponses, setProjectMetaResponses] = useState< 26 | ProjectMetaResponses 27 | >({}); 28 | const [selectedModulesByCategory, setSelectedModulesByCategory] = useState< 29 | PickedCategories 30 | >({}); 31 | const isSlack = props.listoMeta 32 | ? Boolean(props.listoMeta.slackChannelLink && props.listoMeta.slackChannel) 33 | : false; 34 | const [errorState, setErrorState] = useState(false); 35 | let projectId = props.projectId; 36 | 37 | const prepareProjectData = ( 38 | projectResult: AssessmentResult, 39 | boardLink: string, 40 | createdAt: string, 41 | ) => { 42 | projectResult.projectMetaResponses.boardLink = boardLink; 43 | projectResult.projectMetaResponses.createdAt = createdAt; 44 | setProjectMetaResponses(projectResult.projectMetaResponses); 45 | setSelectedModulesByCategory(projectResult.selectedModulesByCategory); 46 | }; 47 | 48 | useEffect(() => { 49 | const fetchData = async () => { 50 | try { 51 | const res = await fetch(`${API_URL}/project/${projectId}`, { 52 | method: 'GET', 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | }); 57 | 58 | if (res.status !== 200) throw new Error('Project not found'); 59 | const data = await res.json(); 60 | const project: ProjectModel = data.project; 61 | const projectResult: AssessmentResult = project.metaData; 62 | prepareProjectData( 63 | projectResult, 64 | project.boardLink || "", 65 | project.createdAt ? project.createdAt.toString() : "", 66 | ); 67 | } catch (err) { 68 | setErrorState(true); 69 | console.log(`Error fetching the project: ${err}`); 70 | } 71 | }; 72 | 73 | fetchData(); 74 | }, [projectId]); 75 | 76 | if (errorState) { 77 | return ( 78 | 79 | 80 | Listo Project Not Found 81 | 82 | 83 | 84 | We can't seem to find your project with ID: {projectId}. 85 | 86 | 87 | ); 88 | } else { 89 | return ( 90 | 91 | 92 | Project Details - {projectMetaResponses.boardName} 93 | 94 | 95 | 96 | {projectId} 97 | 98 | 99 | 100 | 101 |

102 | Thanks for submitting your details, we've created your Trello 103 | board containing checklists for your project here: 104 | 105 | {projectMetaResponses.boardLink} 106 | 107 |

108 |
109 | 110 | 111 |

112 | Got questions? Check out the FAQ's or message 113 | us on Slack{' '} 114 | {isSlack ? ( 115 | 120 | {props.listoMeta.slackChannel} 121 | 122 | ) : null} 123 |

124 |
125 | 126 | Project Information 127 | 128 | {Object.entries(projectMetaResponses).map(([meta, answer]) => ( 129 | 130 | 131 | 132 | ))} 133 | 134 | 135 | 136 | Modules Selected 137 | 138 | {Object.entries(selectedModulesByCategory).map( 139 | ([category, moduleNames]) => ( 140 | 141 | {category} 142 | 143 | 144 | {moduleNames.map(moduleName => ( 145 | 146 | 147 | 148 | ))} 149 | 150 | 151 | ), 152 | )} 153 |
154 |
155 | ); 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import cloneDeep from 'lodash.clonedeep'; 3 | import './App.css'; 4 | import Header from './components/Header'; 5 | import { Paper } from '@material-ui/core'; 6 | import { useStyles } from './styles'; 7 | import { AppContext } from './context'; 8 | import { 9 | Risk, 10 | ModuleCategories, 11 | ProjectMeta, 12 | DirectoryData, 13 | Tools, 14 | AssessmentResult, 15 | Meta, 16 | Checklists, 17 | } from './types'; 18 | import { Router } from '@reach/router'; 19 | import { Home } from './Home'; 20 | import { Faq } from './Faq'; 21 | import { Assessment } from './Assessment'; 22 | import { Project } from './Project'; 23 | import { StepProvider } from './context/StepContext'; 24 | import pickCategoriesWithResponse from './utils/pickCategoriesWithResponse'; 25 | import getSelectedRisks from './utils/getSelectedRisks'; 26 | 27 | import { handleRiskAnswer } from './utils/handleRiskAnswer'; 28 | import { prepareProjectMeta } from './utils/prepareProjectMeta'; 29 | import { getSelectedTools } from './utils/moduleHelpers'; 30 | import { API_URL } from './constants'; 31 | import { QuickChecklist } from './QuickChecklist'; 32 | import { SearchChecklists } from './SearchChecklists'; 33 | 34 | const App: React.FC = ({ children }) => { 35 | const classes = useStyles(); 36 | 37 | const [projectMeta, setProjectMeta] = useState([]); 38 | const [categories, setCategories] = useState({}); 39 | const [risks, setRisks] = useState([]); 40 | const [tools, setTools] = useState({}); 41 | const [meta, setMeta] = useState({}); 42 | const [quickChecklist, setQuickChecklist] = useState({}); 43 | 44 | const handleSelectChecklistItem = (checklistName: string, checklistItemIndex: number, checked: boolean) => { 45 | const clonedChecklist = cloneDeep(quickChecklist); 46 | clonedChecklist[checklistName][checklistItemIndex].checked = checked; 47 | setQuickChecklist(clonedChecklist); 48 | }; 49 | 50 | const initQuickChecklist = (checklists: Checklists) => { 51 | setQuickChecklist(checklists); 52 | }; 53 | 54 | const handleSelectTool = (tool: string, category: string, value: boolean) => { 55 | const clonedTools = cloneDeep(tools); 56 | clonedTools[category][tool].response = value; 57 | setTools(clonedTools); 58 | }; 59 | 60 | const handleSelectModule = ( 61 | categoryKey: string, 62 | moduleKey: string, 63 | value: boolean, 64 | ) => { 65 | const clonedCategories = cloneDeep(categories); 66 | clonedCategories[categoryKey][moduleKey].response = value; 67 | setCategories(clonedCategories); 68 | }; 69 | 70 | const handleUpdateProjectMeta = (name: string, response: string) => { 71 | const clonedProjectMeta = cloneDeep(projectMeta); 72 | const meta = clonedProjectMeta.find(m => m.name === name); 73 | if (meta) { 74 | meta.userResponse = response; 75 | setProjectMeta(clonedProjectMeta); 76 | } 77 | }; 78 | 79 | const prepareResult = (): AssessmentResult => { 80 | return { 81 | selectedRisks: getSelectedRisks(risks), 82 | selectedModulesByCategory: pickCategoriesWithResponse(categories), 83 | projectMetaResponses: prepareProjectMeta(projectMeta, risks), 84 | selectedTools: getSelectedTools(tools), 85 | }; 86 | }; 87 | 88 | const contextValue = { 89 | projectMeta, 90 | categories, 91 | risks, 92 | tools, 93 | quickChecklist, 94 | initQuickChecklist, 95 | handleSelectChecklistItem, 96 | handleUpdateProjectMeta, 97 | handleSelectModule, 98 | handleRiskAnswer: handleRiskAnswer(risks, setRisks), 99 | handleSelectTool, 100 | prepareResult, 101 | }; 102 | 103 | useEffect(() => { 104 | const fetchData = async () => { 105 | try { 106 | const dataRes = await fetch(`${API_URL}/data.json`); 107 | const { data }: DirectoryData = await dataRes.json(); 108 | setProjectMeta(data.projectMeta); 109 | setCategories(data.modules); 110 | setRisks(data.risks.questions); 111 | setTools(data.tooling); 112 | 113 | const metaRes = await fetch(`${API_URL}/meta`); 114 | const meta: Meta = await metaRes.json(); 115 | setMeta(meta); 116 | } catch (err) { 117 | console.log(`Error fetching data dictionary or meta data: ${err}`); 118 | } 119 | }; 120 | 121 | fetchData(); 122 | }, []); 123 | 124 | return ( 125 | 126 | 127 |
128 |
129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 |
143 |
144 |
145 | ); 146 | }; 147 | 148 | export default App; 149 | -------------------------------------------------------------------------------- /server/src/tests.ts: -------------------------------------------------------------------------------- 1 | import * as program from 'commander'; 2 | import fetch from 'node-fetch'; 3 | import * as trello from './trello'; 4 | import * as slack from './slack'; 5 | 6 | program.option('-d, --delete-board ', 'delete board with the supplied id'); 7 | program.option('-c, --create-board', 'create new board'); 8 | program.option('-s, --slack-message', 'send a test slack message'); 9 | program.option('-l, --list-boards', 'List all created boards'); 10 | program.option('-p, --get-project ', 'List a project with a specific ID'); 11 | program.parse(process.argv); 12 | 13 | const TEST_DATA = { 14 | Status: 'Project Hello There Created Successfully!', 15 | Project: 'http://localhost:3000/project/01c77593-852f-46e2-a3e2-fcea4f1a504e', 16 | ProjectDetails: { 17 | boardName: 'Dingo Dango', 18 | slackTeam: 'awesomesquad', 19 | slackUserName: 'julian', 20 | codeLocation: 'sadfasda', 21 | trelloEmail: 'fasdfasfsd@asdfads.com', 22 | riskLevel: 'High Risk', 23 | }, 24 | Trello: 'https://trello.com/b/dsfadsfafsdf', 25 | }; 26 | 27 | const TEST_DATA_FULL_BOARD = { 28 | selectedRisks: [ 29 | { 30 | text: 31 | 'Do you want supporting teams such as Security and Architecture to reach out throughout the project?', 32 | selection: 'Yes', 33 | }, 34 | ], 35 | selectedModulesByCategory: { 36 | code: [ 37 | 'authentication', 38 | 'authorisation', 39 | 'csrf', 40 | 'internal_libs', 41 | 'open_source', 42 | 'third_party_libs', 43 | 'urls', 44 | 'xml', 45 | 'xss', 46 | ], 47 | data: ['general', 'nosql', 'object_store', 'rds'], 48 | general: ['abuse', 'services', 'telemetry', 'threat_modeling'], 49 | service_provider: ['aws', 'datacentre'], 50 | software_env: ['containers', 'servers'], 51 | }, 52 | projectMetaResponses: { 53 | boardName: 'Google Doodle', 54 | slackTeam: 'awesome', 55 | slackUserName: 'julian', 56 | trelloEmail: 'asdfd@sdfadf', 57 | riskLevel: 'High Risk', 58 | }, 59 | selectedTools: ['Gantry'], 60 | }; 61 | 62 | const TEST_DATA_CREATE = { 63 | selectedRisks: [ 64 | { 65 | text: 66 | 'Do you want supporting teams such as Security and Architecture to reach out throughout the project?', 67 | selection: 'Yes', 68 | }, 69 | ], 70 | selectedModulesByCategory: { 71 | general: ['services', 'abuse'], 72 | test: ['test_long_checklist'], 73 | code: ['authentication'], 74 | }, 75 | projectMetaResponses: { 76 | boardName: 'Google Doodle', 77 | slackTeam: 'awesome', 78 | slackUserName: 'julian', 79 | trelloEmail: 'asdfd@sdfadfdfsdfsfdgdsgsfdgd.com', 80 | riskLevel: 'High Risk', 81 | }, 82 | selectedTools: ['Gantry'], 83 | }; 84 | 85 | (async function main() { 86 | try { 87 | if (program.deleteBoard) { 88 | const id = program.deleteBoard; 89 | const { status, statusText } = await trello.deleteBoard(id); 90 | 91 | let exitCode = 0; 92 | if (status === 200) { 93 | console.log(`Successfully deleted board ${id}`); 94 | } else { 95 | console.error( 96 | `Unable to delete board ${id}. Server response: ${status} - ${statusText}`, 97 | ); 98 | exitCode = 1; 99 | } 100 | process.exit(exitCode); 101 | } 102 | 103 | if (program.createBoard) { 104 | const url = 'http://localhost:8000/api/project'; 105 | const data = TEST_DATA_CREATE; 106 | const date = new Date(Date.now()); 107 | data.projectMetaResponses.boardName = `Board_${date 108 | .getSeconds() 109 | .toString()}_${date.getMilliseconds().toString()}`; 110 | const project = JSON.stringify(data); 111 | const options = { 112 | method: 'POST', 113 | headers: { 114 | 'Content-Type': 'application/json', 115 | }, 116 | body: project, 117 | }; 118 | 119 | const res = await fetch(url, options); 120 | // console.log(await res.text()); 121 | 122 | const board = await res.json(); 123 | 124 | let exitCode = 0; 125 | if (board.status === 200) { 126 | console.log(`Successfully created board ${board.id}`); 127 | console.log( 128 | `To delete this board type: projectId=${board.id} make delete_board`, 129 | ); 130 | } else { 131 | console.error(`Unable to create board. Server response: ${board}`); 132 | exitCode = 1; 133 | } 134 | process.exit(exitCode); 135 | } 136 | 137 | if (program.slackMessage) { 138 | const res = await slack.sendMessage(JSON.stringify(TEST_DATA)); 139 | console.log(JSON.stringify(res)); 140 | let exitCode = 0; 141 | process.exit(exitCode); 142 | } 143 | 144 | if (program.getProject) { 145 | const projectId = program.getProject; 146 | const url = `http://localhost:8000/project/${projectId}`; 147 | const options = { 148 | method: 'GET', 149 | headers: { 150 | Accept: 'application/json', 151 | 'Content-Type': 'application/json', 152 | }, 153 | }; 154 | 155 | const project = await (await fetch(url, options)).json(); 156 | 157 | let exitCode = 0; 158 | if (project.status === 200) { 159 | console.log(`Project Info: ${JSON.stringify(project)}`); 160 | } else { 161 | console.error(`Unable to list project:${project.status}`); 162 | exitCode = 1; 163 | } 164 | process.exit(exitCode); 165 | } 166 | } catch (err) { 167 | console.error(err); 168 | } 169 | })(); 170 | -------------------------------------------------------------------------------- /server/src/appFactory.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as trello from './trello'; 3 | import * as slack from './slack'; 4 | import * as cors from 'cors'; 5 | import { 6 | DirectoryData, 7 | AssessmentResult, 8 | Meta, 9 | ProjectModel, 10 | QuickChecklistModel, 11 | } from '../../frontend/src/types'; 12 | import { Repository } from './types'; 13 | const path = require('path'); 14 | 15 | const { 16 | FRONTEND_ASSETS_PATH, 17 | SLACK_CHANNEL_LINK, 18 | SLACK_TARGET_CHANNEL, 19 | TRELLO_BOARD_LINK, 20 | SERVER_URL, 21 | } = process.env; 22 | 23 | function buildProjectURL( 24 | scheme: string, 25 | host: string, 26 | projectId: string, 27 | ): string { 28 | // Support a custom URL, e.g. if listo is behind a reverse proxy 29 | const authority = SERVER_URL 30 | ? SERVER_URL.replace(/\/$/, '') 31 | : `${scheme}://${host}`; 32 | return `${authority}/project/${projectId}`; 33 | } 34 | 35 | function addMandatoryModules( 36 | inputData: AssessmentResult, 37 | listodata: DirectoryData, 38 | ): AssessmentResult { 39 | const categories = listodata.data.modules; 40 | for (let categoryKey of Object.keys(categories)) { 41 | const modules = categories[categoryKey]; 42 | for (let moduleKey of Object.keys(modules)) { 43 | const module = modules[moduleKey]; 44 | if (module.minimumRisk === 'Mandatory') { 45 | if (inputData.selectedModulesByCategory[categoryKey]) { 46 | inputData.selectedModulesByCategory[categoryKey].push(moduleKey); 47 | } else { 48 | inputData.selectedModulesByCategory[categoryKey] = [moduleKey]; 49 | } 50 | } 51 | } 52 | } 53 | return inputData; 54 | } 55 | 56 | async function appFactory(db: Repository, listoData: DirectoryData) { 57 | const app = express(); 58 | app.use(express.json()); 59 | app.use(cors()); 60 | app.disable('etag'); 61 | 62 | if (FRONTEND_ASSETS_PATH) { 63 | app.use(express.static(FRONTEND_ASSETS_PATH)); 64 | } 65 | 66 | app.get('/health', async (_req, res) => { 67 | res.json({ status: 200 }); 68 | }); 69 | 70 | const apiRouter = express.Router(); 71 | 72 | apiRouter.get('/data.json', async (_, res) => { 73 | res.json(listoData); 74 | }); 75 | 76 | apiRouter.get('/meta', async (_req, res) => { 77 | try { 78 | const meta: Meta = { 79 | slackChannel: SLACK_TARGET_CHANNEL, 80 | slackChannelLink: SLACK_CHANNEL_LINK, 81 | exampleTrelloBoardLink: TRELLO_BOARD_LINK, 82 | }; 83 | res.json(meta); 84 | } catch (err) { 85 | console.error(' Failed to list all projects', err); 86 | } 87 | }); 88 | 89 | apiRouter.post('/project', async (req, res) => { 90 | const inputData = addMandatoryModules( 91 | req.body as AssessmentResult, 92 | listoData, 93 | ); 94 | let board = null; 95 | let projectId = null; 96 | 97 | try { 98 | const project: ProjectModel = { metaData: inputData }; 99 | projectId = await db.create(project); 100 | } catch (err) { 101 | throw new Error( 102 | `Failed to store project ${projectId} in the database: ${err}.`, 103 | ); 104 | } 105 | 106 | try { 107 | board = await trello.createFullBoard( 108 | inputData.projectMetaResponses.boardName, 109 | inputData, 110 | listoData, 111 | ); 112 | } catch (err) { 113 | await slack.sendMessage( 114 | JSON.stringify({ 115 | Status: `Failed to create Trello board for ${inputData.projectMetaResponses.boardName}.`, 116 | Project: buildProjectURL(req.protocol, req.hostname, projectId), 117 | ProjectDetails: inputData.projectMetaResponses, 118 | Environment: process.env.STAGE, 119 | }), 120 | ); 121 | 122 | throw new Error( 123 | `Failed to create Trello board for project ${projectId}: ${err}.`, 124 | ); 125 | } 126 | 127 | try { 128 | if (inputData.projectMetaResponses.trelloEmail) { 129 | await trello.addMember( 130 | board.id, 131 | inputData.projectMetaResponses.trelloEmail, 132 | ); 133 | } 134 | } catch (err) { 135 | // Logging the error but not failing the response. We might want to change this in the future to throw an error to the client. 136 | console.log( 137 | `Failed to add Trello user with email ${inputData.projectMetaResponses.trelloEmail} to project ${projectId}: ${err}.`, 138 | ); 139 | } 140 | 141 | try { 142 | await db.update(projectId, board.shortUrl); 143 | } catch (err) { 144 | throw new Error( 145 | `Failed to update project (${projectId}) with board url ${board.shortUrl}: ${err}.`, 146 | ); 147 | } 148 | 149 | try { 150 | await slack.sendMessage( 151 | JSON.stringify({ 152 | Status: `Project ${inputData.projectMetaResponses.boardName} Created Successfully!`, 153 | Project: buildProjectURL(req.protocol, req.hostname, projectId), 154 | ProjectDetails: inputData.projectMetaResponses, 155 | Trello: board.shortUrl, 156 | Environment: process.env.STAGE, 157 | }), 158 | ); 159 | } catch (err) { 160 | throw new Error( 161 | `Failed to send Slack alert for Project ${projectId}: ${err}`, 162 | ); 163 | } 164 | 165 | res.json({ 166 | id: projectId, 167 | details: 'Listo Project Created Successfully', 168 | status: 200, 169 | }); 170 | }); 171 | 172 | apiRouter.post('/slack', async (req, res) => { 173 | try { 174 | const message = JSON.stringify(req.body); 175 | slack.sendMessage(message); 176 | res.sendStatus(204); 177 | } catch (err) { 178 | console.error('Failed send Slack alert', err); 179 | } 180 | }); 181 | 182 | apiRouter.get('/project/:id', async (req, res) => { 183 | try { 184 | const project = await db.get(req.params.id); 185 | res.json({ project: project, status: 200 }); 186 | } catch (err) { 187 | console.error(`Failed to find project with ${req.params.id}`, err); 188 | res.status(404).send(`Project not found`); 189 | } 190 | }); 191 | 192 | apiRouter.get('/quick-checklist/:id', async (req, res) => { 193 | try { 194 | const quickChecklist = await db.getQuickChecklist(req.params.id); 195 | res.json({ quickChecklist: quickChecklist, status: 200 }); 196 | } catch (err) { 197 | console.error(`Failed to find QuickChecklist with ${req.params.id}`, err); 198 | res.status(404).send(`QuickChecklist not found`); 199 | } 200 | }); 201 | 202 | apiRouter.put('/quick-checklist', async (req, res) => { 203 | let id = null; 204 | 205 | try { 206 | const quickChecklist = req.body as QuickChecklistModel; 207 | id = await db.upsertQuickChecklist(quickChecklist); 208 | } catch (err) { 209 | throw new Error( 210 | `Failed to store Quickchecklist with ID ${id} in the database: ${err}.`, 211 | ); 212 | } 213 | res.json({ 214 | id: id, 215 | details: 'Listo QuickChecklist Saved Successfully', 216 | status: 200, 217 | }); 218 | }); 219 | 220 | app.use('/api', apiRouter); 221 | 222 | // support client side routing per https://github.com/reach/router/blob/master/examples/crud/README.md#serving-apps-with-client-side-routing 223 | if (FRONTEND_ASSETS_PATH) { 224 | app.get('/*', (_req, res) => { 225 | res.sendFile(path.resolve(path.join(FRONTEND_ASSETS_PATH, 'index.html'))); 226 | }); 227 | } 228 | 229 | return app; 230 | } 231 | 232 | export default appFactory; 233 | -------------------------------------------------------------------------------- /data/modules/general/threat_modeling.yml: -------------------------------------------------------------------------------- 1 | category: General 2 | assessmentQuestion: Have you performed a theat model on your project? 3 | minimumRisk: Low Risk 4 | guidance: > 5 | Threat modeling is about using models to find security problems. Using a model means abstracting away a lot of details to provide 6 | a look at a bigger picture, rather than the code itself. You model because it enables you to find issues in things you haven't built yet, 7 | and because it enables you to catch a problem before it starts. Lastly, you threat model as a way to anticipate the threats that could 8 | affect you[*](https://threatmodelingbook.com/). 9 | resources: 10 | - "[Threat Modeling: What, Why, and How?](https://misti.com/infosec-insider/threat-modeling-what-why-and-how)" 11 | - "[STRIDE checklists are from the EoP card game](https://github.com/adamshostack/eop)." 12 | - "[The `Threat Assessment` checklist is from Threat Modeling: Designing for Security.](https://threatmodelingbook.com/)" 13 | checkLists: 14 | Threat Assessment: 15 | - question: We have created an architecture or dataflow diagram of the software, can tell a story about the system and consider what could go wrong without using words such as "sometimes" or "also". 16 | - question: "The diagram show all the [trust boundaries](https://en.wikipedia.org/wiki/Trust_boundary) and cover all application roles, and all network interfaces." 17 | - question: The diagram reflects the current or planned reality of the software. 18 | - question: We have looked for each of the STRIDE threats for each element and dataflow in the diagram. 19 | - question: We have written down or filed a bug for each threat discovered. 20 | - question: There is a proposed/planned/implemented way to address each threat. 21 | - question: We have a test case per threat and the software has passed the tests. 22 | Spoofing: 23 | - question: Try one credential after another and there's nothing to slow them down (online or offline) <3> 24 | - question: Anonymously connect, because we expect authentication to be done at a higher level <4> 25 | - question: Confuse a client because there are too many ways to identify a server <5> 26 | - question: Spoof a server because identifiers aren't stored on the client and checked for consistency on re-connection (that is, there's no key persistence) <6> 27 | - question: Connect to a server or peer over a link that isn't authenticated (and encrypted) <7> 28 | - question: Steal credentials stored on the server and reuse them (for example, a key is stored in a world readable file) <8> 29 | - question: Gets a password can reuse it (Use stronger authenticators) <9> 30 | - question: Choose to use weaker or no authentication <10> 31 | - question: Steal credentials stored on the client and reuse them 32 | - question: Go after the way credentials are updated or recovered (account recovery doesn't require disclosing the old password) 33 | - question: Your system ships with a default admin password, and doesn't force a change 34 | Tampering: 35 | - question: Take advantage of your custom key exchange or integrity control which you built instead of using standard crypto <3> 36 | - question: Your code makes access control decisions all over the place, rather than with a security kernel <4> 37 | - question: Replay data without detection because your code doesn't provide timestamps or sequence numbers <5> 38 | - question: Write to a data store your code relies on <6> 39 | - question: Bypass permissions because you don't make names canonical before checking access permissions <7> 40 | - question: Manipulate data because there's no integrity protection for data on the network <8> 41 | - question: Provide or control state information <9> 42 | - question: Alter information in a data store because it has weak/open permissions or includes a group which is equivalent to everyone ("anyone with a Facebook account") <10> 43 | - question: Write to some resource because permissions are granted to the world or there are no ACLs 44 | - question: Change parameters over a trust boundary and after validation (for example, important parameters in a hidden field in HTML, or passing a pointer to critical memory) 45 | - question: Load code inside your process via an extension point 46 | Repudiation: 47 | - question: Pass data through the application to attack a log reader (e.g. splunk, elk, etc). <2> 48 | - question: A low privilege attacker can read interesting security information in the logs <3> 49 | - question: Alter digital signatures because the digital signature system you're implementing is weak, or uses MACs where it should use a signature <4> 50 | - question: Alter log messages on a network because they lack strong integrity controls <5> 51 | - question: Create a log entry without a timestamp (or no log entry is timestamped) <6> 52 | - question: Make the logs wrap around and lose data <7> 53 | - question: Make a log lose or confuse security information <8> 54 | - question: Use a shared key to authenticate as different principals, confusing the information in the logs <9> 55 | - question: Get arbitrary data into logs from unauthenticated (or weakly authenticated) outsiders without validation< 10> 56 | - question: Edit logs and there's no way to tell (perhaps because there's no heartbeat option for the logging system) 57 | - question: Say "I didn't do that," and you'd have no way to prove them wrong 58 | - question: The system has no logs 59 | Information Disclosure: 60 | - question: Brute-force file encryption because there's no defense in place (example defense, password stretching) <2> 61 | - question: See error messages with security sensitive content <3> 62 | - question: Read content because messages (say, an email or HTTP cookie) aren't encrypted even if the channel is encrypted <4> 63 | - question: Be able to read a document or data because it's encrypted with a non-standard algorithm <5> 64 | - question: Read data because it's hidden or occluded (for undo or change tracking) and the user might forget that it's there <6> 65 | - question: Act as a 'man in the middle' because you don't authenticate endpoints of a network connection <7> 66 | - question: Access information through a search indexer, logger, or other such mechanism <8> 67 | - question: Read sensitive information in a file with bad ACLs <9> 68 | - question: Read information in files with no ACLs <10> 69 | - question: Discover the fixed key being used to encrypt 70 | - question: Read the entire channel because the channel (say, HTTP or SMTP) isn't encrypted 71 | - question: Read network information because there's no cryptography used 72 | Denial of Service: 73 | - question: Make your authentication system unusable or unavailable <2> 74 | - question: Drain our easily replacable battery (battery, temporary) <3> 75 | - question: Drain a battery that's hard to replace (sealed in a phone, an implanted medical device, or in a hard to reach location) (battery, persist) <4> 76 | - question: Spend our cloud budget (budget, persist) <5> 77 | - question: Make a server unavailable or unusable without ever authenticating but the problem goes away when the attacker stops (server, anonymous, temporary) <6> 78 | - question: Make a client unavailable or unusable and the problem persists after the attacker goes away (client, auth, persist) <7> 79 | - question: Make a server unavailable or unusable and the problem persists after the attacker goes away (server, auth, persist) <8> 80 | - question: Make a client unavailable or unusable without ever authenticating and the problem persists after the attacker goes away (client, anon, persist) <9> 81 | - question: Make a server unavailable or unusable without ever authenticating and the problem persists after the attacker goes away (server, anon, persist) <10> 82 | - question: Cause the logging subsystem to stop working 83 | - question: Amplify a Denial of Service attack through this component with amplification on the order of 10 to 1 84 | - question: Amplify a denial of service attack through this component with amplification on the order of 100 to 1 85 | Elevation of Privilege: 86 | - question: Force data through different validation paths which give different results <5> 87 | - question: Take advantage of .NET permissions you ask for, but don't use <6> 88 | - question: Provide a pointer across a trust boundary, rather than data which can be validated <7> 89 | - question: Enter data that is checked while still under their control and used later on the other side of a trust boundary <8> 90 | - question: There's no reasonable way for a caller to figure out what validation of tainted data you perform before passing it to them <9> 91 | - question: There's no reasonable way for a caller to figure out what security assumptions you make <10> 92 | - question: Reflect input back to a user, like cross site scripting 93 | - question: You include user-generated content within your page, possibly including the content of random URLs 94 | - question: Inject a command that the system will run at a higher privilege level 95 | tags: WebApp, API, Worker, iOS, Android 96 | title: Threat Modeling -------------------------------------------------------------------------------- /server/src/trello.ts: -------------------------------------------------------------------------------- 1 | import { DirectoryData, AssessmentResult } from '../../frontend/src/types'; 2 | import { URL } from 'url'; 3 | import fetch from 'node-fetch'; 4 | import * as AWS from 'aws-sdk'; 5 | import { region } from './config'; 6 | import plimit from 'p-limit'; 7 | 8 | const TRELLO_URL = process.env.TRELLO_URL || 'https://api.trello.com/1'; 9 | const TRELLO_SECRET_ID = process.env.TRELLO_SECRET_ID; 10 | const TRELLO_TEAM = process.env.TRELLO_TEAM; 11 | 12 | const sm = new AWS.SecretsManager({ region }); 13 | const getSecretParams: AWS.SecretsManager.GetSecretValueRequest = { 14 | SecretId: TRELLO_SECRET_ID, 15 | }; 16 | 17 | let cachedSecretResponse: AWS.SecretsManager.GetSecretValueResponse | undefined; 18 | 19 | async function getTrelloCredentials(): Promise<{ 20 | apiKey: string; 21 | token: string; 22 | }> { 23 | const { TRELLO_API_KEY, TRELLO_TOKEN } = process.env; 24 | if (TRELLO_API_KEY && TRELLO_TOKEN) { 25 | return { 26 | apiKey: TRELLO_API_KEY, 27 | token: TRELLO_TOKEN, 28 | }; 29 | } 30 | cachedSecretResponse = 31 | cachedSecretResponse || 32 | (await sm.getSecretValue(getSecretParams).promise()); 33 | 34 | const parsedSecrets = JSON.parse(cachedSecretResponse.SecretString); 35 | return { 36 | apiKey: parsedSecrets.trello_api_key, 37 | token: parsedSecrets.trello_token, 38 | }; 39 | } 40 | 41 | export interface TrelloCard { 42 | id?: string; 43 | name: string; 44 | category: string; 45 | description: string; 46 | questions?: string; 47 | tags: string; 48 | listId?: string; 49 | checklists?: TrelloCheckList[]; 50 | } 51 | 52 | export interface TrelloCheckList { 53 | name: string; 54 | items: TrelloCheckListItem[]; 55 | cardid?: string; 56 | } 57 | 58 | export interface TrelloCheckListItem { 59 | name: string; 60 | completed: boolean; 61 | } 62 | 63 | async function buildURL( 64 | resourcePath: string, 65 | params: Map = new Map([]), 66 | ): Promise { 67 | const trelloCredentials = await getTrelloCredentials(); 68 | const url = new URL(`${TRELLO_URL}/${resourcePath}`); 69 | 70 | url.searchParams.append('key', trelloCredentials.apiKey); 71 | url.searchParams.append('token', trelloCredentials.token); 72 | 73 | for (const [key, value] of params) { 74 | url.searchParams.append(key, value); 75 | } 76 | 77 | return url.toString(); 78 | } 79 | 80 | export async function createBoard(name: string): Promise { 81 | const params = new Map([ 82 | ['name', name], 83 | ['defaultLists', 'false'], 84 | ['defaultLabels', 'false'], 85 | ]); 86 | 87 | if (TRELLO_TEAM) { 88 | params.set('idOrganization', TRELLO_TEAM); // The Listo Trello team 89 | params.set('prefs_permissionLevel', 'enterprise'); // All users within the Trello org have read access to all boards. 90 | } 91 | 92 | const url = await buildURL('boards', params); 93 | const options = { 94 | method: 'POST', 95 | }; 96 | 97 | const res = await fetch(url, options); 98 | if (!res.ok) throw new Error(res.statusText); 99 | 100 | return res.json(); 101 | } 102 | 103 | export async function deleteBoard(id: string): Promise { 104 | const url = await buildURL(`boards/${id}`); 105 | const options = { 106 | method: 'DELETE', 107 | }; 108 | const res = await fetch(url, options); 109 | if (!res.ok) throw new Error(res.statusText); 110 | return res; 111 | } 112 | 113 | export async function createLists( 114 | names: string[], 115 | boardId: string, 116 | ): Promise { 117 | const options = { 118 | method: 'POST', 119 | }; 120 | 121 | names.push('Done'); 122 | 123 | const responses = await Promise.all( 124 | names.map(async name => { 125 | const params = new Map([ 126 | ['idBoard', boardId], 127 | ['name', name], 128 | ]); 129 | 130 | if (name === 'Done') params.set('pos', 'bottom'); 131 | const url = await buildURL('lists', params); 132 | 133 | const res = await fetch(url, options); 134 | if (!res.ok) throw new Error(res.statusText); 135 | return res.json(); 136 | }), 137 | ); 138 | 139 | return responses; 140 | } 141 | 142 | export async function createCards(cards: TrelloCard[]): Promise { 143 | const responses = await Promise.all( 144 | cards.map(async card => { 145 | const params = new Map([ 146 | ['idList', card.listId], 147 | ['name', card.name], 148 | ['desc', card.description], 149 | ]); 150 | const url = await buildURL('cards', params); 151 | 152 | const options = { 153 | method: 'POST', 154 | }; 155 | 156 | const res = await fetch(url, options); 157 | if (!res.ok) throw new Error(res.statusText); 158 | 159 | return res.json(); 160 | }), 161 | ); 162 | 163 | return responses; 164 | } 165 | 166 | export async function createCheckList( 167 | checklist: TrelloCheckList, 168 | ): Promise[]> { 169 | // Trello API does not create all checklist items if we go above 1 request at a time. 170 | const limit = plimit(1); 171 | const params = new Map([ 172 | ['idCard', checklist.cardid], 173 | ['name', checklist.name], 174 | ]); 175 | const url = await buildURL('checklists', params); 176 | 177 | const options = { 178 | method: 'POST', 179 | }; 180 | 181 | const res = await fetch(url, options); 182 | if (!res.ok) throw new Error(res.statusText); 183 | const trelloChecklist = await res.json(); 184 | 185 | return await Promise.all( 186 | checklist.items.map(checklistItem => 187 | limit(createChecklistItem, checklistItem, trelloChecklist.id), 188 | ), 189 | ); 190 | } 191 | 192 | async function createChecklistItem( 193 | checklistItem: TrelloCheckListItem, 194 | trelloChecklistId: string, 195 | ) { 196 | const params = new Map([ 197 | ['name', checklistItem.name], 198 | ['checked', `${checklistItem.completed}`], 199 | ]); 200 | 201 | const url = await buildURL( 202 | `checklists/${trelloChecklistId}/checkItems`, 203 | params, 204 | ); 205 | 206 | const options = { 207 | method: 'POST', 208 | }; 209 | 210 | const res = await fetch(url, options); 211 | if (!res.ok) throw new Error(res.statusText); 212 | return await res.json(); 213 | } 214 | 215 | export async function createCheckLists(cards: TrelloCard[]): Promise { 216 | let checklists = []; 217 | const limit = plimit(2); 218 | 219 | for (let card of cards) { 220 | for (let checklist of card.checklists) { 221 | checklist.cardid = card.id; 222 | checklists.push(limit(createCheckList, checklist)); 223 | } 224 | } 225 | 226 | return await Promise.all(checklists); 227 | } 228 | 229 | export async function createFullBoard( 230 | name: string, 231 | inputData: AssessmentResult, 232 | listoData: DirectoryData, 233 | ): Promise { 234 | const board = await createBoard(name); 235 | 236 | const listsData = await createLists( 237 | Object.keys(inputData.selectedModulesByCategory), 238 | board.id, 239 | ); 240 | 241 | const cards = []; 242 | 243 | // Create the Card's for all selected modules 244 | for (let category of Object.keys(inputData.selectedModulesByCategory)) { 245 | for (let moduleKey of inputData.selectedModulesByCategory[category]) { 246 | const selectedCategory = listoData.data.modules[category]; 247 | 248 | const list = listsData.find(list => list.name === category); 249 | 250 | let trelloDescription = [selectedCategory[moduleKey].assessmentQuestion]; 251 | let resources = selectedCategory[moduleKey].resources; 252 | let moduleDescription = selectedCategory[moduleKey].guidance; 253 | 254 | moduleDescription 255 | ? trelloDescription.push('', '### Guidance:', '', moduleDescription) 256 | : null; 257 | 258 | if (resources) { 259 | resources = resources.map(resource => `+ ${resource}`); 260 | trelloDescription.push('', '### Resources:', ''); 261 | trelloDescription = trelloDescription.concat(resources); 262 | } 263 | 264 | let cardObj: TrelloCard = { 265 | name: selectedCategory[moduleKey].title, 266 | category: selectedCategory[moduleKey].category, 267 | description: trelloDescription.join('\n'), 268 | tags: selectedCategory[moduleKey].tags, 269 | listId: list.id, 270 | }; 271 | 272 | // Add the checklists to the cards 273 | const checklists = []; 274 | for (let checklist of Object.keys( 275 | selectedCategory[moduleKey].checkLists, 276 | )) { 277 | let checkListObj: TrelloCheckList = { 278 | name: checklist, 279 | items: selectedCategory[moduleKey].checkLists[checklist].map( 280 | checklist => ({ 281 | name: checklist.question, 282 | completed: checklist.tools 283 | ? checklist.tools.some(checklistTool => 284 | inputData.selectedTools.includes(checklistTool), 285 | ) 286 | : false, 287 | }), 288 | ), 289 | }; 290 | 291 | checklists.push(checkListObj); 292 | } 293 | cardObj.checklists = checklists; 294 | cards.push(cardObj); 295 | } 296 | } 297 | 298 | const cardResponses = await createCards(cards); 299 | 300 | for (let card of cards) { 301 | const cardFound = cardResponses.find( 302 | cardResponse => cardResponse.name === card.name, 303 | ); 304 | card.id = cardFound.id; 305 | } 306 | 307 | // create the checklists and checklist items within the cards 308 | await createCheckLists(cards); 309 | 310 | return board; 311 | } 312 | 313 | export async function addMember( 314 | boardID: string, 315 | memberEmail: string, 316 | ): Promise { 317 | const params = new Map([ 318 | ['email', memberEmail], 319 | ['type', 'normal'], 320 | ]); 321 | const url = await buildURL(`boards/${boardID}/members`, params); 322 | 323 | const options = { 324 | method: 'PUT', 325 | }; 326 | 327 | const res = await fetch(url, options); 328 | if (!res.ok) throw new Error(res.statusText); 329 | 330 | return res.json(); 331 | } 332 | -------------------------------------------------------------------------------- /examples/deploy/stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: Stack for deploying listo into AWS Fargate 3 | Parameters: 4 | ImageUrl: 5 | Description: URL for the listo docker image 6 | Type: String 7 | 8 | VpcId: 9 | Description: VPC to deploy in 10 | Type: AWS::EC2::VPC::Id 11 | 12 | PublicSubnetIds: 13 | Description: Subnets exposed to the internet, used for the load balancer 14 | Type: List 15 | 16 | PrivateSubnetIds: 17 | Description: Private subnets 18 | Type: List 19 | 20 | ServiceName: 21 | Type: String 22 | Default: listo 23 | 24 | TrelloTeam: 25 | Type: String 26 | 27 | CertificateArn: 28 | Description: ACM certificate for the load balancer 29 | Type: String 30 | 31 | OidcAuthorizationEndpoint: 32 | Description: Authorization endpoint of the IdP 33 | Type: String 34 | 35 | OidcClientId: 36 | Description: The OAuth 2.0 client identifier 37 | Type: String 38 | 39 | OidcClientSecret: 40 | Description: The OAuth 2.0 client secret 41 | Type: String 42 | NoEcho: true 43 | 44 | OidcIssuer: 45 | Description: The OIDC issuer identifier of the IdP. This must be a full URL, including the HTTPS protocol, the domain, and the path. 46 | Type: String 47 | 48 | OidcTokenEndpoint: 49 | Description: The token endpoint of the IdP. This must be a full URL, including the HTTPS protocol, the domain, and the path. 50 | Type: String 51 | 52 | OidcUserInfoEndpoint: 53 | Description: The user info endpoint of the IdP. This must be a full URL, including the HTTPS protocol, the domain, and the path. 54 | Type: String 55 | 56 | AlbHostedZoneId: 57 | Description: The Route53 zone ID for creating a DNS record to the ALB 58 | Type: String 59 | 60 | AlbDnsName: 61 | Description: The DNS name to assign the load balancer, e.g. listo.example.com 62 | Type: String 63 | Resources: 64 | AlbSecurityGroup: 65 | Type: AWS::EC2::SecurityGroup 66 | Properties: 67 | GroupDescription: listo-alb-security-group 68 | VpcId: !Ref VpcId 69 | SecurityGroupIngress: 70 | - IpProtocol: tcp 71 | FromPort: 443 72 | ToPort: 443 73 | CidrIp: '0.0.0.0/0' 74 | - IpProtocol: tcp 75 | FromPort: 80 76 | ToPort: 80 77 | CidrIp: '0.0.0.0/0' 78 | 79 | Alb: 80 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 81 | Properties: 82 | SecurityGroups: 83 | - !Ref AlbSecurityGroup 84 | Subnets: !Ref PublicSubnetIds 85 | LoadBalancerAttributes: 86 | - Key: idle_timeout.timeout_seconds 87 | Value: 60 88 | - Key: deletion_protection.enabled 89 | Value: true 90 | 91 | HttpsListener: 92 | Type: AWS::ElasticLoadBalancingV2::Listener 93 | Properties: 94 | Certificates: 95 | - CertificateArn: !Ref CertificateArn 96 | DefaultActions: 97 | # enforce authentication on all requests 98 | - Type: authenticate-oidc 99 | AuthenticateOidcConfig: 100 | AuthorizationEndpoint: !Ref OidcAuthorizationEndpoint 101 | ClientId: !Ref OidcClientId 102 | ClientSecret: !Ref OidcClientSecret 103 | Issuer: !Ref OidcIssuer 104 | TokenEndpoint: !Ref OidcTokenEndpoint 105 | UserInfoEndpoint: !Ref OidcUserInfoEndpoint 106 | Order: 1 107 | - Type: forward 108 | TargetGroupArn: !Ref TargetGroup 109 | Order: 2 110 | LoadBalancerArn: !Ref Alb 111 | Port: 443 112 | Protocol: HTTPS 113 | 114 | HttpRedirectListener: 115 | Type: AWS::ElasticLoadBalancingV2::Listener 116 | Properties: 117 | DefaultActions: 118 | - Type: redirect 119 | RedirectConfig: 120 | Host: '#{host}' 121 | Path: '/#{path}' 122 | Port: 443 123 | Protocol: 'HTTPS' 124 | Query: '#{query}' 125 | StatusCode: HTTP_301 126 | LoadBalancerArn: !Ref Alb 127 | Port: 80 128 | Protocol: HTTP 129 | 130 | TargetGroup: 131 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 132 | Properties: 133 | HealthCheckIntervalSeconds: 10 134 | HealthCheckPath: '/health' 135 | HealthCheckTimeoutSeconds: 5 136 | UnhealthyThresholdCount: 2 137 | HealthyThresholdCount: 2 138 | Port: 8080 139 | Protocol: HTTP 140 | TargetGroupAttributes: 141 | - Key: deregistration_delay.timeout_seconds 142 | Value: 60 143 | TargetType: ip 144 | VpcId: !Ref VpcId 145 | 146 | AlbDnsRecord: 147 | Type: AWS::Route53::RecordSet 148 | Properties: 149 | HostedZoneId: !Ref AlbHostedZoneId 150 | Name: !Ref AlbDnsName 151 | Type: A 152 | AliasTarget: 153 | DNSName: !GetAtt Alb.DNSName 154 | HostedZoneId: !GetAtt Alb.CanonicalHostedZoneID 155 | 156 | Cluster: 157 | Type: AWS::ECS::Cluster 158 | Properties: 159 | ClusterName: listo 160 | 161 | TaskDefinition: 162 | Type: AWS::ECS::TaskDefinition 163 | Properties: 164 | NetworkMode: awsvpc 165 | RequiresCompatibilities: 166 | - FARGATE 167 | Cpu: 256 168 | Memory: 512 169 | ExecutionRoleArn: !Ref ExecutionRole 170 | TaskRoleArn: !Ref TaskRole 171 | ContainerDefinitions: 172 | - Name: !Ref ServiceName 173 | Image: !Ref ImageUrl 174 | PortMappings: 175 | - ContainerPort: 8000 176 | Environment: 177 | - Name: WEBHOOK_SECRET_ID 178 | Value: !Ref SlackWebHookSecret 179 | - Name: TRELLO_SCRET_ID 180 | Value: !Ref TrelloCredentialsSecret 181 | - Name: DYNAMODB_TABLE 182 | Value: !Ref BoardsTable 183 | - Name: TRELLO_TEAM 184 | Value: !Ref TrelloTeam 185 | LogConfiguration: 186 | LogDriver: awslogs 187 | Options: 188 | awslogs-region: !Ref AWS::Region 189 | awslogs-group: !Ref LogGroup 190 | awslogs-stream-prefix: ecs 191 | 192 | ExecutionRole: 193 | Type: AWS::IAM::Role 194 | Properties: 195 | AssumeRolePolicyDocument: 196 | Statement: 197 | - Effect: Allow 198 | Principal: 199 | Service: ecs-tasks.amazonaws.com 200 | Action: sts:AssumeRole 201 | ManagedPolicyArns: 202 | - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy 203 | 204 | TaskRole: 205 | Type: AWS::IAM::Role 206 | DeletionPolicy: Retain 207 | Properties: 208 | AssumeRolePolicyDocument: 209 | Statement: 210 | - Effect: Allow 211 | Principal: 212 | Service: ecs-tasks.amazonaws.com 213 | Action: sts:AssumeRole 214 | Path: '/' 215 | Policies: 216 | - PolicyName: read-s3 217 | PolicyDocument: 218 | Version: '2012-10-17' 219 | Statement: 220 | - Effect: Allow 221 | Action: secretsmanager:GetSecretValue 222 | Resource: 223 | - !Ref TrelloCredentialsSecret 224 | - !Ref SlackWebHookSecret 225 | - Effect: Allow 226 | Action: 227 | - dynamodb:Query 228 | - dynamodb:Scan 229 | - dynamodb:GetItem 230 | - dynamodb:PutItem 231 | - dynamodb:UpdateItem 232 | - dynamodb:DeleteItem 233 | Resource: !GetAtt BoardsTable.Arn 234 | 235 | AutoScalingRole: 236 | Type: AWS::IAM::Role 237 | Properties: 238 | AssumeRolePolicyDocument: 239 | Statement: 240 | - Effect: Allow 241 | Principal: 242 | Service: ecs-tasks.amazonaws.com 243 | Action: sts:AssumeRole 244 | ManagedPolicyArns: 245 | - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole 246 | 247 | ContainerSecurityGroup: 248 | Type: AWS::EC2::SecurityGroup 249 | Properties: 250 | GroupDescription: !Sub '${ServiceName}-sg' 251 | VpcId: !Ref VpcId 252 | SecurityGroupIngress: 253 | - IpProtocol: tcp 254 | FromPort: 8000 255 | ToPort: 8000 256 | SourceSecurityGroupId: !Ref AlbSecurityGroup 257 | 258 | ListoService: 259 | Type: AWS::ECS::Service 260 | Properties: 261 | Cluster: !Ref Cluster 262 | TaskDefinition: !Ref TaskDefinition 263 | DeploymentConfiguration: 264 | MinimumHealthyPercent: 100 265 | MaximumPercent: 200 266 | DesiredCount: 2 267 | HealthCheckGracePeriodSeconds: 30 268 | LaunchType: FARGATE 269 | NetworkConfiguration: 270 | AwsvpcConfiguration: 271 | AssignPublicIp: DISABLED 272 | Subnets: !Ref PrivateSubnetIds 273 | SecurityGroups: 274 | - !Ref ContainerSecurityGroup 275 | LoadBalancers: 276 | - ContainerName: !Ref ServiceName 277 | ContainerPort: 8000 278 | TargetGroupArn: !Ref TargetGroup 279 | 280 | LogGroup: 281 | Type: AWS::Logs::LogGroup 282 | Properties: 283 | LogGroupName: /listo 284 | RetentionInDays: 14 285 | 286 | AutoScalingTarget: 287 | Type: AWS::ApplicationAutoScaling::ScalableTarget 288 | Properties: 289 | MinCapacity: 1 290 | MaxCapacity: 5 291 | ResourceId: !Sub 'service/${Cluster}/${ListoService.Name}' 292 | ScalableDimension: ecs:service:DesiredCount 293 | ServiceNamespace: ecs 294 | RoleARN: !GetAtt AutoScalingRole.Arn 295 | 296 | AutoScalingPolicy: 297 | Type: AWS::ApplicationAutoScaling::ScalingPolicy 298 | Properties: 299 | PolicyName: !Sub '${ServiceName}-asp' 300 | PolicyType: TargetTrackingScaling 301 | ScalingTargetId: !Ref AutoScalingTarget 302 | TargetTrackingScalingPolicyConfiguration: 303 | PredefinedMetricSpecification: 304 | PredefinedMetricType: ECSServiceAverageCPUUtilization 305 | ScaleInCooldown: 10 306 | ScaleOutCooldown: 10 307 | TargetValue: 75 308 | 309 | BoardsTable: 310 | Type: AWS::DynamoDB::Table 311 | DeletionPolicy: Retain 312 | Properties: 313 | AttributeDefinitions: 314 | - AttributeName: id 315 | AttributeType: S 316 | KeySchema: 317 | - AttributeName: id 318 | KeyType: HASH 319 | BillingMode: PAY_PER_REQUEST 320 | TableName: listo-boards 321 | 322 | TrelloCredentialsSecret: 323 | Type: AWS::SecretsManager::Secret 324 | Properties: 325 | Name: listo/trello-credentials 326 | Description: Trello API credentials for Listo 327 | 328 | SlackWebHookSecret: 329 | Type: AWS::SecretsManager::Secret 330 | Properties: 331 | Name: listo/slack-web-hook 332 | Description: Slack webhook for Listo 333 | --------------------------------------------------------------------------------