├── src ├── app │ ├── pages │ │ ├── roadmap │ │ │ ├── roadmap.component.css │ │ │ ├── roadmap.component.html │ │ │ ├── roadmap.component.ts │ │ │ └── roadmap.component.spec.ts │ │ ├── usage │ │ │ ├── usage.component.css │ │ │ ├── usage.component.html │ │ │ ├── usage.component.ts │ │ │ └── usage.component.spec.ts │ │ ├── about-us │ │ │ ├── about-us.component.css │ │ │ ├── about-us.component.html │ │ │ ├── about-us.component.ts │ │ │ └── about-us.component.spec.ts │ │ ├── userday │ │ │ ├── userday.component.css │ │ │ ├── userday.component.ts │ │ │ ├── userday.component.html │ │ │ └── userday.component.spec.ts │ │ ├── activity-description │ │ │ ├── activity-description-page.component.html │ │ │ ├── activity-description-page.component.css │ │ │ └── activity-description-page.component.ts │ │ ├── mapping │ │ │ ├── mapping.component.css │ │ │ └── mapping.component.spec.ts │ │ ├── circular-heatmap │ │ │ ├── circular-heatmap.component.spec.ts │ │ │ ├── circular-heatmap.component.css │ │ │ └── circular-heatmap.component.html │ │ ├── teams │ │ │ ├── teams.component.spec.ts │ │ │ ├── teams.component.css │ │ │ └── teams.component.html │ │ ├── matrix │ │ │ ├── matrix.component.css │ │ │ ├── matrix.component.html │ │ │ └── matrix.component.spec.ts │ │ └── settings │ │ │ ├── settings.component.spec.ts │ │ │ └── settings.component.css │ ├── component │ │ ├── sidenav-buttons │ │ │ ├── sidenav-buttons.component.css │ │ │ ├── sidenav-buttons.component.html │ │ │ ├── sidenav-buttons.component.ts │ │ │ └── sidenav-buttons.component.spec.ts │ │ ├── top-header │ │ │ ├── top-header.component.html │ │ │ ├── top-header.component.css │ │ │ ├── top-header.component.ts │ │ │ └── top-header.component.spec.ts │ │ ├── logo │ │ │ ├── logo.component.html │ │ │ ├── logo.component.css │ │ │ ├── logo.component.ts │ │ │ └── logo.component.spec.ts │ │ ├── markdown-viewer │ │ │ ├── markdown-viewer.component.html │ │ │ ├── markdown-viewer.component.css │ │ │ ├── markdown-viewer.component.spec.ts │ │ │ └── markdown-viewer.component.ts │ │ ├── dependency-graph │ │ │ ├── dependency-graph.component.html │ │ │ ├── dependency-graph.component.css │ │ │ └── dependency-graph.component.spec.ts │ │ ├── kpi │ │ │ ├── kpi.component.html │ │ │ ├── kpi.component.ts │ │ │ └── kpi.component.css │ │ ├── modal-message │ │ │ ├── modal-message.component.css │ │ │ ├── modal-message.component.html │ │ │ ├── modal-message.component.spec.ts │ │ │ └── modal-message.component.ts │ │ ├── progress-slider │ │ │ ├── progress-slider.component.css │ │ │ ├── progress-slider.component.html │ │ │ ├── progress-slider.component.ts │ │ │ └── progress-slider.component.spec.ts │ │ ├── teams-groups-editor │ │ │ ├── teams-groups-editor.module.ts │ │ │ ├── teams-groups-editor.component.css │ │ │ ├── selectable-list.component.css │ │ │ ├── teams-groups-editor.component.html │ │ │ ├── selectable-list.component.html │ │ │ └── selectable-list.component.ts │ │ └── activity-description │ │ │ └── activity-description.component.ts │ ├── model │ │ ├── sector.ts │ │ ├── types.ts │ │ ├── ignore-list.ts │ │ ├── markdown-text.ts │ │ ├── data-store.ts │ │ └── meta-store.ts │ ├── util │ │ ├── ArrayHash.ts │ │ ├── download.ts │ │ └── util.ts │ ├── pipe │ │ ├── to-string-value.pipe.spec.ts │ │ └── to-string-value.pipe.ts │ ├── service │ │ ├── loader │ │ │ ├── data-loader.service.spec.ts │ │ │ └── mock-data-loader.service.ts │ │ ├── theme.service.ts │ │ ├── notification.service.ts │ │ ├── title.service.ts │ │ ├── yaml-loader │ │ │ └── yaml-loader.service.spec.ts │ │ ├── settings │ │ │ ├── github.service.ts │ │ │ ├── settings.service.spec.ts │ │ │ └── settings.service.ts │ │ └── sector-service.ts │ ├── app.component.css │ ├── app.component.spec.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app-routing.module.ts │ ├── material │ │ └── material.module.ts │ └── app.module.ts ├── favicon.ico ├── assets │ ├── YAML │ │ ├── team-progress.yaml │ │ ├── default │ │ │ └── teams.yaml │ │ ├── custom │ │ │ └── custom-activities.yaml │ │ └── meta.yaml │ ├── images │ │ ├── logo.png │ │ ├── logo-image.png │ │ ├── userday │ │ │ ├── Brook.png │ │ │ ├── Timo.png │ │ │ ├── Jannik.jpg │ │ │ └── Francesco.png │ │ └── sponsors │ │ │ └── heroku.png │ ├── presentations │ │ └── userday-sf-2024-reach-your-dynamic-depth-with-owasp-securecodebox.pdf │ └── Markdown Files │ │ ├── ABOUT-FORK.md │ │ ├── userday2026-eur.md │ │ ├── TODO-headlines.md │ │ ├── userday2025.md │ │ ├── TODO.md │ │ └── USAGE.md ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── main.ts ├── test.ts ├── index.html ├── styles.css ├── polyfills.ts └── custom-theme.scss ├── .phptidy-config.php ├── .github ├── FUNDING.yml ├── workflows │ ├── ESLint.yml │ ├── tests.yml │ ├── depoy.yml │ ├── stale.yaml │ └── main.yml ├── dependabot.yml └── proposal.md ├── .prettierrc.json ├── tsconfig.spec.json ├── .releaserc.json ├── tsconfig.app.json ├── Caddyfile ├── .prettierignore ├── .gitignore ├── tsconfig.json ├── Dockerfile ├── karma.conf.js ├── .eslintrc.json ├── TODO.md ├── CHANGELOG.md ├── Issue.md ├── package.json ├── Development.md ├── angular.json └── CODE_OF_CONDUCT.md /src/app/pages/roadmap/roadmap.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/usage/usage.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/about-us/about-us.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/component/sidenav-buttons/sidenav-buttons.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.phptidy-config.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/app/component/top-header/top-header.component.html: -------------------------------------------------------------------------------- 1 |

{{ section }}

2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/component/top-header/top-header.component.css: -------------------------------------------------------------------------------- 1 | .mat-display-1 { 2 | padding: 20px; 3 | margin: 0; 4 | } -------------------------------------------------------------------------------- /src/assets/YAML/team-progress.yaml: -------------------------------------------------------------------------------- 1 | # Export team progress from the browser, and replace this file 2 | progress: 3 | -------------------------------------------------------------------------------- /src/app/component/logo/logo.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/pages/userday/userday.component.css: -------------------------------------------------------------------------------- 1 | .userday-inline { 2 | padding-left: 30px; 3 | padding-bottom: 20px; 4 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | experimental: true, 4 | }; 5 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/logo-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/logo-image.png -------------------------------------------------------------------------------- /src/assets/images/userday/Brook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/userday/Brook.png -------------------------------------------------------------------------------- /src/assets/images/userday/Timo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/userday/Timo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://owasp.org/donate/?reponame=www-project-devsecops-maturity-model&title=OWASP+Devsecops+Maturity+Model 2 | github: OWASP 3 | -------------------------------------------------------------------------------- /src/app/component/markdown-viewer/markdown-viewer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /src/assets/images/sponsors/heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/sponsors/heroku.png -------------------------------------------------------------------------------- /src/assets/images/userday/Jannik.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/userday/Jannik.jpg -------------------------------------------------------------------------------- /src/app/component/dependency-graph/dependency-graph.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/component/markdown-viewer/markdown-viewer.component.css: -------------------------------------------------------------------------------- 1 | .main-section{ 2 | padding: 20px; 3 | padding-top: 0px; 4 | max-width: 40rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/images/userday/Francesco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/images/userday/Francesco.png -------------------------------------------------------------------------------- /src/app/model/sector.ts: -------------------------------------------------------------------------------- 1 | import { Activity } from './activity-store'; 2 | 3 | export interface Sector { 4 | dimension: string; 5 | level: number; 6 | activities: Activity[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/about-us/about-us.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/component/logo/logo.component.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 200px; 3 | height: auto; 4 | text-align: center; 5 | padding: 5px; 6 | /*border: 3px solid blue; */ 7 | } -------------------------------------------------------------------------------- /src/app/pages/usage/usage.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/util/ArrayHash.ts: -------------------------------------------------------------------------------- 1 | export function appendHashElement(hash: Record, key: string, element: any): void { 2 | if (!hash.hasOwnProperty(key)) { 3 | hash[key] = []; 4 | } 5 | hash[key].push(element); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/pages/roadmap/roadmap.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /src/assets/presentations/userday-sf-2024-reach-your-dynamic-depth-with-owasp-securecodebox.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/HEAD/src/assets/presentations/userday-sf-2024-reach-your-dynamic-depth-with-owasp-securecodebox.pdf -------------------------------------------------------------------------------- /src/app/pipe/to-string-value.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ToStringValuePipe } from './to-string-value.pipe'; 2 | 3 | describe('ToStringValuePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new ToStringValuePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/component/kpi/kpi.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ title }}
3 |
4 | {{ value }} 5 | {{ suffix }} 6 |
7 |
{{ subtitle }}
8 |
9 | -------------------------------------------------------------------------------- /src/app/component/modal-message/modal-message.component.css: -------------------------------------------------------------------------------- 1 | .dialog { 2 | margin: 0.5em; 3 | padding: 1em; 4 | } 5 | 6 | .dialog-buttons { 7 | display: flex; 8 | justify-content: flex-end; 9 | } 10 | 11 | button { 12 | min-width: 5rem; 13 | margin: 0 1rem; 14 | } -------------------------------------------------------------------------------- /src/app/component/logo/logo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-logo', 5 | templateUrl: './logo.component.html', 6 | styleUrls: ['./logo.component.css'], 7 | }) 8 | export class LogoComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "trailingComma": "es5", 9 | "bracketSameLine": true, 10 | "printWidth": 100, 11 | "endOfLine": "auto" 12 | } -------------------------------------------------------------------------------- /src/app/pages/about-us/about-us.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about-us', 5 | templateUrl: './about-us.component.html', 6 | styleUrls: ['./about-us.component.css'], 7 | }) 8 | export class AboutUsComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/pipe/to-string-value.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'ToStringValue', 5 | pure: true, 6 | }) 7 | export class ToStringValuePipe implements PipeTransform { 8 | transform(value: unknown): string { 9 | return value as string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ESLint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint Check 2 | on: [push,pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install modules 9 | run: yarn 10 | - name: Run ESLint 11 | run: yarn run eslint . --ext .js,.jsx,.ts,.tsx 12 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | experimental: true, 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/component/top-header/top-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-top-header', 5 | templateUrl: './top-header.component.html', 6 | styleUrls: ['./top-header.component.css'], 7 | }) 8 | export class TopHeaderComponent { 9 | @Input() section: string = 'Default'; 10 | 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/util/download.ts: -------------------------------------------------------------------------------- 1 | export function downloadYamlFile(data: string, filename: string): void { 2 | const blob = new Blob([data], { type: 'application/yaml' }); 3 | const url = URL.createObjectURL(blob); 4 | const a = document.createElement('a'); 5 | a.href = url; 6 | a.download = filename.split('/').pop() || 'download.yaml'; 7 | a.click(); 8 | a.remove(); 9 | URL.revokeObjectURL(url); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/component/progress-slider/progress-slider.component.css: -------------------------------------------------------------------------------- 1 | .progress-container { 2 | display: flex; 3 | align-items: center; 4 | gap: 1rem; 5 | width: 100%; 6 | } 7 | 8 | .mat-slider-horizontal { 9 | min-width: 80px; 10 | } 11 | 12 | .mat-slider-track-fill { 13 | background-color: #66bb6a; 14 | height: 10px; 15 | } 16 | .step-label { 17 | min-width: 80px; 18 | font-size: smaller; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/component/kpi/kpi.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-kpi', 5 | templateUrl: './kpi.component.html', 6 | styleUrls: ['./kpi.component.css'], 7 | }) 8 | export class KpiComponent { 9 | @Input() title: string = ''; 10 | @Input() value: number | string | undefined = ''; 11 | @Input() suffix: string = ''; 12 | @Input() subtitle: string = ''; 13 | } 14 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "main", 3 | "plugins": [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | "preset": "angular", 8 | "releaseRules": [ 9 | {"breaking": true, "release": "minor"}, 10 | {"tag": "Breaking", "release": "minor"} 11 | ] 12 | } 13 | ], 14 | "@semantic-release/release-notes-generator", 15 | "@semantic-release/github" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.ts" 14 | ], 15 | "exclude": [ 16 | "src/test.ts", 17 | "src/**/*.spec.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | http_port {$PORT} 3 | } 4 | 5 | :{$PORT}, localhost:{$PORT} { 6 | log stdout 7 | file_server { 8 | root /srv 9 | } 10 | try_files {path} {path}/ /index.html 11 | 12 | header / { 13 | X-Content-Type-Options "nosniff" 14 | X-XSS-Protection "1; mode=block" 15 | X-Robots-Tag "none" 16 | X-Download-Options "noopen" 17 | X-Permitted-Cross-Domain-Policies "none" 18 | Referrer-Policy "no-referrer" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/pages/roadmap/roadmap.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { perfNow } from 'src/app/util/util'; 3 | 4 | @Component({ 5 | selector: 'app-roadmap', 6 | templateUrl: './roadmap.component.html', 7 | styleUrls: ['./roadmap.component.css'], 8 | }) 9 | export class RoadmapComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() { 13 | console.log(`${perfNow()}: Page loaded: Roadmap`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/userday/userday.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { perfNow } from 'src/app/util/util'; 3 | 4 | @Component({ 5 | selector: 'app-userday', 6 | templateUrl: './userday.component.html', 7 | styleUrls: ['./userday.component.css'], 8 | }) 9 | export class UserdayComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() { 13 | console.log(`${perfNow()}: Page loaded: Userday`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Use Node.js 16.16.0 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 16.0 15 | - name: Install dependencies 16 | run: npm install --legacy-peer-deps 17 | - name: Test 18 | run: npm test -- --watch=false --browsers=ChromeHeadless 19 | -------------------------------------------------------------------------------- /src/app/pages/activity-description/activity-description-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/component/modal-message/modal-message.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ data.title }}

3 |

4 |
5 |
6 | 14 |
15 | -------------------------------------------------------------------------------- /src/app/component/progress-slider/progress-slider.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 | {{ steps[currentValue] }} 11 | * 12 | ** 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/assets/YAML/default/teams.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Teams 4 | # 5 | # This file defines the teams and what groups they belong to. 6 | # 7 | # Either edit this file, or you can add your own file 8 | # and update the reference in `meta.yaml`. 9 | # 10 | teams: 11 | - Team A 12 | - Team B 13 | - Team C 14 | - Team D 15 | 16 | teamGroups: 17 | Customer: 18 | - Team A 19 | - Team B 20 | Internal: 21 | - Team C 22 | - Team D 23 | Cloud: 24 | - Team A 25 | - Team D 26 | On-premise: 27 | - Team B 28 | - Team C 29 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/ABOUT-FORK.md: -------------------------------------------------------------------------------- 1 | # About this fork 2 | This fork [vbakke/DevSecOps-MaturityModel](https://github.com/vbakke/DevSecOps-MaturityModel), is a development branch for the official [devsecopsmaturitymodel/DevSecOps-MaturityModel](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel). 3 | 4 | It's purpose is to be a test bed for features not yet included in the official branch. 5 | 6 | ## Scalable circular map 7 | - Responsive UI *[2024-10-20]* 8 | - Responsive UI *[2024-10-20]* 9 | - Circle is no longer fixed size *[2024-10-20]* 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /src/app/pages/userday/userday.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |

Archive

7 | Previous DSOMM User Day pages with the full list of talks, downloadable material, and YouTube 8 | links. 9 |
10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/teams-groups-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TeamsGroupsEditorComponent } from './teams-groups-editor.component'; 4 | import { SelectableListComponent } from './selectable-list.component'; 5 | import { MaterialModule } from '../../material/material.module'; 6 | import { FormsModule } from '@angular/forms'; 7 | 8 | @NgModule({ 9 | declarations: [TeamsGroupsEditorComponent, SelectableListComponent], 10 | imports: [CommonModule, MaterialModule, FormsModule], 11 | exports: [TeamsGroupsEditorComponent], 12 | }) 13 | export class TeamsGroupsEditorModule {} 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | const savedTheme = localStorage.getItem('theme') || 'light'; 2 | document.body.classList.remove('light-theme', 'dark-theme'); 3 | document.body.classList.add(`${savedTheme}-theme`); 4 | console.log('[main.ts] Theme set to:', savedTheme); // 5 | 6 | import { enableProdMode } from '@angular/core'; 7 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 8 | 9 | import { AppModule } from './app/app.module'; 10 | import { environment } from './environments/environment'; 11 | 12 | if (environment.production) { 13 | enableProdMode(); 14 | } 15 | 16 | platformBrowserDynamic() 17 | .bootstrapModule(AppModule) 18 | .catch(err => console.error(err)); 19 | -------------------------------------------------------------------------------- /src/app/model/types.ts: -------------------------------------------------------------------------------- 1 | export type TeamGroups = Record; 2 | export type TeamNames = string[]; 3 | 4 | export interface TeamProgressFile { 5 | progress: Progress; 6 | } 7 | 8 | export interface ProgressDefinition { 9 | score: number; 10 | definition: string; 11 | } 12 | 13 | export type ProgressDefinitions = Record; 14 | export type Progress = Record; 15 | export type ActivityProgress = Record; 16 | export type TeamProgress = Record; 17 | export type Uuid = string; 18 | export type TeamName = string; 19 | export type GroupName = string; 20 | export type ProgressTitle = string; 21 | -------------------------------------------------------------------------------- /src/app/pages/activity-description/activity-description-page.component.css: -------------------------------------------------------------------------------- 1 | .page-container { 2 | padding: 20px; 3 | max-width: 1200px; 4 | margin: 0 auto; 5 | } 6 | 7 | .loading-container { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 50px; 13 | text-align: center; 14 | } 15 | 16 | .error-container { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | padding: 50px; 22 | text-align: center; 23 | color: #f44336; 24 | } 25 | 26 | .error-container mat-icon { 27 | font-size: 48px; 28 | width: 48px; 29 | height: 48px; 30 | margin-bottom: 20px; 31 | } -------------------------------------------------------------------------------- /src/app/component/kpi/kpi.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | flex: 1; 3 | min-width: 0; 4 | } 5 | 6 | .info-kpi { 7 | flex: 1; 8 | padding: 20px; 9 | background-color: var(--background-secondary); 10 | border-radius: 8px; 11 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 12 | } 13 | .info-kpi .kpi-title { 14 | font-size: 1.1rem; 15 | font-style: italic; 16 | margin-bottom: 10px; 17 | color: var(--text-secondary); 18 | } 19 | .info-kpi .kpi-subtitle { 20 | font-size: 1em; 21 | color: var(--text-secondary); 22 | } 23 | .info-kpi .kpi-value { 24 | font-size: 1.5em; 25 | font-weight: bold; 26 | } 27 | .info-kpi .kpi-value .kpi-suffix { 28 | font-weight: normal; 29 | font-size: 1.2rem; 30 | } -------------------------------------------------------------------------------- /src/app/component/logo/logo.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LogoComponent } from './logo.component'; 4 | 5 | describe('LogoComponent', () => { 6 | let component: LogoComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [LogoComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LogoComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/about-us/about-us.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutUsComponent } from './about-us.component'; 4 | 5 | describe('AboutUsComponent', () => { 6 | let component: AboutUsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [AboutUsComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(AboutUsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/roadmap/roadmap.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RoadmapComponent } from './roadmap.component'; 4 | 5 | describe('RoadmapComponent', () => { 6 | let component: RoadmapComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RoadmapComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RoadmapComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/pages/userday/userday.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserdayComponent } from './userday.component'; 4 | 5 | describe('UserdayComponent', () => { 6 | let component: UserdayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserdayComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserdayComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/component/dependency-graph/dependency-graph.component.css: -------------------------------------------------------------------------------- 1 | circle { 2 | cursor: pointer; 3 | stroke: #000; 4 | stroke-width: .5px; 5 | } 6 | line.link { 7 | fill: none; 8 | stroke: #000; 9 | stroke-width: 1.5px; 10 | } 11 | 12 | .clickable { 13 | cursor: pointer; 14 | } 15 | 16 | :host ::ng-deep svg.dependency-graph g .node-rect { 17 | fill: var(--node-fill); 18 | stroke: var(--node-border); 19 | stroke-width: 1.5; 20 | } 21 | 22 | :host ::ng-deep svg.dependency-graph g.clickable { 23 | cursor: pointer; 24 | } 25 | 26 | :host ::ng-deep svg.dependency-graph g.hovered .node-rect { 27 | fill: var(--node-hover-fill); 28 | } 29 | 30 | :host ::ng-deep svg.dependency-graph g.main text { 31 | fill: var(--text-primary, inherit); 32 | } -------------------------------------------------------------------------------- /src/app/component/sidenav-buttons/sidenav-buttons.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ Icons[i] }} 4 |

{{ Options[i] }}

5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ isNightMode ? 'light_mode' : 'dark_mode' }} 15 | 16 |

17 | {{ isNightMode ? 'Switch to Light Mode' : 'Switch to Dark Mode' }} 18 |

19 |
20 |
21 | -------------------------------------------------------------------------------- /src/app/model/ignore-list.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyObj } from '../util/util'; 2 | import { Activity } from './activity-store'; 3 | 4 | export class IgnoreList { 5 | list: Record> = {}; 6 | 7 | add(type: string, id: string) { 8 | if (!this.list.hasOwnProperty(type)) { 9 | this.list[type] = {}; 10 | } 11 | this.list[type][id] = true; 12 | } 13 | 14 | hasActivity(activity: Activity): boolean { 15 | if (isEmptyObj(this.list)) return false; 16 | 17 | for (let type in this.list) { 18 | let id: string = activity[type as keyof Activity].toString(); 19 | if (this.list[type][id]) return true; 20 | } 21 | return false; 22 | } 23 | 24 | isEmpty(): boolean { 25 | return isEmptyObj(this.list); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/service/loader/data-loader.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { LoaderService } from './data-loader.service'; 3 | import { YamlService } from '../yaml-loader/yaml-loader.service'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | 7 | describe('DataLoaderService', () => { 8 | let service: LoaderService; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [HttpClientTestingModule, MatDialogModule], 13 | providers: [LoaderService, YamlService], 14 | }); 15 | service = TestBed.inject(LoaderService); 16 | }); 17 | 18 | it('should be created', () => { 19 | expect(service).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/pages/mapping/mapping.component.css: -------------------------------------------------------------------------------- 1 | .content{ 2 | width: 100%; 3 | } 4 | 5 | .actions-row { 6 | display: flex; 7 | align-items: center; 8 | gap: 16px; 9 | margin: 0 20px; 10 | flex-wrap: wrap; 11 | } 12 | 13 | .search-box { 14 | flex: 1 1 320px; 15 | min-width: 220px; 16 | max-width: 400px; 17 | } 18 | 19 | .export-btn { 20 | margin-left: auto; 21 | min-width: 140px; 22 | } 23 | 24 | .matrix-table { 25 | margin: 20px; 26 | } 27 | 28 | .mat-cell, .mat-header-cell { 29 | padding: 20px 10px; 30 | width: 12.5%; 31 | max-width: 12.5%; 32 | word-wrap: break-word; 33 | } 34 | 35 | .mat-header-cell { 36 | font-size: 16px; 37 | font-weight: 500; 38 | } 39 | 40 | .content ::ng-deep .mat-form-field-wrapper { 41 | padding-bottom: 0; 42 | } 43 | 44 | .hide{ 45 | display: none; 46 | } -------------------------------------------------------------------------------- /src/app/service/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class ThemeService { 6 | private themeSubject = new BehaviorSubject('light'); 7 | public readonly theme$ = this.themeSubject.asObservable(); 8 | 9 | constructor() {} 10 | 11 | initTheme(): void { 12 | const saved = localStorage.getItem('theme') || 'light'; 13 | this.setTheme(saved); 14 | } 15 | 16 | setTheme(theme: string): void { 17 | document.body.classList.remove('light-theme', 'dark-theme'); 18 | document.body.classList.add(`${theme}-theme`); 19 | localStorage.setItem('theme', theme); 20 | this.themeSubject.next(theme); 21 | } 22 | 23 | getTheme(): string { 24 | return this.themeSubject.value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/pages/usage/usage.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { perfNow } from 'src/app/util/util'; 4 | 5 | @Component({ 6 | selector: 'app-usage', 7 | templateUrl: './usage.component.html', 8 | styleUrls: ['./usage.component.css'], 9 | }) 10 | export class UsageComponent implements OnInit { 11 | page: string = 'USAGE'; 12 | constructor(private route: ActivatedRoute) {} 13 | 14 | ngOnInit() { 15 | if (this.route && this.route.params) { 16 | this.route.params.subscribe(params => { 17 | let page = params['page']; 18 | // CWE-79 - sanitize input 19 | if (page.match(/^[\w.-]+$/)) { 20 | this.page = page; 21 | } 22 | }); 23 | } 24 | console.log(`${perfNow()}: Page loaded: Usage`); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context( 12 | path: string, 13 | deep?: boolean, 14 | filter?: RegExp 15 | ): { 16 | (id: string): T; 17 | keys(): string[]; 18 | }; 19 | }; 20 | 21 | // First, initialize the Angular testing environment. 22 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # angular cache 4 | /.angular/cache 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | 27 | # Cache 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | 46 | /src/assets/YAML/generated/generated.yaml -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DSOMM 6 | 7 | 8 | 9 | 10 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # angular cache 4 | /.angular/cache 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | 27 | # Cache 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | /yaml-generation/vendor/ 46 | # Generated YAML 47 | /src/assets/YAML/generated/generated.yaml 48 | -------------------------------------------------------------------------------- /src/app/model/markdown-text.ts: -------------------------------------------------------------------------------- 1 | import * as md from 'markdown-it'; 2 | 3 | let markdown: md = md({ html: true }); 4 | 5 | export class MarkdownText { 6 | private plain: string | undefined; 7 | private md: string | undefined; 8 | 9 | constructor(text: MarkdownText | string | undefined) { 10 | if (text instanceof MarkdownText) { 11 | this.plain = text.plain; 12 | this.md = text.md; 13 | } else { 14 | this.plain = text; 15 | this.md = undefined; 16 | } 17 | } 18 | 19 | empty(): boolean { 20 | return this.plain == undefined || this.plain.trim().length == 0; 21 | } 22 | 23 | hasContent(): boolean { 24 | return !this.empty(); 25 | } 26 | 27 | toString(): string { 28 | return this.plain || ''; 29 | } 30 | 31 | render(): string { 32 | if (!this.plain) return ''; 33 | if (!this.md) this.md = markdown.render(this.plain); 34 | return this.md; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/component/markdown-viewer/markdown-viewer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { MarkdownViewerComponent } from './markdown-viewer.component'; 4 | 5 | describe('MarkdownViewerComponent', () => { 6 | let component: MarkdownViewerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | declarations: [MarkdownViewerComponent], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MarkdownViewerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/teams-groups-editor.component.css: -------------------------------------------------------------------------------- 1 | .editor-header { 2 | text-align: center; 3 | margin-bottom: 1rem; 4 | } 5 | 6 | .editor-container { 7 | display: flex; 8 | gap: 2rem; 9 | justify-content: center; 10 | } 11 | 12 | .editor-list-panel { 13 | background-color: var(--background-secondary); 14 | border-radius: 8px; 15 | box-shadow: 0 2px 8px rgba(0,0,0,0.06); 16 | padding: 1rem; 17 | min-width: 260px; 18 | flex: 1 1 0; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .editor-list-header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | font-weight: bold; 28 | font-size: 1.1rem; 29 | margin-bottom: 0.5rem; 30 | } 31 | 32 | .editor-footer { 33 | text-align: center; 34 | margin-top: 2rem; 35 | } 36 | 37 | @media only screen and (max-width: 750px) { 38 | .editor-container { 39 | flex-direction: column; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | /* --- experimental branch --- */ 2 | /* .mat-drawer-container { 3 | background-color: #fffff4; 4 | } */ 5 | .tag-line { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | .tag-title { 11 | font-size: 0.9em; 12 | } 13 | .tag-subtitle { 14 | font-size: 0.7em; 15 | } 16 | 17 | /* --- experimental branch end --- */ 18 | 19 | .main-container { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | .sidenav-content { 25 | display: flex; 26 | padding: 10px; 27 | justify-content: space-between; 28 | } 29 | 30 | .sidenav-menu { 31 | padding: 20px; 32 | } 33 | 34 | .menu-close { 35 | position: absolute; 36 | right: 0; 37 | top: 2px; 38 | background: transparent; 39 | border: 0; 40 | color: #ddd; 41 | } 42 | 43 | .github-fork-ribbon:before { 44 | background-color: #333; 45 | } 46 | 47 | @media only screen and (max-width: 750px) { 48 | .github-fork-ribbon { 49 | display: none; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/component/dependency-graph/dependency-graph.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHandler } from '@angular/common/http'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { DependencyGraphComponent } from './dependency-graph.component'; 4 | import { MatDialogModule } from '@angular/material/dialog'; 5 | 6 | describe('DependencyGraphComponent', () => { 7 | let component: DependencyGraphComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [DependencyGraphComponent], 13 | imports: [MatDialogModule], 14 | providers: [HttpClient, HttpHandler], 15 | }).compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(DependencyGraphComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/service/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 4 | import { 5 | ModalMessageComponent, 6 | DialogInfo, 7 | } from '../component/modal-message/modal-message.component'; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class NotificationService { 11 | private messageSubject = new Subject<{ title: string; message: string; error: any }>(); 12 | message$ = this.messageSubject.asObservable(); 13 | 14 | constructor(private dialog: MatDialog) {} 15 | 16 | notify(title: string, message: string, error: any = null) { 17 | this.messageSubject.next({ title, message, error }); 18 | 19 | const dialogConfig = new MatDialogConfig(); 20 | dialogConfig.id = 'modal-message'; 21 | dialogConfig.disableClose = true; 22 | dialogConfig.autoFocus = false; 23 | dialogConfig.data = new DialogInfo(message, title); 24 | 25 | this.dialog.open(ModalMessageComponent, dialogConfig); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/depoy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Heroku 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | 7 | 8 | jobs: 9 | heroku: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "Check out Git repository" 13 | uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 14 | - name: "Set Heroku app & branch for ${{ github.ref }}" 15 | run: | 16 | echo $GITHUB_REF 17 | if [ "$GITHUB_REF" == "refs/heads/main" ]; then 18 | echo "HEROKU_APP=" >> $GITHUB_ENV 19 | fi 20 | echo "HEROKU_BRANCH=main" >> $GITHUB_ENV 21 | - name: Install Heroku CLI 22 | run: | 23 | curl https://cli-assets.heroku.com/install.sh | sh 24 | - name: "Deploy ${{ github.ref }} to Heroku" 25 | uses: akhileshns/heroku-deploy@v3.13.15 26 | with: 27 | heroku_api_key: ${{ secrets.HEROKU_API_KEY }} 28 | heroku_app_name: "dsomm" 29 | heroku_email: timo.pagel@owasp.org 30 | branch: ${{ env.HEROKU_BRANCH }} 31 | usedocker: true 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24.7.0-alpine3.22 AS build 2 | 3 | ARG COMMIT_HASH 4 | ARG COMMIT_DATE 5 | ARG GIT_BRANCH 6 | 7 | WORKDIR /usr/src/app 8 | COPY package.json package-lock.json ./ 9 | 10 | RUN apk add --upgrade python3 build-base py3-setuptools py3-pip \ 11 | && pip3 install --break-system-packages setuptools \ 12 | && npm install 13 | COPY . . 14 | RUN npm run build --configuration=production 15 | 16 | RUN mkdir -p /usr/src/app/dist/dsomm/assets && \ 17 | echo "commit: \"${COMMIT_HASH:-unknown}\"" > /usr/src/app/dist/dsomm/assets/build-info.yaml && \ 18 | echo "commit_date: \"${COMMIT_DATE:-unknown}\"" >> /usr/src/app/dist/dsomm/assets/build-info.yaml && \ 19 | echo "branch: \"${GIT_BRANCH:-unknown}\"" >> /usr/src/app/dist/dsomm/assets/build-info.yaml 20 | 21 | 22 | FROM wurstbrot/dsomm-yaml-generation:1.24.0 AS yaml 23 | 24 | FROM caddy:2.10.2 25 | ENV PORT=8080 26 | 27 | COPY Caddyfile /etc/caddy/Caddyfile 28 | COPY --from=build ["/usr/src/app/dist/dsomm/", "/srv"] 29 | COPY --from=yaml ["/var/www/html/generated/model.yaml", "/srv/assets/YAML/default/model.yaml"] 30 | -------------------------------------------------------------------------------- /src/app/component/markdown-viewer/markdown-viewer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import * as md from 'markdown-it'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | @Component({ 6 | selector: 'app-markdown-viewer', 7 | templateUrl: './markdown-viewer.component.html', 8 | styleUrls: ['./markdown-viewer.component.css'], 9 | }) 10 | export class MarkdownViewerComponent implements OnInit { 11 | @Input() MDFile: string = ''; 12 | markdown: md = md({ 13 | html: true, 14 | }); 15 | markdownURI: any; 16 | toRender: string = ''; 17 | constructor(private http: HttpClient) {} 18 | 19 | ngOnInit(): void { 20 | this.loadMarkdownFiles(this.MDFile); 21 | } 22 | 23 | async loadMarkdownFiles(MDFile: string): Promise { 24 | try { 25 | this.markdownURI = await this.http.get(MDFile, { responseType: 'text' }).toPromise(); 26 | this.toRender = this.markdown.render(this.markdownURI); 27 | return true; 28 | } catch { 29 | this.toRender = 'Markdown file could not be found'; 30 | return false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PR' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: > 13 | This issue has been automatically marked as `stale` because it has not had 14 | recent activity. :calendar: It will be _closed automatically_ in one week if no further activity occurs. 15 | stale-pr-message: > 16 | This PR has been automatically marked as `stale` because it has not had 17 | recent activity. :calendar: It will be _closed automatically_ in two weeks if no further activity occurs. 18 | close-issue-message: This issue was closed because it has been stalled for 7 days with no activity. 19 | close-pr-message: This PR was closed because it has been stalled for 20 days with no activity. 20 | days-before-stale: 35 21 | days-before-close: 7 22 | days-before-pr-close: 20 23 | exempt-issue-labels: 'critical,technical debt' 24 | exempt-assignees: wurstbrot 25 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | let app: AppComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [RouterTestingModule], 12 | declarations: [AppComponent], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AppComponent); 18 | app = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create the app', () => { 23 | expect(app).toBeTruthy(); 24 | }); 25 | 26 | it('check for fork me on github ribbon generation', () => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | const HTMLElement: HTMLElement = fixture.nativeElement; 29 | var divTag = HTMLElement.querySelector('div')!; 30 | var aTag = divTag.querySelector('a')!; 31 | expect(aTag.textContent).toContain('GitHub'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 | 14 |
15 |
{{ title || defaultTitle }}
16 |
{{ subtitle }}
17 |
18 |
19 | Fork me on GitHub 27 |
28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/selectable-list.component.css: -------------------------------------------------------------------------------- 1 | .selectable-list-header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | font-weight: bold; 6 | font-size: 1.1rem; 7 | margin-bottom: 0.5rem; 8 | } 9 | 10 | .selectable-list input { 11 | padding: 0.4rem; 12 | } 13 | 14 | .selectable-list ul { 15 | list-style: none; 16 | padding: 0; 17 | margin: 0; 18 | } 19 | .selectable-list li { 20 | display: flex; 21 | align-items: center; 22 | justify-content: space-between; 23 | padding: 0.5rem 0.75rem; 24 | border-radius: 4px; 25 | margin-bottom: 0.25rem; 26 | cursor: pointer; 27 | transition: background 0.2s; 28 | } 29 | .selectable-list li.selected { 30 | background: color-mix(in oklab, var(--primary-color), black 10%); 31 | color: white; 32 | } 33 | .selectable-list li.highlighted { 34 | background: color-mix(in oklab, var(--primary-color), transparent 70%); 35 | } 36 | .selectable-list li:not(.selected):not(.highlighted) { 37 | opacity: 0.6; 38 | } 39 | .selectable-list button { 40 | margin-left: 0.5rem; 41 | } 42 | 43 | .selectable-list .edit-hint { 44 | font-weight: normal; 45 | font-size: 0.8em; 46 | color: #666;; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/service/loader/mock-data-loader.service.ts: -------------------------------------------------------------------------------- 1 | // Create mock LoaderService 2 | import { Data } from 'src/app/model/activity-store'; 3 | import { DataStore } from 'src/app/model/data-store'; 4 | 5 | /* eslint-disable */ 6 | const FALLBACK_MOCK_META: any = { 7 | progressDefinition: { 8 | NOT_STARTED: 0.00, 9 | IN_PROGRESS: 0.40, 10 | COMPLETED: 1.00 11 | }, 12 | teams: ['Team A', 'Team B', 'Team C'], 13 | teamGroups: { AB: ['Team A', 'Team B'] }, 14 | } 15 | /* eslint-enable */ 16 | 17 | export class MockLoaderService { 18 | private MOCK_DATA: Data; 19 | private dataStore: DataStore | null = null; 20 | 21 | constructor(MOCK_DATA: Data) { 22 | this.MOCK_DATA = MOCK_DATA; 23 | } 24 | 25 | load() { 26 | console.log('MOCK loader service'); 27 | let errors: string[] = []; 28 | this.dataStore = new DataStore(); 29 | this.dataStore?.meta?.addMeta(FALLBACK_MOCK_META); 30 | this.dataStore?.activityStore?.addActivityFile(this.MOCK_DATA, errors); 31 | console.log('MOCK dataStore:', this.dataStore); 32 | return Promise.resolve(this.dataStore); 33 | } 34 | getLevelTitles() { 35 | return ['Level 1', 'Level 2', 'Level 3', 'Level 4', 'Level 5']; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/userday2026-eur.md: -------------------------------------------------------------------------------- 1 | ## Upcoming SAMM & DSOMM User Day in Vienna 2026 2 | 3 | The OWASP DSOMM team is excited to announce our upcoming User Day, this time teaming up with [OWASP SAMM](https://owasp.org/www-project-samm/), as part of [Global AppSec Europe (Vienna)](https://owasp.glueup.com/event/owasp-global-appsec-eu-2026-vienna-austria-162243/). 4 | 5 | We’ll be spending the day sharing experiences, exploring real-world use of SAMM and DSOMM, and learning from each other about how to advance software security maturity. Whether you're deep into assessments or just getting started, we’d love to hear your perspective. 6 | 7 | ### Location 8 | Austria Center \ 9 | Bruno-Kreisky-Platz 1 \ 10 | Wien, Austria 11 | 12 | 24 April 2025 13 | 14 | ### Agenda 15 | 16 | See [https://owaspsamm.org/user-day/](https://owaspsamm.org/user-day/) for latest update of the agenda. 17 | 18 | 19 | | Title | Speaker | 20 | |-------------------------------------------------------|------------------------------------------| 21 | | _To be decided_ | Aram Hovsepyan, Timo Pagel and many more | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app/service/title.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | export interface TitleInfo { 5 | level?: number; 6 | dimension?: string; 7 | subdimension?: string; 8 | } 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class TitleService { 12 | private titleSubject = new BehaviorSubject(null); 13 | public readonly titleInfo$ = this.titleSubject.asObservable(); 14 | 15 | constructor() {} 16 | 17 | setTitle(titleInfo: TitleInfo | null): void { 18 | this.titleSubject.next(titleInfo); 19 | } 20 | 21 | clearTitle(): void { 22 | this.titleSubject.next(null); 23 | } 24 | 25 | formatTitle(titleInfo: TitleInfo | null, defaultTitle: string): string { 26 | if (!titleInfo) { 27 | return defaultTitle; 28 | } 29 | 30 | let parts: string[] = []; 31 | 32 | if (titleInfo.dimension) { 33 | parts.push(titleInfo.dimension); 34 | } 35 | 36 | if (titleInfo.level !== undefined) { 37 | parts.push(`Level ${titleInfo.level}`); 38 | } 39 | 40 | if (titleInfo.subdimension) { 41 | parts.push(titleInfo.subdimension); 42 | } 43 | 44 | return parts.length > 0 ? parts.join(' - ') : defaultTitle; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/pages/usage/usage.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UsageComponent } from './usage.component'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { of } from 'rxjs'; 6 | 7 | describe('UsageComponent', () => { 8 | let component: UsageComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | declarations: [UsageComponent], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create', () => { 18 | TestBed.overrideProvider(ActivatedRoute, { 19 | useValue: { params: of({}) }, 20 | }); 21 | 22 | fixture = TestBed.createComponent(UsageComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | 26 | expect(component).toBeTruthy(); 27 | expect(component.page).toBe('USAGE'); 28 | }); 29 | 30 | it('should load page', () => { 31 | TestBed.overrideProvider(ActivatedRoute, { 32 | useValue: { params: of({ page: 'test-page' }) }, 33 | }); 34 | 35 | fixture = TestBed.createComponent(UsageComponent); 36 | component = fixture.componentInstance; 37 | fixture.detectChanges(); 38 | 39 | expect(component.page).toBe('test-page'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/component/top-header/top-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TopHeaderComponent } from './top-header.component'; 4 | 5 | describe('TopHeaderComponent', () => { 6 | let component: TopHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TopHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TopHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | 25 | it('check if header is being generated', () => { 26 | const HTMLElement: HTMLElement = fixture.nativeElement; 27 | const heading = HTMLElement.querySelector('h1')!; 28 | expect(heading.textContent).toEqual('Default'); 29 | }); 30 | 31 | it('check if header is being changed', () => { 32 | const changedTextElement = 'changed'; 33 | component.section = changedTextElement; 34 | fixture.detectChanges(); 35 | const HTMLElement: HTMLElement = fixture.nativeElement; 36 | const heading = HTMLElement.querySelector('h1')!; 37 | expect(heading.textContent).toEqual('changed'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | :root { 4 | --slider-primary: #66bb6a; /* Using the same green as your userday table */ 5 | --slider-track: #e0e0e0; 6 | --slider-thumb: #66bb6a; 7 | } 8 | 9 | html, body { height: 100%; } 10 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 11 | 12 | h1, h2, h3 { 13 | font-weight: 400; 14 | } 15 | 16 | .mat-typography h1.mat-display-1 { 17 | margin: 0; 18 | } 19 | 20 | /* Slider styling */ 21 | .mat-slider-track-background { 22 | background-color: var(--slider-track) !important; 23 | } 24 | 25 | .mat-slider-track-fill { 26 | background-color: var(--slider-primary) !important; 27 | } 28 | 29 | .mat-slider-thumb { 30 | background-color: var(--slider-thumb) !important; 31 | } 32 | 33 | .mat-slider-thumb-label { 34 | background-color: var(--slider-thumb) !important; 35 | } 36 | 37 | .userday table :is(td, th) { 38 | border: 1px solid black; 39 | padding: 0.3em; 40 | } 41 | 42 | .dark-theme .userday table :is(td, th) { 43 | border-color: #e0e0e0; 44 | } 45 | .dark-theme .userday tr:nth-child(even) { 46 | background-color: #365d36; 47 | } 48 | 49 | .userday tr:nth-child(even) { 50 | background-color: #66bb6a; 51 | } 52 | 53 | .userday img { 54 | max-height: 100px; 55 | float: left; 56 | margin-right: 10px; 57 | } 58 | 59 | .usage-dimensions img { 60 | max-width: 40rem; 61 | } -------------------------------------------------------------------------------- /src/app/util/util.ts: -------------------------------------------------------------------------------- 1 | export function perfNow(): string { 2 | return (performance.now() / 1000).toFixed(3); 3 | } 4 | 5 | export function isEmptyObj(obj: any): boolean { 6 | for (let tmp in obj) { 7 | return false; 8 | } 9 | return true; 10 | } 11 | 12 | export function hasData(obj: any): boolean { 13 | for (let tmp in obj) { 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | export function deepCopy(obj: any): any { 20 | return JSON.parse(JSON.stringify(obj)); 21 | } 22 | 23 | export function renameArrayElement(array: any[], oldName: string, newName: string): any[] { 24 | return array.map(item => (item === oldName ? newName : item)); 25 | } 26 | 27 | export function equalArray(a: any[] | undefined | null, b: any[] | undefined | null): boolean { 28 | if (!a && !b) return true; 29 | if (!a || !b) return false; 30 | if (a.length !== b.length) return false; 31 | 32 | return a.every((v, i) => v === b[i]); 33 | } 34 | 35 | export function uniqueCount(array: any[]): number { 36 | const set: Set = new Set(array); 37 | return set.size; 38 | } 39 | 40 | export function dateStr( 41 | date: Date | null | undefined, 42 | locale: string | null | undefined = undefined 43 | ): string { 44 | if (!date) return ''; 45 | if (!locale || locale === 'BROWSER') locale = navigator.language; 46 | 47 | return date.toLocaleDateString(locale, { 48 | year: 'numeric', 49 | month: '2-digit', 50 | day: '2-digit', 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/pages/circular-heatmap/circular-heatmap.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHandler } from '@angular/common/http'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { LoaderService } from 'src/app/service/loader/data-loader.service'; 4 | import { CircularHeatmapComponent } from './circular-heatmap.component'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { MatChip } from '@angular/material/chips'; 7 | import { ModalMessageComponent } from '../../component/modal-message/modal-message.component'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | 10 | describe('CircularHeatmapComponent', () => { 11 | let component: CircularHeatmapComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | declarations: [CircularHeatmapComponent, MatChip], 17 | imports: [RouterTestingModule, MatDialogModule], 18 | providers: [ 19 | LoaderService, 20 | HttpClient, 21 | HttpHandler, 22 | { provide: ModalMessageComponent, useValue: {} }, 23 | ], 24 | }).compileComponents(); 25 | 26 | fixture = TestBed.createComponent(CircularHeatmapComponent); // Create fixture and component here 27 | component = fixture.componentInstance; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('should create', () => { 32 | expect(component).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/assets/YAML/custom/custom-activities.yaml: -------------------------------------------------------------------------------- 1 | # Sample file to show how to customize your own activities, or override existing ones. 2 | # 3 | Build and Deployment: 4 | Build: 5 | New CUSTOM activity: 6 | uuid: 11111111-1111-1111-1111-111111111111 7 | level: 1 8 | description: 9 | This activity is an example of a custom activity that is specific to your organization. 10 | measure: 11 | Write your own measure text here, using markdown if needed. 12 | risk: 13 | See default/activities.yaml for examples of how to write risk statements. 14 | 15 | Defined OUR build process: 16 | uuid: f6f7737f-25a9-4317-8de2-09bf59f29b5b 17 | description: 18 | This is an example with a custom _title_ and _description_ overriding the standard text. 19 | 20 | # Pinning of artifacts: 21 | # uuid: f3c4971e-9f4d-4e59-8ed0-f0bdb6111111 22 | # description: 23 | # This activity has a two different UUIDs for the same name, and will cause an error when loading in the browser. 24 | 25 | SBOM of components: 26 | ignore: true 27 | description: 28 | This will remove this activity from the list of activities in the browser. 29 | 30 | 31 | New CUSTOM dimension: 32 | CUSTOM sub-dimension: 33 | Re-classify SBOM activity to CUSTOM dimension: 34 | uuid: 2858ac12-0179-40d9-9acf-1b839c030473 35 | level: 2 36 | description: 37 | This activity has been moved to a custom dimension. 38 | -------------------------------------------------------------------------------- /src/app/component/progress-slider/progress-slider.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-progress-slider', 5 | templateUrl: './progress-slider.component.html', 6 | styleUrls: ['./progress-slider.component.css'], 7 | }) 8 | export class ProgressSliderComponent implements OnInit { 9 | @Input() DBG_name: string = ''; 10 | @Input() steps: string[] = []; 11 | @Input() state: string = ''; 12 | @Input() originalState: string = ''; 13 | @Output() progressChange = new EventEmitter(); 14 | 15 | originalValue: number = 0; 16 | currentValue: number = 0; 17 | 18 | ngOnInit() { 19 | this.currentValue = this.steps.indexOf(this.state); 20 | this.originalValue = this.steps.indexOf(this.originalState); 21 | 22 | if (this.currentValue === -1) this.currentValue = 0; 23 | if (this.originalValue === -1) this.originalValue = 0; 24 | 25 | if (this.originalValue <= 0) this.originalValue = this.currentValue; 26 | } 27 | 28 | getCurrent() { 29 | return this.steps[this.currentValue]; 30 | } 31 | 32 | hasChanged(): boolean { 33 | return this.originalValue != this.currentValue; 34 | } 35 | 36 | onSlide(event: any) { 37 | console.log('Slider changed:', event); 38 | } 39 | 40 | onStepChange(step: number | null) { 41 | if (step !== null) { 42 | this.currentValue = step as number; 43 | this.progressChange?.emit(this.getCurrent()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/component/sidenav-buttons/sidenav-buttons.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ThemeService } from '../../service/theme.service'; 3 | 4 | @Component({ 5 | selector: 'app-sidenav-buttons', 6 | templateUrl: './sidenav-buttons.component.html', 7 | styleUrls: ['./sidenav-buttons.component.css'], 8 | }) 9 | export class SidenavButtonsComponent implements OnInit { 10 | Options: string[] = [ 11 | 'Overview', 12 | 'Matrix', 13 | 'Mappings', 14 | 'Teams', 15 | 'Settings', 16 | 'Usage', 17 | 'Roadmap', 18 | 'DSOMM User Day', 19 | 'About Us', 20 | ]; 21 | Icons: string[] = [ 22 | 'pie_chart', 23 | 'table_chart', 24 | 'timeline', 25 | 'people', 26 | 'list', 27 | 'description', 28 | 'landscape', 29 | 'school', 30 | 'info', 31 | ]; 32 | Routing: string[] = [ 33 | '/circular-heatmap', 34 | '/matrix', 35 | '/mapping', 36 | '/teams', 37 | '/settings', 38 | '/usage', 39 | '/roadmap', 40 | '/userday', 41 | '/about', 42 | ]; 43 | 44 | isNightMode = false; 45 | 46 | constructor(private themeService: ThemeService) {} 47 | 48 | ngOnInit(): void { 49 | const currentTheme = this.themeService.getTheme(); 50 | this.isNightMode = currentTheme === 'dark'; 51 | } 52 | 53 | toggleTheme(): void { 54 | this.isNightMode = !this.isNightMode; 55 | const newTheme = this.isNightMode ? 'dark' : 'light'; 56 | this.themeService.setTheme(newTheme); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/pages/mapping/mapping.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHandler } from '@angular/common/http'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { MatAutocomplete } from '@angular/material/autocomplete'; 4 | 5 | import { MappingComponent } from './mapping.component'; 6 | import { ModalMessageComponent } from 'src/app/component/modal-message/modal-message.component'; 7 | import { MatDialogModule } from '@angular/material/dialog'; 8 | 9 | describe('MappingComponent', () => { 10 | let component: MappingComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async () => { 14 | /* eslint-disable */ 15 | await TestBed.configureTestingModule({ 16 | declarations: [MappingComponent, MatAutocomplete], 17 | imports: [MatDialogModule], 18 | providers: [HttpClient, 19 | HttpHandler, 20 | { provide: ModalMessageComponent, useValue: {} }, 21 | ], 22 | }).compileComponents(); 23 | /* eslint-enable */ 24 | }); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(MappingComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | 36 | it('check for table generation', () => { 37 | const HTMLElement: HTMLElement = fixture.nativeElement; 38 | const table = HTMLElement.querySelector('table')!; 39 | expect(table).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Subject, takeUntil } from 'rxjs'; 3 | import { ThemeService } from './service/theme.service'; 4 | import { TitleService } from './service/title.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.css'], 10 | }) 11 | export class AppComponent implements OnInit, OnDestroy { 12 | title = ''; 13 | defaultTitle = ''; 14 | subtitle = ''; 15 | menuIsOpen: boolean = true; 16 | 17 | private destroy$ = new Subject(); 18 | 19 | constructor(private themeService: ThemeService, private titleService: TitleService) { 20 | this.themeService.initTheme(); 21 | } 22 | 23 | ngOnInit(): void { 24 | let menuState: string | null = localStorage.getItem('state.menuIsOpen'); 25 | if (menuState === 'false') { 26 | setTimeout(() => { 27 | this.menuIsOpen = false; 28 | }, 600); 29 | } 30 | 31 | // Subscribe to title changes 32 | this.titleService.titleInfo$.pipe(takeUntil(this.destroy$)).subscribe(titleInfo => { 33 | this.title = titleInfo?.dimension || ''; 34 | this.subtitle = titleInfo?.level ? 'Level ' + titleInfo?.level : ''; 35 | }); 36 | } 37 | 38 | ngOnDestroy(): void { 39 | this.destroy$.next(); 40 | this.destroy$.complete(); 41 | } 42 | 43 | toggleMenu(): void { 44 | this.menuIsOpen = !this.menuIsOpen; 45 | localStorage.setItem('state.menuIsOpen', this.menuIsOpen.toString()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/TODO-headlines.md: -------------------------------------------------------------------------------- 1 | ## Things we'd like to improve 2 | ### Upgrade to Angular 21 3 | - We’d like to update to lates Angular, as we are currently running on a dated version 4 | 5 | ### Evidence 6 | - Support handling evidence for each activity. 7 | 8 | ### Review Level 2 activities in the model 9 | - Do a quality assurance on the model of all level 2 activities in the [DSOMM-data](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data) repository. 10 | 11 |   12 | 13 |
14 | 15 | ## Completed 16 | ### DSOMM application v4.0 17 | The release of v4.0 came with some breaking changing of the data model. This was to separate the role of responsibility into the model itself, and the progress information of a team. This way one can more easily update to a new version of the model, without affecting the historic progress of each team. 18 | 19 | The headlines of the release is: 20 | - Breaking changes: Data model 21 | - The `generated.yaml` is split into `model.yaml` and `team-progress.yaml` 22 | - The `model.yaml` has now includes a _"header"_ that contains the version of the DSOMM model it contains 23 | - Customize your own Team names and Groups in the browser 24 | - A Team's progress has changed from a `yes|no` boolean, to customizable steps, from zero to fully complete 25 | - The view of an activity has been improved including the dependencies between activities 26 | - Centralized data loader, all pages uses the same loading mechanism and we only load once at startup 27 | 28 | *Released: 2025-12-16* 29 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/dsomm'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*", 5 | "*.css" 6 | ], 7 | "overrides": [ 8 | { 9 | "files": [ 10 | "*.ts" 11 | ], 12 | "parserOptions": { 13 | "project": [ 14 | "tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates", 21 | "plugin:prettier/recommended" 22 | ], 23 | "rules": { 24 | "@angular-eslint/directive-selector": [ 25 | "error", 26 | { 27 | "type": "attribute", 28 | "prefix": "app", 29 | "style": "camelCase" 30 | } 31 | ], 32 | "@angular-eslint/component-selector": [ 33 | "error", 34 | { 35 | "type": "element", 36 | "prefix": "app", 37 | "style": "kebab-case" 38 | } 39 | ] 40 | } 41 | }, 42 | { 43 | "files": [ 44 | "*.html" 45 | ], 46 | "extends": [ 47 | "plugin:@angular-eslint/template/recommended" 48 | ], 49 | "rules": {} 50 | }, 51 | { 52 | "files": ["*.html"], 53 | "excludedFiles": ["*inline-template-*.component.html"], 54 | "extends": ["plugin:prettier/recommended"], 55 | "rules": { 56 | // NOTE: WE ARE OVERRIDING THE DEFAULT CONFIG TO ALWAYS SET THE PARSER TO ANGULAR (SEE BELOW) 57 | "prettier/prettier": ["error", { "parser": "angular" }] 58 | } 59 | } 60 | 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # File issue: 2 | - UI not responsive to screen size 3 | - Changing team names has no effect 4 | - Default installation (no generated.yaml) does not work 5 | - Filter illogical / not working as expected 6 | 7 | 8 | # ToDo 9 | - App: Alert when generated.yaml is not found 10 | - App: Filter radio buttons: Default, no selections: meaning all selected 11 | - App: Make radio button, and use Ctrl-Click to multiple (hold click on mobile) 12 | - App: Fix bug, that greys out all sectors on startup 13 | - App: Onboarding: Define teams, Setup generated.yaml (is 'generated.yaml' a good name?) 14 | 15 | - Heatmap: TeamGroup filter: No selection means all selected 16 | - Heatmap: TeamGroup filter: Fix removing last filter 17 | - Heatmap: Add Reset data under settings 18 | - Heatmap: Highlight selected sector 19 | 20 | - Heatmap: Alter current bright yellow hover 21 | 22 | - Heatmap modal: Default: Close some tabs 23 | - Heatmap modal: Store opened/closed tabs in local storage 24 | 25 | - Mapping: Add "Sort by:" 26 | - Mapping: Fix: Sort by ISO 2017 is DESC (and 12.2) 27 | 28 | - Matrix: Make radio button, and use Ctrl-Click to multiple (hold click on mobile) 29 | 30 | # Doing 31 | - Heatmap: Fix color calculations, to base on TeamVisible 32 | - Heatmap: Allow non-standard team names and groups 33 | 34 | # Done 35 | - Heatmap: Make heatmap the start page 36 | - Heatmap: Center labels on sectors 37 | - Heatmap: Fix calculations of heatmap dimension 38 | - Heatmap: Toggle filters' visibility 39 | - Heatmap: (Re)move Reset button 40 | - Heatmap: Fix responsive layout 41 | -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/teams-groups-editor.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 20 |
21 |
22 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/userday2025.md: -------------------------------------------------------------------------------- 1 |
2 | SAMM and DSOMM User Day 2025 - Washington DC 3 | 4 | ## Washington DC SAMM & DSOMM User Day 5 | 6 | The OWASP DSOMM team is excited to announce our upcoming User Day, this time teaming up with [OWASP SAMM](https://owasp.org/www-project-samm/), as part of [Global AppSec USA (Washington, DC)](https://owasp.glueup.com/event/owasp-2025-global-appsec-usa-washington-dc-131624/), on November 5th, 2025. 7 | 8 | We’ll be spending the day sharing experiences, exploring real-world use of SAMM and DSOMM, and learning from each other about how to advance software security maturity. Whether you're deep into assessments or just getting started, we’d love to hear your perspective. 9 | 10 | ### Location 11 | Marriott Marquis Washington, DC \ 12 | 901 Massachusetts Avenue NW \ 13 | Washington, District of Columbia \ 14 | United States 15 | 16 | ### Agenda 17 | 18 | | Title | Speaker | 19 | |------------------------------------------------------------------------------------|-------------| 20 | |An overview and comparison of SAMM and DSOMM | Aram Hovsepyan and Timo Pagel | 21 | |An Ordinary Dev Team meets DSOMM | Vegard Bakke | 22 | |Learning from Setbacks: Pitfalls and Lessons in Scaling SAMM at a Fortune 500 Company | Sunny Sharma | 23 | |Soft Challenges, Hard Impact: Launching AppSec with OWASP SAMM | Nariman Aga-Tagiyev | 24 | |Stacking Frameworks | Dag Flachet | 25 | |From Policy to Proof: Automating Testing for Compliance | Spyros Gasteratos | 26 | |SAMM Benchmark Updates | Brian Glas | 27 | |Round table: What's next for SAMM and DSOMM users | All participants | 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/app/component/progress-slider/progress-slider.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatSliderModule } from '@angular/material/slider'; 4 | import { ProgressSliderComponent } from './progress-slider.component'; 5 | 6 | describe('ProgressSliderComponent', () => { 7 | let component: ProgressSliderComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [ProgressSliderComponent], 13 | imports: [FormsModule, MatSliderModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(ProgressSliderComponent); 19 | component = fixture.componentInstance; 20 | component.steps = ['Step 1', 'Step 2', 'Step 3']; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | 28 | it('should initialize with the correct initial step', () => { 29 | component.state = 'Step 2'; 30 | component.ngOnInit(); 31 | expect(component.currentValue).toBe(1); 32 | }); 33 | 34 | it('should emit step changes', () => { 35 | spyOn(component.progressChange, 'emit'); 36 | component.onStepChange(2); 37 | expect(component.progressChange.emit).toHaveBeenCalledWith('Step 3'); 38 | }); 39 | 40 | it('should display the correct step label', () => { 41 | component.currentValue = 1; 42 | fixture.detectChanges(); 43 | const compiled = fixture.nativeElement as HTMLElement; 44 | expect(compiled.querySelector('.step-label')?.textContent).toContain('Step 2'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/service/yaml-loader/yaml-loader.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { YamlService } from './yaml-loader.service'; 3 | import { parse } from 'yamljs'; 4 | 5 | describe('YamlLoaderService', () => { 6 | let service: YamlService; 7 | const mockMetaYaml = ` 8 | name: Me 9 | references: 10 | teams: "CORRECT" 11 | teams: 12 | $ref: "#/references/teams" 13 | external_ref: 14 | $ref: "external.yaml#/external/ref2" 15 | `; 16 | 17 | const mockReferencedYaml = ` 18 | external: 19 | ref1: "REF 1" 20 | ref2: "REF 2" 21 | `; 22 | 23 | beforeEach(() => { 24 | TestBed.configureTestingModule({ 25 | providers: [YamlService], 26 | }); 27 | service = TestBed.inject(YamlService); 28 | (service as any)._refs['external.yaml'] = parse(mockReferencedYaml); 29 | }); 30 | 31 | it('should be created', () => { 32 | expect(service).toBeTruthy(); 33 | }); 34 | 35 | it('should substitute $ref in meta', async () => { 36 | let yaml = parse(mockMetaYaml); 37 | 38 | await service.substituteYamlRefs(yaml, '.'); 39 | 40 | expect(yaml.name).toBe('Me'); 41 | expect(yaml.teams).toBe('CORRECT'); 42 | expect(yaml.external_ref).toBe('REF 2'); 43 | }); 44 | 45 | it('should throw error when $ref not found', async () => { 46 | let yaml = parse(mockMetaYaml); 47 | yaml['not-found'] = { $ref: '#/references/not-there' }; 48 | console.log('PRE:\n' + JSON.stringify(yaml)); 49 | try { 50 | await service.substituteYamlRefs(yaml, '.'); 51 | expect('substituteYamlRefs()').toThrowError('Should not get here'); 52 | } catch (error) { 53 | expect(String(error)).toEqual("Error: Cannot find 'references/not-there' in yaml file"); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AboutUsComponent } from './pages/about-us/about-us.component'; 4 | import { UserdayComponent } from './pages/userday/userday.component'; 5 | import { CircularHeatmapComponent } from './pages/circular-heatmap/circular-heatmap.component'; 6 | import { MappingComponent } from './pages/mapping/mapping.component'; 7 | import { MatrixComponent } from './pages/matrix/matrix.component'; 8 | import { ActivityDescriptionPageComponent } from './pages/activity-description/activity-description-page.component'; 9 | import { UsageComponent } from './pages/usage/usage.component'; 10 | import { TeamsComponent } from './pages/teams/teams.component'; 11 | import { RoadmapComponent } from './pages/roadmap/roadmap.component'; 12 | import { SettingsComponent } from './pages/settings/settings.component'; 13 | 14 | const routes: Routes = [ 15 | { path: '', component: CircularHeatmapComponent }, 16 | { path: 'circular-heatmap', component: CircularHeatmapComponent }, 17 | { path: 'matrix', component: MatrixComponent }, 18 | { path: 'activity-description', component: ActivityDescriptionPageComponent }, 19 | { path: 'mapping', component: MappingComponent }, 20 | { path: 'usage', redirectTo: 'usage/' }, 21 | { path: 'usage/:page', component: UsageComponent }, 22 | { path: 'teams', component: TeamsComponent }, 23 | { path: 'about', component: AboutUsComponent }, 24 | { path: 'userday', component: UserdayComponent }, 25 | { path: 'roadmap', component: RoadmapComponent }, 26 | { path: 'settings', component: SettingsComponent }, 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forRoot(routes)], 31 | exports: [RouterModule], 32 | }) 33 | export class AppRoutingModule {} 34 | -------------------------------------------------------------------------------- /src/assets/YAML/meta.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | browserSettings: 3 | changeTeamNames: true 4 | changeProgressionDefinition: true 5 | 6 | 7 | teamProgressFile: 'team-progress.yaml' 8 | progressDefinition: 9 | Not implemented: 10 | score: 0% 11 | definition: Activity not started and not planned 12 | Started: 13 | score: 20% 14 | definition: Activity has been started, or completed for a few applications 15 | Partly implemented: 16 | score: 50% 17 | definition: Activity has been implemented for at least half of the applications 18 | Fully implemented: 19 | score: 100% 20 | definition: Fully implemented for (almost) all applications 21 | 22 | 23 | teams: 24 | $ref: 'default/teams.yaml#/teams' 25 | teamGroups: 26 | $ref: 'default/teams.yaml#/teamGroups' 27 | 28 | activityFiles: 29 | # - generated/generated.yaml # Old structure - No longer used 30 | - default/model.yaml 31 | # - custom/custom-activities.yaml # For customizing your own activities 32 | 33 | 34 | # 35 | # Various strings and messages 36 | # 37 | lang: en 38 | strings: 39 | en: 40 | allTeamsGroupName: 'All teams' 41 | maturityLevels: 42 | [ 43 | 'Level 1: Basic understanding of security practices', 44 | 'Level 2: Adoption of basic security practices', 45 | 'Level 3: High adoption of security practices', 46 | 'Level 4: Very high adoption of security practices', 47 | 'Level 5: Advanced deployment of security practices at scale', 48 | ] 49 | labels: ['Very Low', 'Low', 'Medium', 'High', 'Very High'] 50 | knowledgeLabels: 51 | [ 52 | 'Very Low (one discipline)', 53 | 'Low (one discipline)', 54 | 'Medium (two disciplines)', 55 | 'High (two disciplines)', 56 | 'Very High (three or more disciplines)', 57 | ] 58 | -------------------------------------------------------------------------------- /src/app/material/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatSidenavModule } from '@angular/material/sidenav'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatListModule } from '@angular/material/list'; 6 | import { MatDividerModule } from '@angular/material/divider'; 7 | import { MatTableModule } from '@angular/material/table'; 8 | import { MatChipsModule } from '@angular/material/chips'; 9 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 10 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 11 | import { MatInputModule } from '@angular/material/input'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatFormFieldModule } from '@angular/material/form-field'; 14 | import { MatExpansionModule } from '@angular/material/expansion'; 15 | import { MatCardModule } from '@angular/material/card'; 16 | import { MatCheckboxModule } from '@angular/material/checkbox'; 17 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 18 | import { MatSliderModule } from '@angular/material/slider'; 19 | import { MatSortModule } from '@angular/material/sort'; 20 | import { CommonModule } from '@angular/common'; 21 | 22 | const MaterialComponents = [ 23 | CommonModule, 24 | MatSidenavModule, 25 | MatButtonModule, 26 | MatIconModule, 27 | MatListModule, 28 | MatDividerModule, 29 | MatTableModule, 30 | MatChipsModule, 31 | MatProgressSpinnerModule, 32 | MatAutocompleteModule, 33 | MatInputModule, 34 | MatSelectModule, 35 | MatFormFieldModule, 36 | MatExpansionModule, 37 | MatCardModule, 38 | MatCheckboxModule, 39 | MatButtonToggleModule, 40 | MatSliderModule, 41 | MatSortModule, 42 | ]; 43 | 44 | @NgModule({ 45 | imports: [MaterialComponents], 46 | exports: [MaterialComponents], 47 | }) 48 | export class MaterialModule {} 49 | -------------------------------------------------------------------------------- /src/app/component/sidenav-buttons/sidenav-buttons.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { SidenavButtonsComponent } from './sidenav-buttons.component'; 5 | 6 | describe('SidenavButtonsComponent', () => { 7 | let component: SidenavButtonsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [SidenavButtonsComponent], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SidenavButtonsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | 26 | it('check for navigation list generation', () => { 27 | const HTMLElement: HTMLElement = fixture.nativeElement; 28 | const NavigationList = HTMLElement.querySelector('mat-nav-list')!; 29 | //console.log(NavigationList); 30 | expect(NavigationList).toBeTruthy(); 31 | }); 32 | 33 | it('check for navigation names being shown in the same order as options array', () => { 34 | const HTMLElement: HTMLElement = fixture.nativeElement; 35 | const NavigationList = HTMLElement.querySelectorAll('a > h3')!; 36 | let NavigationNamesBeingShown = []; 37 | for (var x = 0; x < NavigationList.length; x += 1) { 38 | NavigationNamesBeingShown.push(NavigationList[x].textContent); 39 | } 40 | //console.log({ ...NavigationNamesBeingShown }); 41 | //console.log(component.Options); 42 | expect(NavigationNamesBeingShown).toEqual(component.Options); 43 | }); 44 | 45 | it('ensure all navigation options has its own icon and route', () => { 46 | expect(component.Icons.length).toEqual(component.Options.length); 47 | expect(component.Routing.length).toEqual(component.Options.length); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.10.0](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.9.0...v3.10.0) (2023-11-10) 2 | 3 | 4 | ### Features 5 | 6 | * decouple yaml-data and application ([45611e8](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/45611e8ee58ec7e9ed8ecf5bb1c54b5bfcb8e885)) 7 | * enhance signing description ([231a5e9](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/231a5e97b66a49b95bbc14147ea43d5ce9646788)) 8 | 9 | # [3.9.0](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.8.0...v3.9.0) (2023-11-09) 10 | 11 | 12 | ### Features 13 | 14 | * enhance signing description ([4546078](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/454607882a909ef5d7c3e5f2f14bcc0a6a43076e)) 15 | 16 | ## [3.5.2](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.5.1...v3.5.2) (2023-11-07) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * YAML Structure description ([33e50f0](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/33e50f0fb168c5c91b4fedb5a2a7d5e8a4ac0a80)) 22 | 23 | ## [3.5.1](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.5.0...v3.5.1) (2023-11-07) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * YAML ([889422b](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/889422b791cf141838e2ec637406a14d8849ff6a)) 29 | 30 | # [3.5.0](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.4.0...v3.5.0) (2023-11-07) 31 | 32 | 33 | ### Features 34 | 35 | * add WAF ([a98947d](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/a98947da41691e23af255cad8778208db09ccc53)) 36 | 37 | # [3.4.0](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/compare/v3.3.0...v3.4.0) (2023-11-07) 38 | 39 | 40 | ### Features 41 | 42 | * Activity Contexualized Encoding ([f81d3cf](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/commit/f81d3cfedd013b579fac73e1b62bb57dfbc5a7a3)) 43 | -------------------------------------------------------------------------------- /src/app/service/settings/github.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { get } from 'http'; 5 | 6 | export interface GithubReleaseInfo { 7 | tagName: string; 8 | publishedAt?: Date; 9 | downloadUrl?: string; 10 | changelogUrl?: string; 11 | } 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class GithubService { 17 | /* eslint-disable */ 18 | private readonly DSOMM_MODEL_URL = 'https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/'; 19 | private readonly CHANGELOG_URL = this.DSOMM_MODEL_URL + '/blob/main/CHANGELOG.md'; 20 | private readonly LATEST_RELEASE_URL = this.DSOMM_MODEL_URL.replace('//github.com', '//api.github.com/repos') + 'releases/latest'; 21 | private readonly DOWNLOAD_URL_TEMPLATE = this.DSOMM_MODEL_URL.replace('//github.com', '//raw.githubusercontent.com') + '{tag}/generated/model.yaml'; 22 | /* eslint-enable */ 23 | 24 | constructor(private http: HttpClient) {} 25 | 26 | public getDsommModelUrl(): string { 27 | return this.DSOMM_MODEL_URL; 28 | } 29 | 30 | async getLatestRelease(): Promise { 31 | const obs = this.http.get(this.LATEST_RELEASE_URL); 32 | const remote: any = await firstValueFrom(obs); 33 | let releaseInfo: GithubReleaseInfo = { 34 | tagName: remote?.tag_name || '', 35 | publishedAt: remote.published_at ? new Date(remote.published_at) : undefined, 36 | downloadUrl: this.getDownloadUrl(remote?.tag_name), 37 | changelogUrl: this.getChangelogUrl(), 38 | }; 39 | return releaseInfo; 40 | } 41 | 42 | getDownloadUrl(tag: string): string { 43 | if (!tag) return ''; 44 | // Ensure tag is encoded safely 45 | const safeTag = encodeURIComponent(tag); 46 | return this.DOWNLOAD_URL_TEMPLATE.replace('{tag}', safeTag); 47 | } 48 | 49 | getChangelogUrl(): string { 50 | return this.CHANGELOG_URL; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/component/modal-message/modal-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { DialogInfo, ModalMessageComponent } from './modal-message.component'; 3 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | 7 | describe('ModalMessageComponent', () => { 8 | let component: ModalMessageComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [NoopAnimationsModule, MatDialogModule], 14 | declarations: [ModalMessageComponent], 15 | providers: [ 16 | { provide: MatDialogRef, useValue: {} }, 17 | { provide: MAT_DIALOG_DATA, useValue: {} }, 18 | ], 19 | }).compileComponents(); 20 | }); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(ModalMessageComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | 32 | it('should render markdown correctly in the dialog', () => { 33 | const dialogInfo: DialogInfo = new DialogInfo('A *test* _markdown_.'); 34 | const dialogRef: MatDialogRef = component.openDialog(dialogInfo); 35 | 36 | expect(dialogRef.componentInstance.data.message).toContain('test'); 37 | expect(dialogRef.componentInstance.data.message).toContain('markdown'); 38 | }); 39 | 40 | it('should render markdown correctly in the dialog', () => { 41 | const dialogInfo: DialogInfo = new DialogInfo('A **test** markdown.'); 42 | const dialogRef: MatDialogRef = component.openDialog(dialogInfo); 43 | 44 | // Check if markdown rendering is applied 45 | expect(dialogRef.componentInstance.data.message).toContain('test'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/service/settings/settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { SettingsService } from './settings.service'; 3 | 4 | describe('SettingsService', () => { 5 | let service: SettingsService; 6 | let localStorageSpy: any; 7 | 8 | beforeEach(() => { 9 | // Clear all mocks and create fresh instance for each test 10 | localStorage.clear(); 11 | TestBed.configureTestingModule({}); 12 | service = TestBed.inject(SettingsService); 13 | localStorageSpy = spyOn(localStorage, 'setItem').and.callThrough(); 14 | }); 15 | 16 | afterEach(() => { 17 | localStorage.clear(); 18 | }); 19 | 20 | it('should be created', () => { 21 | expect(service).toBeTruthy(); 22 | }); 23 | 24 | describe('General Settings Operations', () => { 25 | it('should handle empty string settings', () => { 26 | service.saveSettings('test.key', ''); 27 | expect(localStorage.getItem('test.key')).toBeNull(); 28 | }); 29 | 30 | it('should handle empty array settings', () => { 31 | service.saveSettings('test.key', []); 32 | expect(localStorage.getItem('test.key')).toBeNull(); 33 | }); 34 | 35 | it('should handle empty object settings', () => { 36 | service.saveSettings('test.key', {}); 37 | expect(localStorage.getItem('test.key')).toBeNull(); 38 | }); 39 | 40 | it('should properly store and retrieve number settings', () => { 41 | localStorage.setItem('test.key', '42'); 42 | expect(service.getSettingsNumber('test.key')).toBe(42); 43 | }); 44 | 45 | it('should return null for non-existent number settings', () => { 46 | expect(service.getSettingsNumber('nonexistent.key')).toBeNull(); 47 | }); 48 | 49 | it('should handle complex object settings', () => { 50 | const complexObj = { 51 | key1: 'value1', 52 | key2: 42, 53 | nested: { prop: true }, 54 | }; 55 | service.saveSettings('test.complex', complexObj); 56 | expect(service.getSettings('test.complex')).toEqual(complexObj); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /Issue.md: -------------------------------------------------------------------------------- 1 | # Changing team names has no effect 2 | 3 | ## Expected outcome 4 | * Updating the teams names and groups in `meta.yaml` should be visible in the browser after a refresh 5 | 6 | ## Actual outcome 7 | 8 | ## Steps to reproduce 9 | 1) Clone the repo \ 10 | `git clone https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel.git` 11 | 12 | 2) Install dependencies \ 13 | `cd DevSecOps-MaturityModel` \ 14 | `npm install` 15 | 16 | 3) Download the default teams setup \ 17 | `curl https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/main/src/assets/YAML/generated/generated.yaml -o src/assets/YAML/generated/generated.yaml` 18 | 19 | 4) Start the web server \ 20 | `ng server` (or maybe `npx ng server`) 21 | 22 | 5) Open *incognito mode* os a web browser and visit \ 23 | http://localhost:4200/circular-heatmap 24 | 25 | 6) Verify that the teams are 'Default', 'B' and 'C' 26 | 27 | 7) Fill in data for some of the teams 28 | - Click on a sector in the circle (e.g. *Build* Level 1) 29 | - Expand *Defined build process* 30 | - Tick all three teams 31 | - Click on another sector in the circle (e.g. *Deployment* Level 1) 32 | - Expand *Defined deployment process* 33 | - Tick 'Default' and 'B' only 34 | 35 | 8) Download `generated.yaml` 36 | 37 | ### Change names of teams 38 | 9) Open `src\assets\YAML\meta.yaml` 39 | 10) Edit team names in 'meta' 40 | - Rename `Default` to `A` in `teams` and `teamGroups` 41 | - Add `D` on `teams` and `teamGroups.GroupA` 42 | - Add `GroupD: ['C', 'D']` under `teamGroups` 43 | 11) Update team names in 'generated' 44 | - Rename all `Default:` to `A:` in the downloaded `generated.yaml` 45 | - Add `D: true` on line 130 for *Defined build process* 46 | 47 | 12) Replace `src/assets/YAML/generated/generated.yaml` with the newly modified version 48 | 49 | ### Verify data in your browser 50 | 13) Refresh your browser 51 | * The team filters are showing the new names 52 | * But expanding the activity cards only show `B` and `C` 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/app/pages/teams/teams.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule, HttpHandler } from '@angular/common/http'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { MatChip } from '@angular/material/chips'; 6 | 7 | import { TeamsComponent } from './teams.component'; 8 | import { ModalMessageComponent } from 'src/app/component/modal-message/modal-message.component'; 9 | import { LoaderService } from 'src/app/service/loader/data-loader.service'; 10 | import { MockLoaderService } from 'src/app/service/loader/mock-data-loader.service'; 11 | import { isEmptyObj, perfNow } from 'src/app/util/util'; 12 | 13 | let mockLoaderService: MockLoaderService; 14 | 15 | describe('TeamsComponent', () => { 16 | let component: TeamsComponent; 17 | let fixture: ComponentFixture; 18 | mockLoaderService = new MockLoaderService({}); 19 | 20 | beforeEach(async () => { 21 | /* eslint-disable */ 22 | // await mockLoaderService.load(); 23 | await TestBed.configureTestingModule({ 24 | providers: [ 25 | HttpClientTestingModule, 26 | { provide: ModalMessageComponent, useValue: {} }, 27 | { provide: LoaderService, useValue: mockLoaderService }, 28 | ], 29 | imports: [RouterTestingModule, HttpClientModule], 30 | declarations: [TeamsComponent, MatChip], 31 | }).compileComponents(); 32 | /* eslint-enable */ 33 | }); 34 | 35 | beforeEach(async () => { 36 | fixture = TestBed.createComponent(TeamsComponent); 37 | component = fixture.componentInstance; 38 | 39 | fixture.detectChanges(); 40 | await fixture.whenStable(); 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should create', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | 48 | it('check loading teams', () => { 49 | expect(component.teams).toContain('Team A'); 50 | expect(component.teams).toContain('Team B'); 51 | expect(component.teamGroups?.['AB']).toBeDefined(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/app/service/settings/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class SettingsService { 7 | private readonly KEY_DATEFORMAT = 'settings.dateformat'; 8 | private readonly KEY_MAX_LEVEL = 'settings.maxlevel'; 9 | 10 | private dateformat: string | null = null; 11 | private maxLevel: number | null = null; 12 | 13 | getDateFormat(): string | null { 14 | if (this.dateformat == null) { 15 | this.dateformat = this.getSettings(this.KEY_DATEFORMAT); 16 | } 17 | return this.dateformat; 18 | } 19 | 20 | setDateFormat(format: string | null): void { 21 | this.dateformat = format; 22 | this.saveSettings(this.KEY_DATEFORMAT, format); 23 | } 24 | 25 | getMaxLevel(): number | null { 26 | if (this.maxLevel == null) { 27 | this.maxLevel = this.getSettingsNumber(this.KEY_MAX_LEVEL); 28 | } 29 | return this.maxLevel; 30 | } 31 | 32 | setMaxLevel(maxLevel: number | null): void { 33 | this.maxLevel = maxLevel; 34 | this.saveSettings(this.KEY_MAX_LEVEL, maxLevel); 35 | } 36 | 37 | getSettingsNumber(key: string): number | null { 38 | let setting: string | null = localStorage.getItem(key); 39 | if (setting == null) { 40 | return null; 41 | } 42 | return Number(setting); 43 | } 44 | 45 | getSettings(key: string): any { 46 | const settings = localStorage.getItem(key); 47 | if (settings == null) return null; 48 | else return settings ? JSON.parse(settings) : {}; 49 | } 50 | 51 | saveSettings(key: string, settings: any): void { 52 | if (settings == null) { 53 | localStorage.removeItem(key); 54 | } else if (typeof settings == 'string' && settings.trim().length == 0) { 55 | localStorage.removeItem(key); 56 | } else if (settings instanceof Array && settings.length == 0) { 57 | localStorage.removeItem(key); 58 | } else if (settings instanceof Object && Object.keys(settings).length == 0) { 59 | localStorage.removeItem(key); 60 | } else { 61 | localStorage.setItem(key, JSON.stringify(settings)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/service/sector-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Activity } from 'src/app/model/activity-store'; 3 | import { Progress, ProgressDefinitions, TeamNames, Uuid } from 'src/app/model/types'; 4 | import { ProgressStore } from 'src/app/model/progress-store'; 5 | 6 | /** 7 | * The SectorViewController class is responsible for providing activity progress for all 8 | * activities sharing the same dimension and level, which we call a "sector". It takes 9 | * into account the current teams that are visible in the UI for the calculation. 10 | */ 11 | 12 | @Injectable({ providedIn: 'root' }) 13 | export class SectorService { 14 | private progressStore!: ProgressStore; 15 | private allTeams: TeamNames = []; 16 | private visibleTeams: TeamNames = []; 17 | private allProgress: Progress | null = null; 18 | private progressStates: string[] = []; 19 | private progressValues: ProgressDefinitions | null = null; 20 | 21 | init( 22 | progressStore: ProgressStore, 23 | teamnames: TeamNames, 24 | progress: Progress, 25 | progressStates: ProgressDefinitions 26 | ) { 27 | this.progressStore = progressStore; 28 | this.allTeams = teamnames; 29 | this.allProgress = progress; 30 | this.progressValues = progressStates; 31 | this.progressStates = Object.keys(progressStates).sort( 32 | (a, b) => progressStates[b].score - progressStates[a].score 33 | ); 34 | } 35 | 36 | setVisibleTeams(teams: TeamNames) { 37 | this.visibleTeams = teams; 38 | } 39 | 40 | getProgressStates() { 41 | return this.progressStates.slice().reverse(); 42 | } 43 | 44 | getSectorProgress(activities: Activity[]): number { 45 | if (!activities || activities.length === 0) { 46 | return NaN; 47 | } 48 | const teams = this.visibleTeams.length === 0 ? this.allTeams : this.visibleTeams; 49 | let progress = 0; 50 | for (const activity of activities) { 51 | progress += this.getActivityProgress(activity.uuid, teams); 52 | } 53 | return activities.length ? progress / activities.length : 0; 54 | } 55 | 56 | private getActivityProgress( 57 | uuid: Uuid, 58 | teams: TeamNames, 59 | getBackupValue: boolean = false 60 | ): number { 61 | let progress = 0; 62 | for (const team of teams) { 63 | progress += this.progressStore?.getTeamActivityProgressValue(uuid, team, getBackupValue) || 0; 64 | } 65 | return teams.length ? progress / teams.length : 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/pages/teams/teams.component.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | color: #000000; 3 | text-align: center; 4 | font-weight: 800; 5 | font-size: 2em; 6 | margin: 1em 1em 0.2em; 7 | } 8 | h3 { 9 | color: #000000; 10 | text-align: center; 11 | font-weight: 500; 12 | font-size: 1.2em; 13 | font-style: italic; 14 | margin: 1em 1em; 15 | } 16 | 17 | .team-section { 18 | padding: 0 1rem 1rem; 19 | } 20 | 21 | .team-list { 22 | width: 70%; 23 | overflow-x: auto; 24 | white-space: nowrap; 25 | padding: 20px; 26 | margin: 0 auto; 27 | } 28 | 29 | .team-list ul { 30 | list-style-type: none; 31 | padding: 0; 32 | margin: 0; 33 | display: flex; /* Use flex layout */ 34 | } 35 | 36 | .team-list ul li { 37 | flex: 0 0 150px; /* Set a fixed width */ 38 | height: 100px; 39 | background-color: #66bb6a; 40 | border-radius: 10px; 41 | margin-right: 20px; 42 | display: flex; /* Use flex layout */ 43 | justify-content: center; /* Center horizontally */ 44 | align-items: center; /* Center vertically */ 45 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 46 | font-size: 16px; 47 | font-weight: bold; 48 | } 49 | 50 | .button-container { 51 | display: flex; 52 | flex-direction: row; 53 | justify-content: flex-end; 54 | } 55 | .button-container button { 56 | margin: 10px; 57 | } 58 | 59 | .team-info { 60 | display: flex; 61 | flex-direction: column; 62 | } 63 | 64 | .team-info .subheader { 65 | text-align: center; 66 | font-style: italic; 67 | } 68 | 69 | .info-kpis { 70 | display: flex; 71 | flex-direction: row; 72 | gap: 1rem; 73 | } 74 | 75 | 76 | .info-table { 77 | margin: 20px; 78 | } 79 | 80 | .mat-cell, .mat-header-cell { 81 | padding: 20px 10px; 82 | /* width: 12.5%; */ 83 | /* max-width: 12.5%; */ 84 | word-wrap: break-word; 85 | } 86 | 87 | .mat-header-cell { 88 | font-size: 16px; 89 | font-weight: bold; 90 | } 91 | 92 | td.mat-cell { 93 | padding: 0 10px; 94 | } 95 | 96 | .progress-col { 97 | width: 120px; 98 | min-width: 120px; 99 | max-width: 120px; 100 | text-align: center; 101 | } 102 | 103 | .teams-table { 104 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 105 | } 106 | 107 | .dimension-cell { 108 | display: flex; 109 | align-items: center; 110 | gap: 8px; 111 | } 112 | 113 | .dimension-icon { 114 | font-size: 18px; 115 | width: 18px; 116 | height: 18px; 117 | line-height: 18px; 118 | } -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/selectable-list.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

{{ title }}

5 |
Click Accept when finished
6 |
Click to toggle relationship
7 | 14 | 15 | 18 | 21 | 24 | 25 |
26 | 27 |
    28 |
  • 33 | 34 | 35 | 41 | 42 | 43 | {{ name }} 44 | 45 | 46 | 47 | 50 | 53 | 54 | 55 | check_box 58 | check_box_outline_blank 59 | 60 |
  • 61 |
62 |
63 | -------------------------------------------------------------------------------- /src/app/model/data-store.ts: -------------------------------------------------------------------------------- 1 | import { ActivityStore } from './activity-store'; 2 | import { Progress } from './types'; 3 | import { MetaStore, MetaStrings } from './meta-store'; 4 | import { ProgressStore } from './progress-store'; 5 | 6 | export class DataStore { 7 | public meta: MetaStore | null = null; 8 | public activityStore: ActivityStore | null = null; 9 | public progressStore: ProgressStore | null = null; 10 | 11 | constructor() { 12 | this.meta = new MetaStore(); 13 | this.activityStore = new ActivityStore(); 14 | this.progressStore = new ProgressStore(); 15 | } 16 | 17 | public addActivities(activities: ActivityStore): void { 18 | this.activityStore = activities; 19 | } 20 | public addProgressData(progress: Progress): void { 21 | this.progressStore?.addProgressData(progress); 22 | } 23 | 24 | public getMetaStrings(): MetaStrings { 25 | if (this.meta == null) { 26 | throw Error('Meta yaml has not yet been loaded successfully'); 27 | } 28 | 29 | let lang: string = this.meta.lang || 'en'; 30 | if (!this.meta.strings?.hasOwnProperty(lang)) { 31 | // Requested lang does not exist. Fall back to first available lang 32 | let availableLangs: string[] = Object.keys(this.meta?.strings || {}); 33 | if (availableLangs.length > 0) { 34 | lang = availableLangs[0]; 35 | this.meta.lang = lang; 36 | } 37 | } 38 | return this.meta?.strings?.[lang]; 39 | } 40 | 41 | public getMetaString(name: keyof MetaStrings, index: number = 0): string { 42 | let meta: MetaStrings = this.getMetaStrings(); 43 | if (meta === undefined) { 44 | throw Error('Meta strings not loaded'); 45 | } 46 | if (!meta.hasOwnProperty(name)) { 47 | throw Error(`Meta string '${name}' not found in meta.yaml`); 48 | } 49 | if (Array.isArray(meta[name])) { 50 | if (index < 0 || index >= meta[name].length) { 51 | return index.toString(); 52 | } 53 | return meta[name][index]; 54 | } else if (typeof meta[name] === 'string') { 55 | return meta[name] as string; 56 | } 57 | throw Error(`Meta string '${name}' is not a string or array in meta.yaml`); 58 | } 59 | 60 | public getMaxLevel(): number { 61 | return this.activityStore?.getMaxLevel() || 0; 62 | } 63 | 64 | public getLevelTitles(maxLevel: number | null = null): string[] { 65 | if (maxLevel == null) maxLevel = this.getMaxLevel(); 66 | let titles: string[] = this.getMetaStrings()?.maturityLevels?.slice(0, maxLevel) || []; 67 | if (titles.length < maxLevel) { 68 | for (let i = titles.length + 1; i <= maxLevel; i++) { 69 | titles.push(`Level ${i}`); 70 | } 71 | } 72 | return titles; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 16 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** 26 | * By default, zone.js will patch all possible macroTask and DomEvents 27 | * user can disable parts of macroTask/DomEvents patch by setting following flags 28 | * because those flags need to be set before `zone.js` being loaded, and webpack 29 | * will put import in the top of bundle, so user need to create a separate file 30 | * in this directory (for example: zone-flags.ts), and put the following flags 31 | * into that file, and then add the following code before importing zone.js. 32 | * import './zone-flags'; 33 | * 34 | * The flags allowed in zone-flags.ts are listed here. 35 | * 36 | * The following flags will work for all browsers. 37 | * 38 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 39 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 40 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 41 | * 42 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 43 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 44 | * 45 | * (window as any).__Zone_enable_cross_context_check = true; 46 | * 47 | */ 48 | 49 | /*************************************************************************************************** 50 | * Zone JS is required by default for Angular itself. 51 | */ 52 | import 'zone.js'; // Included with Angular CLI. 53 | 54 | /*************************************************************************************************** 55 | * APPLICATION IMPORTS 56 | */ 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dsomm", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --configuration=production" , 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "heroku-postbuild": "ng build --aot --prod" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^13.0.0", 16 | "@angular/cdk": "^13.0.0", 17 | "@angular/cli": "^13.0.0", 18 | "@angular/common": "^13.0.0", 19 | "@angular/compiler": "^13.0.0", 20 | "@angular/core": "^13.0.0", 21 | "@angular/forms": "^13.0.0", 22 | "@angular/localize": "^13.0.0", 23 | "@angular/material": "^13.0.0", 24 | "@angular/platform-browser": "^13.0.0", 25 | "@angular/platform-browser-dynamic": "^13.0.0", 26 | "@angular/router": "^13.0.0", 27 | "@grafana/faro-web-sdk": "^1.12.2", 28 | "@grafana/faro-web-tracing": "^1.12.2", 29 | "@ngneat/until-destroy": "^10.0.0-beta.0", 30 | "d3": "^7.5.0", 31 | "js-yaml": "^4.1.0", 32 | "markdown-it": "^13.0.1", 33 | "rxjs": "~7.5.0", 34 | "tslib": "^2.8.1", 35 | "xlsx": "^0.18.5", 36 | "yaml": "^2.8.1", 37 | "yamljs": "^0.3.0", 38 | "zone.js": "~0.11.4" 39 | }, 40 | "devDependencies": { 41 | "@angular-devkit/build-angular": "^13.0.0", 42 | "@angular-eslint/builder": "^13.0.0", 43 | "@angular-eslint/eslint-plugin": "^13.0.0", 44 | "@angular-eslint/eslint-plugin-template": "^13.0.0", 45 | "@angular-eslint/schematics": "^13.0.0", 46 | "@angular-eslint/template-parser": "^13.0.0", 47 | "@angular/compiler-cli": "^13.0.0", 48 | "@grafana/faro-webpack-plugin": "^0.1.1", 49 | "@types/d3": "^7.4.0", 50 | "@types/jasmine": "~3.10.0", 51 | "@types/js-yaml": "^4.0.9", 52 | "@types/markdown-it": "^12.2.3", 53 | "@types/node": "^12.11.1", 54 | "@types/yamljs": "^0.2.31", 55 | "@typescript-eslint/eslint-plugin": "5.27.1", 56 | "@typescript-eslint/parser": "5.27.1", 57 | "eslint": "^8.17.0", 58 | "eslint-config-prettier": "^8.5.0", 59 | "eslint-config-standard-with-typescript": "^22.0.0", 60 | "eslint-plugin-import": "^2.26.0", 61 | "eslint-plugin-n": "^15.2.5", 62 | "eslint-plugin-prettier": "^4.2.1", 63 | "eslint-plugin-promise": "^6.0.1", 64 | "jasmine-core": "~4.0.0", 65 | "karma": "~6.3.0", 66 | "karma-chrome-launcher": "~3.1.0", 67 | "karma-coverage": "~2.2.1", 68 | "karma-jasmine": "~4.0.0", 69 | "karma-jasmine-html-reporter": "~1.7.0", 70 | "prettier": "^2.7.1", 71 | "prettier-eslint": "^15.0.1", 72 | "qs": "^6.11.0", 73 | "typescript": "^4.6.4" 74 | }, 75 | "browser": { 76 | "fs": false, 77 | "path": false, 78 | "os": false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/pages/matrix/matrix.component.css: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 100%; 3 | } 4 | 5 | .mat-form-field { 6 | margin: 20px; 7 | margin-bottom: 0; 8 | width: 90%; 9 | } 10 | 11 | .matrix-table { 12 | margin: 20px; 13 | } 14 | 15 | .mat-cell { 16 | padding: 20px; 17 | /* padding-bottom: 0; */ 18 | width: 12.5%; 19 | max-width: 17%; 20 | word-wrap: break-word; 21 | } 22 | 23 | .mat-header-cell { 24 | padding: 10px; 25 | width: 12.5%; 26 | max-width: 17%; 27 | word-wrap: break-word; 28 | font-size: 16px; 29 | font-weight: 500; 30 | } 31 | 32 | .mat-cell ul.activity-list { 33 | list-style-type: disclosure-closed; 34 | } 35 | 36 | .mat-cell-activity { 37 | margin-bottom: 1em; 38 | position: relative; 39 | } 40 | .mat-cell-activity a.activity-title { 41 | text-decoration: unset; 42 | margin: 0; 43 | } 44 | .mat-cell-activity a::after { 45 | content: ""; 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | height: 100%; 51 | } 52 | .mat-cell-activity a.activity-title:link { 53 | color: inherit; 54 | } 55 | 56 | .mat-cell .dim-icon { 57 | display: flex; 58 | align-items: center; 59 | gap: 8px; 60 | 61 | } 62 | .mat-cell .dim-icon mat-icon { 63 | font-size: 45px; 64 | width: 45px; 65 | height: 45px; 66 | } 67 | 68 | .table-small-width { 69 | width: 5%; 70 | max-width: 9%; 71 | } 72 | 73 | .tags-activity { 74 | font-weight: 800; 75 | font-style: italic; 76 | font-size: 12px; 77 | } 78 | /*tag activity - light */ 79 | :host-context(body.light-theme) .tags-activity, 80 | :host-context(body.light-theme) .tags-activity span { 81 | color: rgb(0, 113, 151); 82 | } 83 | 84 | /*tag activity - dark */ 85 | :host-context(body.dark-theme) .tags-activity, 86 | :host-context(body.dark-theme) .tags-activity span { 87 | color: #397af4; 88 | } 89 | 90 | .reset-button { 91 | background-color: #66bb6a; 92 | display: block; 93 | margin: 0 20px; 94 | padding: 7px 12px; 95 | border-radius: 16px; 96 | font: 500 14px / 20px Roboto, 'Helvetica Neue', sans-serif; 97 | } 98 | 99 | .mat-mdc-row .mat-mdc-cell { 100 | border-bottom: 1px solid transparent; 101 | border-top: 1px solid transparent; 102 | cursor: pointer; 103 | } 104 | 105 | .mat-mdc-row:hover .mat-mdc-cell { 106 | border-color: currentColor; 107 | } 108 | 109 | .mat-mdc-header-cell { 110 | font-weight: bold; 111 | font-size: medium; 112 | } 113 | 114 | .matrix-table { 115 | width: 100%; 116 | margin-bottom: 20px; 117 | } 118 | 119 | /* No data message styling */ 120 | .mat-no-data-row { 121 | height: 100px; 122 | } 123 | 124 | .mat-no-data-row td { 125 | text-align: center; 126 | font-size: 16px; 127 | color: rgba(0, 0, 0, 0.54); 128 | padding: 24px; 129 | } 130 | -------------------------------------------------------------------------------- /Development.md: -------------------------------------------------------------------------------- 1 | # DevSecOps Maturity Model (DSOMM) 2 | 3 | ## Introduction 4 | 5 | The DevSecOps Maturity Model (DSOMM) is an open-source framework designed to help organizations evaluate and improve their **DevSecOps** practices. 6 | It provides structured **security maturity levels**, recommendations, and automation insights to enable teams to build **secure, efficient, and scalable software**. 7 | 8 | This guide walks you through **setting up the project locally**, making contributions, and submitting a pull request. 9 | 10 | ## **Project Setup** 11 | 12 | ### Development Server 13 | 14 | The DSOMM is based [Angular](https://angular.dev/) and uses npm for package management. 15 | 16 | - If you have not yet installed npm or the Angular command line tools, install them now. First [NodeJS](https://nodejs.org/en/download) (which provides npm), then Angular: 17 | 18 | ```bash 19 | npm install -g @angular/cli 20 | ``` 21 | 22 | - Clone the DSOMM repo 23 | 24 | ```bash 25 | git clone https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel.git 26 | ``` 27 | 28 | - Change directory to DSOMM 29 | 30 | ```bash 31 | cd DevSecOps-MaturityModel 32 | ``` 33 | 34 | - Install Dependencies 35 | 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | - **NB!** The DSOMM activities are maintained separately. Download the `generated.yaml` and put it in the required folder 41 | 42 | ```bash 43 | curl https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/main/src/assets/YAML/generated/generated.yaml -o src/assets/YAML/generated/generated.yaml 44 | ``` 45 | 46 | - Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 47 | 48 | ## Code Scaffolding 49 | 50 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 51 | 52 | ## Build 53 | 54 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 55 | 56 | ## Running Unit Tests 57 | 58 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 59 | 60 | ## Coding Style Conventions 61 | 62 | - We follow the coding style defined by [ESLint](https://eslint.org/). 63 | - We also use [Prettier](https://prettier.io/docs/en/index.html) as our opinionated code formatter. 64 | - To validate the schemas of the DSOMM yaml files in the IDE, it is recommended to use the VS Code extension [redhat.vscode-yaml](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml). The schemas are stored in `/src/assets/YAML/schemas` 65 | 66 | ### Running Linter 67 | 68 | Run `ng lint` to run the linter from the command line. 69 | If you want to lint only a specific component, use: 70 | 71 | ```bash 72 | ng lint --lint-file-patterns .\src\app\component\xxxxxx\ 73 | -------------------------------------------------------------------------------- /src/app/pages/activity-description/activity-description-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { LoaderService } from '../../service/loader/data-loader.service'; 4 | import { Activity, ActivityStore } from '../../model/activity-store'; 5 | import { DataStore } from 'src/app/model/data-store'; 6 | import { 7 | ModalMessageComponent, 8 | DialogInfo, 9 | } from '../../component/modal-message/modal-message.component'; 10 | 11 | @Component({ 12 | selector: 'app-activity-description-page', 13 | templateUrl: './activity-description-page.component.html', 14 | styleUrls: ['./activity-description-page.component.css'], 15 | }) 16 | export class ActivityDescriptionPageComponent implements OnInit { 17 | currentActivity: Activity | null = null; 18 | isLoading: boolean = true; 19 | 20 | constructor( 21 | private route: ActivatedRoute, 22 | private loader: LoaderService, 23 | private router: Router, 24 | public modal: ModalMessageComponent 25 | ) {} 26 | 27 | ngOnInit() { 28 | this.route.queryParams.subscribe(params => { 29 | const uuid: string = params['uuid']; 30 | const name: string = params['name']; 31 | this.loadActivity(uuid, name); 32 | }); 33 | } 34 | 35 | loadActivity(uuid?: string, name?: string) { 36 | this.isLoading = true; 37 | 38 | this.loader 39 | .load() 40 | .then((dataStore: DataStore) => { 41 | if (!dataStore.activityStore) throw Error('DataStore not loaded'); 42 | 43 | // Ensure uuid and name are strings (fallback to empty string if undefined) 44 | const uuidStr = uuid ?? ''; 45 | const nameStr = name ?? ''; 46 | let activity: Activity = dataStore.activityStore.getActivity(uuidStr, nameStr); 47 | 48 | if (!activity) { 49 | throw new Error('Activity not found'); 50 | } 51 | 52 | this.currentActivity = activity; 53 | this.isLoading = false; 54 | }) 55 | .catch(err => { 56 | console.error('Error loading activity data:', err); 57 | this.isLoading = false; 58 | this.displayMessage( 59 | new DialogInfo(err.message || 'Failed to load activity', 'An error occurred') 60 | ); 61 | }); 62 | } 63 | 64 | displayMessage(dialogInfo: DialogInfo) { 65 | this.modal.openDialog(dialogInfo); 66 | } 67 | 68 | onActivityClicked(activityName: string) { 69 | // Find the activity by name and update the view without reloading the page 70 | const activityStore: ActivityStore = this.loader.datastore?.activityStore as ActivityStore; 71 | const activity: Activity = activityStore?.getActivityByName(activityName) as Activity; 72 | 73 | if (activity) { 74 | // Update the URL query params (SPA style) 75 | this.router.navigate([], { 76 | relativeTo: this.route, 77 | queryParams: { uuid: activity.uuid }, 78 | queryParamsHandling: 'merge', 79 | }); 80 | this.loadActivity(activity.uuid, activity.name); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/TODO.md: -------------------------------------------------------------------------------- 1 | # ToDo 2 | The file contains items that would be good to improve. No particular order, apart from 'Doing' amd 'Next in the line'. 3 | 4 | 5 | ## Doing 6 | 7 | 8 | ## Next in line 9 | 10 | 11 | ## Backlog 12 | ### Upgrade to latest Angular 13 | - Angular: Upgrade from v13 14 | 15 | ### Activity view 16 | - Activity: Show Team Evidence from yaml file 17 | - Activity: Input Teams' evidence 18 | - Activity: Dependency: Do not list activities beyond Max Level 19 | - Activity: Give user message if selected activity is of higher level than visible 20 | 21 | ### Matrix 22 | - Matrix: Add Filter search (like for Mapping) 23 | - Matrix: Remember filters when returning to matrix page 24 | - Matrix: Add a Close/Back button on Activity 25 | - Matrix: Close on pushing ESCAPE 26 | 27 | ### KPI 28 | - Teams: Bug: Reads progress heading from activityStore, not metaStore 29 | - Team KPI: One KPI per ProgressDefinition 30 | - KPI: Add Sub-title 31 | 32 | ### Teams 33 | - Teams: Bug: Editing name, pushes the item last 34 | - Teams: Allow user to re-order teams and groups 35 | - Teams: Allow editing dates for progress stages 36 | 37 | ### Heatmap: 38 | - Heatmap: Known Bug ([#432](https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel/issues/432)): Occasionally, the `getComputedStyle()` returns a \ CSSStyleDeclaration` object with empty styles, which leave the heatmap all black. 39 | - Heatmap: Allow 'change all' if more than four activities 40 | - Heatmap: Highlight (outline) the activity card that is open 41 | - Heatmap, Card: Add Complete-symbol per activity 42 | - Heatmap: Slider: Fix: asterisk marks when modified 43 | - ViewController needs to know about changes vs temp storage 44 | - Heatmap: Bug: Selecting a team group does not always get deselected when flipping teams 45 | - Heatmap: meta-yaml: If progress definition is missing, default to 0% + 100% 46 | - Heatmap: Outer rim: Increase subdimension to be two lines (and increase size) 47 | - Heatmap: Outer rim: Make hover display Dimension (over subdimension) 48 | - Heatmap: Search: A bit like 'Filter' but needs to highlight each sector and activity card 49 | - Heatmap: Filter: Bug: SPACE key does not trigger 50 | - Export to Excel. Move from Mapping, to just progress data 51 | 52 | ### Settings 53 | - Settings: Terms: Allow custom names for: 'Team' and 'Group' (e.g. to 'App' and 'Portfolio') 54 | 55 | ### Misc 56 | - Move all getMetaString into MetaStore() 57 | - Add fallbacks for getMetaString in MetaStore() 58 | - Move META_FILE constant from data service to main app 59 | - Loader: Check if loader can be optimized by load in yaml in parallel 60 | - Matrix: Go through tags: remove, add and rename 61 | 62 | 63 | ## Done 64 | ### DSOMM v4.0.0 65 | - Breaking changes: Data model 66 | - The `generated.yaml` is split into `model.yaml` and `team-progress.yaml` 67 | - The `model.yaml` has now includes a _"header"_ that contains the version of the DSOMM model it contains 68 | - Customize your own Team names and Groups in the browser 69 | - A Team's progress has changed from a `yes|no` boolean, to customizable steps, from zero to fully complete 70 | - The view of an activity has been improved including the dependencies between activities 71 | - Centralized data loader, all pages uses the same loading mechanism and we only load once at startup 72 | 73 | -------------------------------------------------------------------------------- /src/app/component/teams-groups-editor/selectable-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; 2 | import { perfNow } from 'src/app/util/util'; 3 | 4 | @Component({ 5 | selector: 'app-selectable-list', 6 | templateUrl: './selectable-list.component.html', 7 | styleUrls: ['./selectable-list.component.css'], 8 | }) 9 | export class SelectableListComponent { 10 | @Input() title: string = ''; 11 | @Input() items: string[] = []; 12 | @Input() selectedItem: string | null = null; 13 | @Input() highlightedItems: string[] = []; 14 | @Input() canEdit = true; 15 | @Input() editMode = false; 16 | @Input() addLabel = 'Add'; 17 | @Input() typeLabel = ''; 18 | @Input() relationshipEditMode = false; 19 | @Output() itemSelected = new EventEmitter(); 20 | @Output() addItem = new EventEmitter(); 21 | @Output() cancel = new EventEmitter(); 22 | @Output() save = new EventEmitter(); 23 | @Output() renameItem = new EventEmitter<{ oldName: string; newName: string }>(); 24 | @Output() deleteItem = new EventEmitter(); 25 | @Output() relationshipToggle = new EventEmitter(); 26 | @Output() editModeChange = new EventEmitter(); 27 | 28 | @ViewChild('editInput') editInputRef: ElementRef | undefined; 29 | 30 | editingName: string = ''; 31 | editingOrgName: string = ''; 32 | 33 | onItemClicked(name: string) { 34 | console.log(`Item clicked: ${name}`); 35 | if (!this.relationshipEditMode) { 36 | this.itemSelected.emit(name); 37 | } else { 38 | this.relationshipToggle.emit(name); 39 | } 40 | } 41 | 42 | toggleEditMode() { 43 | this.editMode = !this.editMode; 44 | if (this.editMode) { 45 | if (!this.selectedItem && this.items.length > 0) { 46 | this.onItemClicked(this.items[0]); 47 | } 48 | } 49 | this.editModeChange.emit(this.editMode); 50 | } 51 | 52 | startEditItem(name: string) { 53 | this.editingName = name; 54 | this.editingOrgName = name; 55 | this.itemSelected.emit(name); // Select the item when editing starts 56 | setTimeout(() => { 57 | if (this.editInputRef) { 58 | this.editInputRef.nativeElement.focus(); 59 | this.editInputRef.nativeElement.select(); 60 | } 61 | }); 62 | } 63 | 64 | cancelEditItem(oldName: string) { 65 | console.log(`${perfNow()}: Cancel editing: ${oldName}`); 66 | this.editingName = ''; 67 | this.editingOrgName = ''; 68 | } 69 | 70 | saveEditedItem(oldName: string) { 71 | let newName: string = this.editingName?.trim() || oldName; 72 | console.log(`${perfNow()}: Save Item: Setting new name: ${newName}`); 73 | if (this.editingName?.trim() && this.editingName !== oldName) { 74 | this.renameItem.emit({ oldName, newName }); 75 | } 76 | this.editingName = ''; 77 | this.editingOrgName = ''; 78 | } 79 | 80 | deleteListItem(name: string) { 81 | console.log(`${perfNow()}: Delete Item: ${name}`); 82 | let index: number = this.items.indexOf(name); 83 | this.deleteItem.emit(name); 84 | 85 | // Select next item 86 | if (index < this.items.length - 1) { 87 | this.onItemClicked(this.items[index + 1]); 88 | } else if (index > 0) { 89 | this.onItemClicked(this.items[index - 1]); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/app/component/modal-message/modal-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Component, OnInit } from '@angular/core'; 2 | import { 3 | MAT_DIALOG_DATA, 4 | MatDialogRef, 5 | MatDialog, 6 | MatDialogConfig, 7 | } from '@angular/material/dialog'; 8 | import * as md from 'markdown-it'; 9 | import { MarkdownText } from 'src/app/model/markdown-text'; 10 | import { NotificationService } from 'src/app/service/notification.service'; 11 | 12 | @Component({ 13 | selector: 'app-modal-message', 14 | templateUrl: './modal-message.component.html', 15 | styleUrls: ['./modal-message.component.css'], 16 | }) 17 | export class ModalMessageComponent implements OnInit { 18 | data: DialogInfo; 19 | markdown: md = md(); 20 | 21 | DSOMM_host: string = 'https://github.com/devsecopsmaturitymodel'; 22 | DSOMM_url: string = `${this.DSOMM_host}/DevSecOps-MaturityModel-data`; 23 | meassageTemplates: Record = { 24 | generated_yaml: new DialogInfo( 25 | `{message}\n\n` + 26 | `Please download the activity template \`generated.yaml\` ` + 27 | `from [DSOMM-data](${this.DSOMM_url}) on GitHub.\n\n` + 28 | 'The DSOMM activities are maintained and distributed ' + 29 | 'separately from the software.', 30 | 'DSOMM startup problems' 31 | ), 32 | }; 33 | 34 | constructor( 35 | public dialog: MatDialog, 36 | public dialogRef: MatDialogRef, 37 | @Inject(MAT_DIALOG_DATA) data: DialogInfo, 38 | private notificationService: NotificationService 39 | ) { 40 | this.data = data; 41 | } 42 | 43 | // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method 44 | ngOnInit(): void { 45 | this.notificationService.message$.subscribe(({ title, message }) => { 46 | this.openDialog(new DialogInfo(message, title)); 47 | }); 48 | } 49 | 50 | openDialog(dialogInfo: DialogInfo | string): MatDialogRef { 51 | // Remove focus from the button that becomes aria unavailable (avoids ugly console error message) 52 | const buttonElement = document.activeElement as HTMLElement; 53 | if (buttonElement) buttonElement.blur(); 54 | 55 | if (typeof dialogInfo === 'string') { 56 | dialogInfo = new DialogInfo(dialogInfo); 57 | } 58 | if (dialogInfo.template && this.meassageTemplates.hasOwnProperty(dialogInfo.template)) { 59 | let template: DialogInfo = this.meassageTemplates[dialogInfo.template]; 60 | dialogInfo.title = dialogInfo.title || template?.title; 61 | dialogInfo.message = template?.message?.replace('{message}', dialogInfo.message); 62 | } 63 | 64 | const dialogConfig = new MatDialogConfig(); 65 | dialogConfig.id = 'modal-message'; 66 | dialogConfig.disableClose = true; 67 | dialogConfig.data = dialogInfo; 68 | dialogConfig.autoFocus = false; 69 | this.dialogRef = this.dialog.open(ModalMessageComponent, dialogConfig); 70 | return this.dialogRef; 71 | } 72 | 73 | closeDialog(buttonName: string) { 74 | this.dialogRef?.close(buttonName); 75 | } 76 | } 77 | 78 | export class DialogInfo { 79 | title: string = ''; 80 | template: string | null = ''; 81 | message: string = ''; 82 | buttons: string[] = ['OK']; 83 | 84 | constructor(msg: string = '', title: string = '') { 85 | let md: MarkdownText = new MarkdownText(msg); 86 | this.message = md.render(); 87 | this.title = title; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/app/pages/matrix/matrix.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Dimension Filter 5 | 6 | 12 | {{ dim.key }} 13 | 14 | 15 | 16 | 17 | Activity Tag Filter 18 | 19 | 25 | {{ tag.key }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 |
58 | 59 |
60 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 |
Dimension 42 |
43 | {{dataStore?.meta?.getIcon(element.Category)}} 44 | {{ element.Category }} 45 |
46 |
Sub-dimension 53 | {{ element.Dimension }} 54 | {{ level.value }} 61 |
    62 |
  • 63 |
    64 | 71 | 72 | 73 | , [{{ tag }}] 75 | 76 |
    77 |
  • 78 |
79 |
87 | No activities match the selected filters 88 |
95 |
96 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { MaterialModule } from './material/material.module'; 9 | import { CircularHeatmapComponent } from './pages/circular-heatmap/circular-heatmap.component'; 10 | import { MatrixComponent } from './pages/matrix/matrix.component'; 11 | import { MappingComponent } from './pages/mapping/mapping.component'; 12 | import { TeamsComponent } from './pages/teams/teams.component'; 13 | import { UsageComponent } from './pages/usage/usage.component'; 14 | import { UserdayComponent } from './pages/userday/userday.component'; 15 | import { RoadmapComponent } from './pages/roadmap/roadmap.component'; 16 | import { SettingsComponent } from './pages/settings/settings.component'; 17 | import { AboutUsComponent } from './pages/about-us/about-us.component'; 18 | import { LogoComponent } from './component/logo/logo.component'; 19 | import { SidenavButtonsComponent } from './component/sidenav-buttons/sidenav-buttons.component'; 20 | import { TopHeaderComponent } from './component/top-header/top-header.component'; 21 | import { ActivityDescriptionComponent } from './component/activity-description/activity-description.component'; 22 | import { ActivityDescriptionPageComponent } from './pages/activity-description/activity-description-page.component'; 23 | import { LoaderService } from './service/loader/data-loader.service'; 24 | import { HttpClientModule } from '@angular/common/http'; 25 | import { MarkdownViewerComponent } from './component/markdown-viewer/markdown-viewer.component'; 26 | import { DependencyGraphComponent } from './component/dependency-graph/dependency-graph.component'; 27 | import { ToStringValuePipe } from './pipe/to-string-value.pipe'; 28 | import { ModalMessageComponent } from './component/modal-message/modal-message.component'; 29 | import { ProgressSliderComponent } from './component/progress-slider/progress-slider.component'; 30 | import { KpiComponent } from './component/kpi/kpi.component'; 31 | import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 32 | import { TeamsGroupsEditorModule } from './component/teams-groups-editor/teams-groups-editor.module'; 33 | 34 | @NgModule({ 35 | declarations: [ 36 | AppComponent, 37 | LogoComponent, 38 | MatrixComponent, 39 | SidenavButtonsComponent, 40 | TopHeaderComponent, 41 | ActivityDescriptionComponent, 42 | ActivityDescriptionPageComponent, 43 | CircularHeatmapComponent, 44 | MappingComponent, 45 | MarkdownViewerComponent, 46 | UsageComponent, 47 | AboutUsComponent, 48 | DependencyGraphComponent, 49 | TeamsComponent, 50 | ToStringValuePipe, 51 | UserdayComponent, 52 | RoadmapComponent, 53 | ModalMessageComponent, 54 | ProgressSliderComponent, 55 | KpiComponent, 56 | SettingsComponent, 57 | ], 58 | imports: [ 59 | BrowserModule, 60 | AppRoutingModule, 61 | BrowserAnimationsModule, 62 | MaterialModule, 63 | MatDialogModule, 64 | ReactiveFormsModule, 65 | FormsModule, 66 | HttpClientModule, 67 | TeamsGroupsEditorModule, 68 | ], 69 | providers: [ 70 | LoaderService, 71 | ModalMessageComponent, 72 | { provide: MAT_DIALOG_DATA, useValue: {} }, 73 | { provide: MatDialogRef, useValue: { close: (dialogResult: any) => {} } }, 74 | ], 75 | bootstrap: [AppComponent], 76 | }) 77 | export class AppModule {} 78 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 7 * * *" 9 | 10 | permissions: 11 | contents: write 12 | issues: read 13 | #pull-requests: write # to be able to comment on released pull requests 14 | 15 | jobs: 16 | build: 17 | if: github.repository == 'devsecopsmaturitymodel/DevSecOps-MaturityModel' 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | persist-credentials: false # This is important if you have branch protection rules! 23 | - name: Semantic Release 24 | uses: cycjimmy/semantic-release-action@v4 25 | with: 26 | branch: 'main' 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Get Semantic Release Version 30 | id: get-version 31 | run: echo "::set-output name=version::$(grep -oP '\[\d+\.\d+\.\d+\]' CHANGELOG.md | tr -d '[]')" 32 | 33 | - name: show version 34 | run: | 35 | echo "Semantic Release Version: ${{ steps.get-version.outputs.version }}" 36 | 37 | - name: setup qemu for multi-arch build 38 | uses: docker/setup-qemu-action@v2 39 | with: 40 | platforms: amd64,arm64 41 | - name: setup buildx 42 | uses: docker/setup-buildx-action@v2 43 | - name: Log in to Docker Hub 44 | uses: docker/login-action@v2 45 | with: 46 | #registry: registry.hub.docker.com 47 | username: wurstbrot 48 | password: ${{ secrets.HUB_TOKEN }} 49 | - name: create and push dsomm image 50 | uses: docker/build-push-action@v3 51 | with: 52 | push: true 53 | platforms: linux/amd64,linux/arm64 54 | tags: wurstbrot/dsomm:${{ steps.get-version.outputs.version }},wurstbrot/dsomm:latest 55 | build-args: | 56 | COMMIT_HASH=${{ github.sha }} 57 | COMMIT_DATE=${{ github.event.head_commit.timestamp }} 58 | GIT_BRANCH=${{ github.ref_name }} 59 | # Commit all changed files back to the repository 60 | - uses: planetscale/ghcommit-action@v0.1.6 61 | with: 62 | commit_message: "🤖 fmt" 63 | repo: ${{ github.repository }} 64 | branch: ${{ github.head_ref || github.ref_name }} 65 | env: 66 | GITHUB_TOKEN: ${{secrets.ACCESS_TOKEN}} 67 | heroku: 68 | if: github.repository == 'devsecopsmaturitymodel/DevSecOps-MaturityModel' && github.event_name == 'push' && github.ref == 'refs/heads/main' 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: "Check out Git repository" 72 | uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 73 | - name: "Set Heroku app & branch for ${{ github.ref }}" 74 | run: | 75 | echo $GITHUB_REF 76 | if [ "$GITHUB_REF" == "refs/heads/main" ]; then 77 | echo "HEROKU_APP=" >> $GITHUB_ENV 78 | echo "HEROKU_BRANCH=main" >> $GITHUB_ENV 79 | fi 80 | echo "HEROKU_BRANCH=main" >> $GITHUB_ENV 81 | - name: Install Heroku CLI 82 | run: | 83 | curl https://cli-assets.heroku.com/install.sh | sh 84 | - name: "Deploy ${{ github.ref }} to Heroku" 85 | uses: akhileshns/heroku-deploy@v3.13.15 86 | with: 87 | heroku_api_key: ${{ secrets.HEROKU_API_KEY }} 88 | heroku_app_name: "dsomm" 89 | heroku_email: timo.pagel@owasp.org 90 | branch: ${{ env.HEROKU_BRANCH }} 91 | usedocker: true 92 | docker_build_args: | 93 | COMMIT_HASH 94 | COMMIT_DATE 95 | GIT_BRANCH 96 | env: 97 | COMMIT_HASH: ${{ github.sha }} 98 | COMMIT_DATE: ${{ github.event.head_commit.timestamp }} 99 | GIT_BRANCH: ${{ github.ref_name }} 100 | -------------------------------------------------------------------------------- /src/assets/Markdown Files/USAGE.md: -------------------------------------------------------------------------------- 1 | # DSOMM - DevSecOps Maturity Model 2 | 3 | ## What is DSOMM? 4 | DSOMM is a framework that helps organizations to assess, improve and prioritize security activities in their software development cycle. 5 | 6 | DSOMM is a project of the OWASP Foundation. 7 | 8 | ## DSOMM vs OWASP SAMM 9 | [DSOMM](https://dsomm.owasp.org/) and [OWASP SAMM](https://owaspsamm.org/) are both frameworks that share a common goal of improving security. 10 | 11 | **OWASP SAMM** is more focused on the overall maturity of an organization's software assurance and security practices, with a broader scope that includes governance, compliance, risk management, and secure software development. 12 | 13 | SAMM is written by security specialists for security specialists, focusing on security processes across the whole organizations. 14 | 15 | **DSOMM** focuses on activities that integrate security directly into the DevOps workflows. DSOMM takes a more technical approach, going lower in the technology stack it provides a roadmap on how to systematically improve the security in the software development. 16 | 17 | DSOMM is written for technical teams focused on implementing secure software. 18 | 19 | DSOMM has currently has a OWASP Lab status, while SAMM has a Flagship status. 20 | 21 | # How to use this DSOMM site 22 | The DSOMM application is a frontend only application, storing all progress in your local storage in your browser. If you delete your local storage, your progress will be gone, and you cannot share your saved progress with anyone else. 23 | 24 | To do that, you need to install your own local DSOMM application. 25 | 26 | You can export the progress of the different activities as a `generated.yaml` file, which you may import into your own site. 27 | 28 | 29 | ## How to setup your own DSOMM 30 | The DSOMM application can be run as a Docker image, an Amazon EC2 instance, or as a standalone Angular application using NodeJS. Please see [README.md](./usage/README) for further instructions. 31 | 32 | The DSOMM application is currently still a lightweight frontend only application, without a backend to store changes of progress. Any changes are stored in the browser. However, as above, you can export the `generated.yaml` and update your own site with this. 33 | 34 | 35 | # The DSOMM framework 36 | The DSOMM framework has a number of _activities_ grouped by _dimensions_ and _maturity levels_. E.g. the _Centralized system logging_ is a maturity level 1 activity in the _Logging_ dimension, while _Correlation of security events_ is considered level 5. 37 | 38 | 39 | 40 | ## Before you start 41 | To prepare you for there are some activities that we recommend you do before you start using DSOMM. Getting the stakeholders onboard will ease your path. 42 | 43 | See [Maturity level 0](./usage/maturity-level-0) to learn about the important first steps. 44 | 45 | 46 | ## Dimensions 47 | The DSOMM framework categorizes its activities into dimensions, each representing a key area of the software development lifecycle where security can be integrated and matured. 48 | 49 | Dimensions Overview: 50 | - **Build and Deployment**: Focuses on security practices in the CI/CD pipeline and deployment processes 51 | - **Culture and Organization**: Addresses organizational culture, education, and processes that support security initiatives. 52 | - **Implementation**: Covers secure coding and infrastructure hardening practices. 53 | - **Information Gathering**: Involves gathering data for threat analysis, risk assessment, and metrics collection. 54 | - **Test and Verification**: Focuses on testing practices to validate security measures and ensure continuous improvement. 55 | 56 | For detailed information on each dimension, refer to [Dimensions](./usage/dimensions). 57 | 58 | 59 | 60 | 61 | 62 | ## Evidence 63 | If your CISO requires you to document evidence that an activity is completed, you can edit your `generated.yaml` file as documented in the [README.md](./usage/README) _Teams and Groups_. It is currently not possible to provide evidence directly in the browser. 64 | -------------------------------------------------------------------------------- /src/app/pages/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHandler } from '@angular/common/http'; 2 | import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MatSelectModule } from '@angular/material/select'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { MatSliderModule } from '@angular/material/slider'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatIconModule } from '@angular/material/icon'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { SettingsComponent } from './settings.component'; 13 | import { SettingsService } from '../../service/settings/settings.service'; 14 | import { LoaderService } from '../../service/loader/data-loader.service'; 15 | import { MockLoaderService } from '../../service/loader/mock-data-loader.service'; 16 | import { Data } from 'src/app/model/activity-store'; 17 | import { ModalMessageComponent } from 'src/app/component/modal-message/modal-message.component'; 18 | 19 | let mockLoaderService: MockLoaderService; 20 | const MOCK_DATA = { 21 | 'Build and Deployment': { 22 | 'Deployment Process': { 23 | 'Automated Deployment': { 24 | uuid: 'test-uuid-1', 25 | level: 5, 26 | name: 'Automated Deployment', 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | describe('SettingsComponent', () => { 33 | let component: SettingsComponent; 34 | let fixture: ComponentFixture; 35 | let settingsService: jasmine.SpyObj; 36 | let modalComponent: jasmine.SpyObj; 37 | mockLoaderService = new MockLoaderService(MOCK_DATA as unknown as Data); 38 | 39 | beforeEach(async () => { 40 | await mockLoaderService.load(); 41 | settingsService = jasmine.createSpyObj('SettingsService', [ 42 | 'getMaxLevel', 43 | 'setMaxLevel', 44 | 'getDateFormat', 45 | 'setDateFormat', 46 | ]); 47 | modalComponent = jasmine.createSpyObj('ModalMessageComponent', ['openDialog']); 48 | 49 | await TestBed.configureTestingModule({ 50 | imports: [ 51 | FormsModule, 52 | ReactiveFormsModule, 53 | NoopAnimationsModule, 54 | MatSelectModule, 55 | MatFormFieldModule, 56 | MatInputModule, 57 | MatSliderModule, 58 | MatCardModule, 59 | MatIconModule, 60 | MatButtonModule, 61 | ], 62 | declarations: [SettingsComponent], 63 | providers: [ 64 | HttpClient, 65 | HttpHandler, 66 | { provide: SettingsService, useValue: settingsService }, 67 | { provide: LoaderService, useValue: mockLoaderService }, 68 | { provide: ModalMessageComponent, useValue: modalComponent }, 69 | ], 70 | }).compileComponents(); 71 | }); 72 | 73 | beforeEach(fakeAsync(() => { 74 | fixture = TestBed.createComponent(SettingsComponent); 75 | component = fixture.componentInstance; 76 | fixture.detectChanges(); 77 | tick(); 78 | fixture.detectChanges(); 79 | })); 80 | 81 | it('should create', fakeAsync(() => { 82 | expect(component).toBeTruthy(); 83 | })); 84 | 85 | it('should update max level settings correctly', fakeAsync(() => { 86 | component.onMaxLevelChange(3); 87 | 88 | expect(component.selectedMaxLevel).toBe(3); 89 | expect(settingsService.setMaxLevel).toHaveBeenCalledWith(3); 90 | })); 91 | 92 | it('should handle max level reset to default', fakeAsync(() => { 93 | component.onMaxLevelChange(5); 94 | 95 | expect(component.selectedMaxLevel).toBe(5); 96 | 97 | // Remove localStorage when settings' maxLevel is set to activity's maxLevel 98 | expect(settingsService.setMaxLevel).toHaveBeenCalledWith(null); 99 | })); 100 | }); 101 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "DSOMM": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/dsomm", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "allowedCommonJsDependencies": ["yamljs"], 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/custom-theme.scss", 32 | "src/styles.css" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "initial", 41 | "maximumWarning": "1mb", 42 | "maximumError": "3mb" 43 | }, 44 | { 45 | "type": "anyComponentStyle", 46 | "maximumWarning": "2kb", 47 | "maximumError": "6kb" 48 | } 49 | ], 50 | "fileReplacements": [ 51 | { 52 | "replace": "src/environments/environment.ts", 53 | "with": "src/environments/environment.prod.ts" 54 | } 55 | ], 56 | "outputHashing": "all" 57 | }, 58 | "development": { 59 | "buildOptimizer": false, 60 | "optimization": false, 61 | "vendorChunk": true, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "DSOMM:build:production" 74 | }, 75 | "development": { 76 | "browserTarget": "DSOMM:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "DSOMM:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 100 | "src/styles.css" 101 | ], 102 | "scripts": [] 103 | } 104 | }, 105 | "lint": { 106 | "builder": "@angular-eslint/builder:lint", 107 | "options": { 108 | "lintFilePatterns": [ 109 | "src/**/*.ts", 110 | "src/**/*.html" 111 | ] 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "defaultProject": "DSOMM", 118 | "cli": { 119 | "defaultCollection": "@angular-eslint/schematics" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/app/pages/teams/teams.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |

{{ infoTitle }}

17 |
18 | {{ info[infoTitle]?.teams?.join(', ') }} 19 |
20 |
21 |
22 | 26 | 30 | 34 |
35 |
36 | 37 |

Activities in progress

38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | 53 | 54 | 55 | 56 | 59 | 69 | 70 | 71 | 74 | 77 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 |
Team 42 | {{ element?.team }} 43 | 48 | Activity 49 | 51 | {{ element?.activity?.name }} 52 | 57 | Dimension 58 | 60 |
61 | 64 | {{ dataStore?.meta?.getIcon(element?.activity?.category || '') }} 65 | 66 | {{ element?.activity?.dimension }} 67 |
68 |
75 | {{ progressColumn }} 76 | 78 | 79 | {{ dateFormat(element?.progress?.[progressColumn]) }} 80 | 81 |
87 | Currently no activities in progress for {{ info[infoTitle]?.teams?.join(', ') }} 88 |
95 |
96 |
97 | -------------------------------------------------------------------------------- /src/app/pages/matrix/matrix.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { MatrixComponent, MatrixRow } from './matrix.component'; 6 | import { MatChip } from '@angular/material/chips'; 7 | import { ModalMessageComponent } from '../../component/modal-message/modal-message.component'; 8 | import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; 9 | import { LoaderService } from 'src/app/service/loader/data-loader.service'; 10 | import { MockLoaderService } from 'src/app/service/loader/mock-data-loader.service'; 11 | 12 | // Setup test data 13 | const MOCK_DATA: any = { 14 | 'Test Category': { 15 | 'Test Dimension': { 16 | 'Activity 1': { uuid: '1', level: 1, tags: ['tag1', 'tag2'] }, 17 | 'Activity 2': { uuid: '2', level: 1, tags: ['tag2', 'tag3'] }, 18 | }, 19 | }, 20 | 'Test Category 2': { 21 | 'Test Dimension 2': { 22 | 'Activity Other': { uuid: '3', level: 1, tags: [] }, 23 | }, 24 | }, 25 | }; 26 | let mockLoaderService: MockLoaderService; 27 | 28 | describe('MatrixComponent', () => { 29 | let component: MatrixComponent; 30 | let fixture: ComponentFixture; 31 | 32 | beforeEach(async () => { 33 | mockLoaderService = new MockLoaderService(MOCK_DATA); 34 | await TestBed.configureTestingModule({ 35 | declarations: [MatrixComponent, MatChip], 36 | imports: [RouterTestingModule, HttpClientModule, MatDialogModule], 37 | providers: [ 38 | HttpClientTestingModule, 39 | { provide: LoaderService, useValue: mockLoaderService }, 40 | { provide: MatDialogRef, useValue: {} }, 41 | { provide: ModalMessageComponent, useValue: {} }, 42 | ], 43 | }).compileComponents(); 44 | }); 45 | 46 | beforeEach(async () => { 47 | fixture = TestBed.createComponent(MatrixComponent); 48 | component = fixture.componentInstance; 49 | 50 | fixture.detectChanges(); 51 | await fixture.whenStable(); 52 | }); 53 | 54 | it('should create', () => { 55 | expect(component).toBeTruthy(); 56 | }); 57 | 58 | it('should build matrix data', () => { 59 | // Verify the data was loaded 60 | expect(component.MATRIX_DATA).toBeTruthy(); 61 | expect(component.MATRIX_DATA.length).toBeGreaterThan(0); 62 | expect(component.MATRIX_DATA[0].Category).toBe('Test Category'); 63 | expect(component.MATRIX_DATA[0].Dimension).toBe('Test Dimension'); 64 | expect(component.MATRIX_DATA[0].level1.length).toBe(2); 65 | 66 | // Verify filters were initialized 67 | expect(Object.keys(component.filtersTag)).toContain('tag1'); 68 | expect(Object.keys(component.filtersDim)).toContain('Test Dimension'); 69 | }); 70 | 71 | it('should filter data when tag filter is selected', () => { 72 | expect(component.dataSource.data.length).toBe(2); 73 | expect(component.dataSource.data[0].level1.length).toBe(2); 74 | 75 | // Create a mock MatChip with proper state tracking 76 | const mockChip = { 77 | value: 'tag1', 78 | selected: false, 79 | toggleSelected: function () { 80 | this.selected = !this.selected; 81 | }, 82 | } as MatChip; 83 | 84 | // Ensure initial state 85 | mockChip.selected = false; 86 | 87 | // Toggle tag filter on 88 | console.log('Turn chip filter on'); 89 | component.toggleTagFilters(mockChip); 90 | // console.log('data after "on":', component.dataSource.data); 91 | expect(component.filtersTag['tag1']).toBeTrue(); 92 | expect(component.dataSource.data.length).toBe(1); 93 | expect(component.dataSource.data[0].level1.length).toBe(1); 94 | 95 | // Toggle tag filter off again 96 | console.log('Turn chip filter off'); 97 | component.toggleTagFilters(mockChip); 98 | // console.log('data after "off": ', component.dataSource.data); 99 | expect(component.filtersTag['tag1']).toBeFalse(); 100 | expect(component.dataSource.data.length).toBe(2); 101 | expect(component.dataSource.data[0].level1.length).toBe(2); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /.github/proposal.md: -------------------------------------------------------------------------------- 1 | # OWASP DSOMM Enhancement Proposal 2 | 3 | ## Overview 4 | 5 | This proposal outlines key enhancements to the OWASP DevSecOps Maturity Model (DSOMM) to improve its functionality, usability, and integration with other security frameworks. The total estimated effort for all proposed features is 268 hours (33.5 days). 6 | 7 | ## Proposed Enhancements 8 | 9 | ### 1. Vulnerability Management and Patch Management Expansion 10 | 11 | **Problem:** Current vulnerability management coverage is incomplete, particularly lacking metrics. 12 | **Solution:** Integrate concepts from the Vulnerability Management Maturity Model and "Effective Vulnerability Management" book to add: 13 | - New activities mapped to SAMM, ISO, and OpenCRE 14 | - Risk and measure descriptions 15 | - Implementation guidance 16 | - Level justifications based on effort and security value 17 | 18 | **Estimated Effort:** 32 hours 19 | 20 | ### 2. Compliance Date Integration 21 | 22 | **Problem:** Activity implementation status doesn't account for time-based assessment/compliance requirements. 23 | 24 | **Solution:** 25 | As a security architect, I want teams to perform threat modeling quaterly. 26 | As a project team, I perform a threat modeling and the status is DSOMM for that team is changed to "implemented". As there is no automatic removal of the status, it stays "implemented". 27 | 28 | Tasks: 29 | - Add `threshold` attribute to activities for time-based assessment/compliance 30 | - Enhance `teamsImplemented` attribute to track implementation dates 31 | - Update UI to display assessment/compliance status based on dates and thresholds 32 | 33 | Sample `threshold`: 34 | ``` 35 | threshold: 36 | targets: 37 | - type: "count" 38 | minValue: 1 39 | period: 40 | periodType: sliding 41 | timeframe: "2Y" 42 | ``` 43 | The `teamsImplemented` attribute, to be filled out by teams: 44 | ``` 45 | teamsImplemented: 46 | - teamA: 47 | conductionDate: 2024-08-08 00:00:00 48 | - teamB: 49 | conductionDate: 2024-08-08 00:00:00 50 | - teamB: 51 | implemented: true 52 | ``` 53 | 54 | **Estimated Effort:** 80 hours 55 | 56 | ### 3. Score Calculation 57 | 58 | **Problem:** Current visualization can be difficult to interpret quickly. 59 | 60 | **Solution:** Implement an overall score calculation for each sub-dimension, showing implemented vs. maximum possible activities for teams. 61 | 62 | **Estimated Effort:** 8 hours 63 | 64 | ### 4. Customization Capabilities 65 | 66 | **Problem:** Organizations need to adapt DSOMM for their specific security programs, which is currently challenging. 67 | 68 | **Solution:** Make the DSOMM application customizable: 69 | - Auto-adjust levels in visualizations when changed 70 | - Allow hiding/adding attributes for activity descriptions 71 | - Ensure consistent updates across linked elements (e.g., overview tables, detailed descriptions) 72 | 73 | **Estimated Effort:** 80 hours 74 | 75 | ### 5. OpenCRE Integration Enhancement 76 | 77 | **Problem:** Current OpenCRE chatbot lacks comprehensive DSOMM content integration. 78 | 79 | **Solution:** 80 | - Customize OpenCRE content with DSOMM-specific information 81 | - Provide sample pre-questions for improved DSOMM coverage 82 | - Create a guide for enhancing OpenCRE content for other projects 83 | 84 | The solution needs to be implemented together with openCRE team. 85 | 86 | **Estimated Effort:** 60 hours 87 | 88 | ### 6. Status `notApplicable` 89 | **Problem:** An application security program defines activities to be implemented by product teams. Sometimes, the activities are not applicable to a product/application. 90 | 91 | **Solution:** Add the status `notApplicable` for teams 92 | 93 | **Estimated Effort:** 8 hours 94 | 95 | ## Total Estimated Effort 96 | 97 | 268 hours (33.5 days) 98 | 99 | ## Benefits 100 | 101 | - Improved vulnerability management guidance 102 | - Better compliance tracking and reporting 103 | - Enhanced data visualization and interpretation 104 | - Increased flexibility for organizational adoption 105 | - Tighter integration with broader security ecosystems 106 | 107 | ## Conclusion 108 | 109 | These enhancements will significantly improve the usability, adaptability, and value of OWASP DSOMM for organizations implementing DevSecOps practices. The proposed changes will make DSOMM a more comprehensive and user-friendly tool for assessing and improving security maturity. 110 | -------------------------------------------------------------------------------- /src/app/pages/circular-heatmap/circular-heatmap.component.css: -------------------------------------------------------------------------------- 1 | .margin30 { 2 | margin-bottom: 30px; 3 | } 4 | .axis path, 5 | .axis line { 6 | fill: none; 7 | stroke: #000; 8 | shape-rendering: crispEdges; 9 | } 10 | 11 | .title-button { 12 | background-color: transparent; 13 | border: none; 14 | text-align: left; 15 | cursor: pointer; 16 | font-weight: 700; 17 | } 18 | 19 | .right-panel { 20 | margin: 5%; 21 | padding: 20px; 22 | height: 80vh; 23 | display: grid; 24 | grid-template-rows: auto 1fr auto; 25 | } 26 | 27 | .heatmapClass { 28 | display: grid; 29 | grid-template-rows: auto 1fr auto; 30 | grid-template-columns: 6fr 4fr; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .heatmapChart { 36 | grid-row: 1/4; 37 | display: grid; 38 | justify-items: center; 39 | align-content: space-between; 40 | } 41 | #chart { 42 | width: 100%; 43 | max-width: min(100vh - 60px, 100vw - 60px); 44 | } 45 | 46 | .downloadButtonClass { 47 | margin: 10px 0; 48 | } 49 | .overlay-details { 50 | z-index: 2; 51 | background-color: rgba(0, 0, 0, 0.555); 52 | backdrop-filter: blur(3px); 53 | position: absolute; 54 | /* padding: 6em; */ 55 | /* margin-left: 20%; */ 56 | width: 60%; 57 | min-height: 100%; 58 | } 59 | .overlay-modal { 60 | /* border: 1px solid black; */ 61 | margin: 1em; 62 | background-color: rgb(238, 238, 238); 63 | padding: 1em; 64 | border-radius: 1em; 65 | height: 100%; 66 | } 67 | 68 | .overlay-header { 69 | display: grid; 70 | grid-template-columns: 1fr 0.1fr; 71 | } 72 | 73 | .overlay-close { 74 | border: none; 75 | background-color: rgba(0, 0, 0, 0); 76 | grid-column: 2/3; 77 | grid-row: 1/4; 78 | display: grid; 79 | justify-content: center; 80 | align-items: start; 81 | margin-left: auto; 82 | } 83 | 84 | /* overlay-close - light theme */ 85 | :host-context(body.light-theme) .overlay-close { 86 | color: black; 87 | } 88 | 89 | /* overlay-close - dark theme */ 90 | :host-context(body.dark-theme) .overlay-close { 91 | color: white; 92 | } 93 | 94 | .team-filter { 95 | padding: 0.4rem; 96 | grid-column: 2/3; 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | 101 | .team-filter.hidden { 102 | height: 0; 103 | visibility: collapse; 104 | } 105 | 106 | .filter-toggle .hidden { 107 | display: none; 108 | } 109 | .team-list { 110 | width: 100%; 111 | list-style: none; 112 | padding: 0; 113 | margin: 0; 114 | } 115 | .team-list li { 116 | display: grid; 117 | grid-template-columns: minmax(100px, auto) 1fr; 118 | gap: 16px; 119 | align-items: center; 120 | margin-bottom: 8px; 121 | } 122 | .team-label { 123 | white-space: nowrap; 124 | text-align: right; 125 | font-size: smaller; 126 | } 127 | app-progress-slider { 128 | width: 100%; 129 | } 130 | .mat-chip-list { 131 | display: flex; 132 | flex-direction: row; 133 | flex-wrap: wrap; 134 | padding: 1rem; 135 | } 136 | .filter-container { 137 | position: relative; 138 | } 139 | 140 | .smaller-italics { 141 | font-size: smaller; 142 | font-style: italic; 143 | } 144 | 145 | button.filter-toggle { 146 | position: absolute; 147 | top: 2px; 148 | right: 2px; 149 | border: 0; 150 | background-color: transparent; 151 | z-index: 1; 152 | padding: 0; 153 | min-width: 0; 154 | line-height: 24px; 155 | } 156 | 157 | .footer-buttons { 158 | grid-column: 2/3; 159 | place-self: flex-end; 160 | margin: 0 1rem; 161 | padding-top: 1rem; 162 | display: flex; 163 | align-items: flex-end; 164 | flex-direction: column; 165 | } 166 | 167 | :host ::ng-deep .activity-card .mat-expansion-panel-body { 168 | padding: 0 5px; 169 | } 170 | 171 | :host ::ng-deep .activity-card .mat-expansion-panel-body li { 172 | justify-content: center; 173 | align-items: center; 174 | } 175 | 176 | @media only screen and (max-width: 750px) { 177 | .heatmapClass { 178 | grid-template-rows: auto auto 1fr auto; 179 | grid-template-columns: 1fr; 180 | } 181 | 182 | .team-filter, .heatmapChart, .footer-buttons { 183 | grid-column: 1; 184 | } 185 | 186 | .team-filter { 187 | grid-row: 1; 188 | padding: 0.4rem; 189 | } 190 | 191 | .mat-chip-list { 192 | padding: 0.4rem; 193 | } 194 | 195 | .heatmapChart { 196 | grid-row: 2; 197 | } 198 | 199 | #chart { 200 | max-width: max(60vh, 60vw); 201 | } 202 | 203 | .overlay-details { 204 | width: 100%; 205 | } 206 | 207 | } -------------------------------------------------------------------------------- /src/app/pages/settings/settings.component.css: -------------------------------------------------------------------------------- 1 | .vflex { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5em; 5 | } 6 | 7 | .hflex { 8 | display: flex; 9 | align-items: center; 10 | gap: 0.5em; 11 | } 12 | 13 | .grid-row-1 { 14 | grid-row: 1; 15 | } 16 | .grid-row-2 { 17 | grid-row: 2; 18 | } 19 | .grid-col-1 { 20 | grid-column: 1; 21 | } 22 | .grid-col-2 { 23 | grid-column: 2; 24 | } 25 | .grid-col-3 { 26 | grid-column: 3; 27 | } 28 | .grid-col-4 { 29 | grid-column: 4; 30 | } 31 | 32 | .settings-container { 33 | margin-top: 1em; 34 | padding: 0 1rem; 35 | } 36 | 37 | .mat-slider-horizontal { 38 | max-width: 100px; 39 | width: 100px; 40 | min-width: 80px; 41 | } 42 | 43 | .date-format-container { 44 | display: flex; 45 | align-items: center; 46 | gap: 0.2em; 47 | width: auto; 48 | max-width: 40rem; 49 | } 50 | 51 | .nowrap { 52 | white-space: nowrap; 53 | } 54 | 55 | .date-format-field { 56 | flex: 1; 57 | margin: 0; 58 | } 59 | 60 | .max-level-row { 61 | display: flex; 62 | align-items: center; 63 | gap: 1em; 64 | } 65 | 66 | .max-slider { 67 | flex: 1; 68 | } 69 | 70 | 71 | .progress-definitions-grid { 72 | display: grid; 73 | grid-template-columns: auto 5em minmax(200px, 2fr) auto; 74 | gap: 0.3em; 75 | } 76 | 77 | .progress-definitions-grid ::ng-deep .mat-form-field-wrapper { 78 | padding-bottom: 0; 79 | } 80 | 81 | .progress-definitions-grid ::ng-deep .mat-form-field-suffix { 82 | top: 0; 83 | } 84 | 85 | .grid-cell.progress-key { 86 | font-weight: 500; 87 | } 88 | 89 | .grid-cell.progress-score { 90 | text-align: right; 91 | padding-right: 0.5em; 92 | } 93 | input.progress-score::-webkit-outer-spin-button, 94 | input.progress-score::-webkit-inner-spin-button { 95 | -webkit-appearance: none; 96 | /* margin: 0; */ 97 | } 98 | input.progress-score { 99 | -moz-appearance: textfield; 100 | /* text-align: right; */ 101 | } 102 | 103 | .grid-cell.action-buttons, 104 | .grid-cell.progress-definition { 105 | margin-bottom: 0.8em; 106 | } 107 | 108 | 109 | .grid-cell.progress-score span, 110 | .grid-cell.action-buttons { 111 | color: #aaa; 112 | } 113 | .grid-cell.action-buttons { 114 | align-self: center; 115 | justify-self: center; 116 | } 117 | 118 | .selectable-list { 119 | padding: 0; 120 | } 121 | 122 | .selectable-list-header { 123 | display: flex; 124 | align-items: center; 125 | gap: 8px; 126 | margin-bottom: 16px; 127 | } 128 | 129 | .selectable-list-header h2 { 130 | margin: 0; 131 | flex: 1; 132 | } 133 | 134 | .version-info-section .error { 135 | font-style: italic; 136 | } 137 | 138 | .edit-hint { 139 | color: #666; 140 | font-size: 0.9em; 141 | font-style: italic; 142 | } 143 | 144 | mat-icon.mandatory-icon { 145 | padding: 8px; 146 | } 147 | 148 | .help-text { 149 | font-size: 0.75em; 150 | color: #666; 151 | margin-top: 0.5em; 152 | } 153 | 154 | .version-info-customization { 155 | margin-top: 2rem; 156 | } 157 | 158 | .version-info-table { 159 | display: grid; 160 | grid-template-columns: 160px 1fr 1fr 1fr; 161 | gap: 8px; 162 | margin-bottom: 16px; 163 | max-width: 40rem; 164 | } 165 | 166 | @media screen and (max-width: 650px) { 167 | .progress-definitions-grid { 168 | grid-template-columns: auto 5em; 169 | } 170 | 171 | .grid-cell.action-buttons, 172 | .grid-cell.progress-definition { 173 | margin-bottom: 1.8em; 174 | } 175 | 176 | .edit-hint { 177 | display: none; 178 | } 179 | } 180 | 181 | /* About DSOMM styles */ 182 | .settings-about-section { 183 | margin-top: 1rem; 184 | } 185 | .settings-about-section .mat-card-header { 186 | padding-bottom: 0.25rem; 187 | } 188 | .settings-about-section .mat-card-title { 189 | font-size: 1.1rem; 190 | } 191 | .settings-about-section .mat-card-subtitle { 192 | color: #666; 193 | font-size: 0.9rem; 194 | } 195 | .settings-about-section .subheader { 196 | font-style: italic; 197 | text-align: center; 198 | margin-bottom: 0.5rem; 199 | } 200 | .settings-about-section .card-content { 201 | /* keep vertical spacing but align left with page container */ 202 | padding-top: 0; 203 | padding-right: 1rem; 204 | padding-bottom: 1rem; 205 | padding-left: 0; 206 | } 207 | .settings-about-section .button-container { 208 | display: flex; 209 | align-items: flex-start; 210 | gap: 0.6rem; 211 | margin: 0.5rem 0; 212 | } 213 | .muted { 214 | color: #555; 215 | margin: 0.25rem 0 0.75rem 0; 216 | } 217 | .small { 218 | font-size: 0.85rem; 219 | color: #666; 220 | } 221 | .about-actions { 222 | display: flex; 223 | align-items: center; 224 | gap: 0.75rem; 225 | flex-wrap: wrap; 226 | } 227 | .about-actions .status { 228 | display: flex; 229 | align-items: center; 230 | gap: 0.75rem; 231 | } 232 | .status-icon { 233 | color: var(--accent-color, #1976d2); 234 | } 235 | .update-available { 236 | display: flex; 237 | align-items: center; 238 | gap: 0.5rem; 239 | color: #d84315; /* warm accent for update */ 240 | } 241 | .update-none { 242 | display: flex; 243 | align-items: center; 244 | gap: 0.5rem; 245 | color: #2e7d32; /* green for ok */ 246 | } 247 | .action-link { 248 | margin-left: 0.5rem; 249 | font-weight: 500; 250 | } 251 | -------------------------------------------------------------------------------- /src/app/pages/circular-heatmap/circular-heatmap.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 8 |
9 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 24 |

Nothing to show

25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 36 |
37 | 38 | Team Group Filter 39 | 40 | 45 | {{ group.key }} 46 | 47 | 48 | 49 | 50 | Team Filter 51 | 52 | 58 | {{ team.key }} 59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | {{ showActivityCard.dimension }} 67 | Level {{ showActivityCard.level }} 68 | 69 | 71 | 74 | 75 | 76 | 79 | 80 | 81 | 82 |
    83 | 84 |
  • 85 | 86 | 94 |
  • 95 |
    96 |
97 |
98 | * Has been modified
99 | ** Has been modified to less complete stage than before 100 |
101 |
102 |
103 |
104 |
105 | 121 |
122 |
123 |
124 |
125 |
126 | -------------------------------------------------------------------------------- /src/app/component/activity-description/activity-description.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ViewChildren, 4 | QueryList, 5 | Input, 6 | OnChanges, 7 | SimpleChanges, 8 | Output, 9 | EventEmitter, 10 | OnInit, 11 | HostListener, 12 | } from '@angular/core'; 13 | import { MatAccordion } from '@angular/material/expansion'; 14 | import { Activity } from '../../model/activity-store'; 15 | import { LoaderService } from '../../service/loader/data-loader.service'; 16 | import { TeamName, ProgressTitle } from '../../model/types'; 17 | 18 | @Component({ 19 | selector: 'app-activity-description', 20 | templateUrl: './activity-description.component.html', 21 | styleUrls: ['./activity-description.component.css'], 22 | }) 23 | export class ActivityDescriptionComponent implements OnInit, OnChanges { 24 | @Input() activity: Activity | null = null; 25 | @Input() iconName: string = ''; 26 | @Input() showCloseButton: boolean = false; 27 | @Output() activityClicked = new EventEmitter(); 28 | @Output() closeRequested = new EventEmitter(); 29 | 30 | currentActivity: Partial = {}; 31 | TimeLabel: string = ''; 32 | KnowledgeLabel: string = ''; 33 | ResourceLabel: string = ''; 34 | UsefulnessLabel: string = ''; 35 | SAMMVersion: string = 'OWASP SAMM v2'; 36 | ISOVersion: string = 'ISO 27001:2017'; 37 | ISO22Version: string = 'ISO 27001:2022'; 38 | openCREVersion: string = 'OpenCRE'; 39 | isNarrowScreen: boolean = false; 40 | teamsImplemented: Map = new Map(); 41 | teamsByProgressTitle: Map = new Map(); 42 | progressTitlesWithTeams: ProgressTitle[] = []; 43 | 44 | @ViewChildren(MatAccordion) accordion!: QueryList; 45 | 46 | constructor(private loader: LoaderService) {} 47 | 48 | ngOnInit() { 49 | // Set activity data if provided 50 | if (this.activity) { 51 | this.setActivityData(this.activity); 52 | } 53 | // Check initial screen size 54 | this.checkWidthForActivityPanel(); 55 | // Set up observers to watch for layout changes 56 | } 57 | 58 | @HostListener('window:resize', ['$event']) 59 | onResize(event: any) { 60 | this.checkWidthForActivityPanel(); 61 | } 62 | 63 | ngOnChanges(changes: SimpleChanges) { 64 | // Handle changes to activity input 65 | if (changes['activity'] && changes['activity'].currentValue) { 66 | this.setActivityData(changes['activity'].currentValue); 67 | } 68 | } 69 | 70 | setActivityData(activity: Activity) { 71 | this.currentActivity = activity; 72 | 73 | // Get datastore for labels 74 | const dataStore = this.loader.datastore; 75 | if (dataStore) { 76 | /* eslint-disable */ 77 | this.KnowledgeLabel = dataStore.getMetaString('knowledgeLabels', activity.difficultyOfImplementation.knowledge - 1); 78 | this.TimeLabel = dataStore.getMetaString('labels', activity.difficultyOfImplementation.time - 1); 79 | this.ResourceLabel = dataStore.getMetaString('labels', activity.difficultyOfImplementation.resources - 1); 80 | this.UsefulnessLabel = dataStore.getMetaString('labels', activity.usefulness - 1); 81 | /* eslint-enable */ 82 | 83 | // Get teams that have implemented this activity 84 | this.updateTeamsImplemented(); 85 | } 86 | } 87 | 88 | updateTeamsImplemented() { 89 | this.teamsImplemented.clear(); 90 | this.teamsByProgressTitle.clear(); 91 | this.progressTitlesWithTeams = []; 92 | 93 | const dataStore = this.loader.datastore; 94 | if (!dataStore || !dataStore.progressStore || !dataStore.meta || !this.currentActivity.uuid) { 95 | return; 96 | } 97 | 98 | const teams = dataStore.meta.teams; 99 | const progressStore = dataStore.progressStore; 100 | const activityUuid = this.currentActivity.uuid; 101 | 102 | // Get all progress titles (excluding the first one which is "Not started") 103 | const inProgressTitles = progressStore.getInProgressTitles(); 104 | const completedTitle = progressStore.getCompletedProgressTitle(); 105 | const allProgressTitles = [...inProgressTitles, completedTitle]; 106 | 107 | // Check each team to see if they have started or completed this activity 108 | for (const teamName of teams) { 109 | const progressTitle = progressStore.getTeamProgressTitle(activityUuid, teamName); 110 | const progressValue = progressStore.getTeamActivityProgressValue(activityUuid, teamName); 111 | 112 | // Only include teams that have made progress (value > 0) 113 | if (progressValue > 0) { 114 | this.teamsImplemented.set(teamName, progressTitle); 115 | 116 | // Group teams by progress title 117 | if (!this.teamsByProgressTitle.has(progressTitle)) { 118 | this.teamsByProgressTitle.set(progressTitle, []); 119 | } 120 | this.teamsByProgressTitle.get(progressTitle)!.push(teamName); 121 | } 122 | } 123 | 124 | // Create ordered list of progress titles that have teams (skip "Not started") 125 | for (const progressTitle of allProgressTitles) { 126 | if (this.teamsByProgressTitle.has(progressTitle)) { 127 | this.progressTitlesWithTeams.push(progressTitle); 128 | } 129 | } 130 | } 131 | 132 | onActivityClicked(activityName: string) { 133 | // Emit event for parent component to handle 134 | this.activityClicked.emit(activityName); 135 | } 136 | 137 | onCloseRequested() { 138 | this.closeRequested.emit(); 139 | } 140 | 141 | // Check if screen is narrow and update property 142 | private checkWidthForActivityPanel(): void { 143 | let elemtn: HTMLElement | null = document.querySelector('app-activity-description'); 144 | if (!elemtn) return; 145 | 146 | const currentWidth = elemtn.offsetWidth; 147 | const wasNarrow = this.isNarrowScreen; 148 | this.isNarrowScreen = currentWidth < 500; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for 6 | everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity 7 | and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, 8 | color, religion, or sexual identity and orientation. 9 | 10 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our community include: 15 | 16 | * Demonstrating empathy and kindness toward other people 17 | * Being respectful of differing opinions, viewpoints, and experiences 18 | * Giving and gracefully accepting constructive feedback 19 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 20 | * Focusing on what is best not just for us as individuals, but for the overall community 21 | 22 | Examples of unacceptable behavior include: 23 | 24 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 25 | * Trolling, insulting or derogatory comments, and personal or political attacks 26 | * Public or private harassment 27 | * Publishing others' private information, such as a physical or email address, without their explicit permission 28 | * Other conduct which could reasonably be considered inappropriate in a professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take 33 | appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, 34 | or harmful. 35 | 36 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 37 | issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for 38 | moderation decisions when appropriate. 39 | 40 | ## Scope 41 | 42 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing 43 | the community in public spaces. Examples of representing our community include using an official e-mail address, posting 44 | via an official social media account, or acting as an appointed representative at an online or offline event. 45 | 46 | ## Enforcement 47 | 48 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible 49 | for enforcement at 50 | [bjoern.kimminich@owasp.org](mailto:bjoern.kimminich@owasp.org). All complaints will be reviewed and investigated 51 | promptly and fairly. 52 | 53 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 54 | 55 | ## Enforcement Guidelines 56 | 57 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem 58 | in violation of this Code of Conduct: 59 | 60 | ### 1. Correction 61 | 62 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the 63 | community. 64 | 65 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation 66 | and an explanation of why the behavior was inappropriate. A public apology may be requested. 67 | 68 | ### 2. Warning 69 | 70 | **Community Impact**: A violation through a single incident or series of actions. 71 | 72 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including 73 | unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding 74 | interactions in community spaces as well as external channels like social media. Violating these terms may lead to a 75 | temporary or permanent ban. 76 | 77 | ### 3. Temporary Ban 78 | 79 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 80 | 81 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified 82 | period of time. No public or private interaction with the people involved, including unsolicited interaction with those 83 | enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 84 | 85 | ### 4. Permanent Ban 86 | 87 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate 88 | behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 89 | 90 | **Consequence**: A permanent ban from any sort of public interaction within the community. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at 95 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 96 | 97 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 98 | 99 | For answers to common questions about this code of conduct, see the FAQ 100 | at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 101 | [https://www.contributor-covenant.org/translations][translations]. 102 | 103 | [homepage]: https://www.contributor-covenant.org 104 | 105 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 106 | 107 | [Mozilla CoC]: https://github.com/mozilla/diversity 108 | 109 | [FAQ]: https://www.contributor-covenant.org/faq 110 | 111 | [translations]: https://www.contributor-covenant.org/translations 112 | -------------------------------------------------------------------------------- /src/app/model/meta-store.ts: -------------------------------------------------------------------------------- 1 | import { YamlService } from '../service/yaml-loader/yaml-loader.service'; 2 | import { ActivityFileMeta } from './activity-store'; 3 | import { ProgressDefinitions, TeamNames, TeamGroups } from './types'; 4 | import { perfNow } from 'src/app/util/util'; 5 | 6 | export interface MetaStrings { 7 | allTeamsGroupName: string; 8 | labels: string[]; 9 | maturityLevels: string[]; 10 | knowledgeLabels: string[]; 11 | } 12 | const fallbackMetaStrings: MetaStrings = { 13 | allTeamsGroupName: 'All', 14 | maturityLevels: ['Level 1', 'Level 2'], 15 | labels: ['Easy', 'Medium', 'Hard'], 16 | knowledgeLabels: ['Low', 'Medium', 'High'], 17 | }; 18 | 19 | const STORAGE_TEAMS_KEY: string = 'meta.teams'; 20 | const STORAGE_GROUPS_KEY: string = 'meta.teamGroups'; 21 | const STORAGE_PROGRESS_DEFINITIONS_KEY: string = 'meta.progressDefinitions'; 22 | 23 | export class MetaStore { 24 | private yamlService: YamlService = new YamlService(); 25 | 26 | public hasLocalStorage: boolean = false; 27 | private defaultProgressDefinition: ProgressDefinitions = {}; 28 | 29 | public activityMeta: ActivityFileMeta | null = null; 30 | checkForDsommUpdates: boolean = false; 31 | lang: string = 'en'; 32 | strings: Record = { en: fallbackMetaStrings }; 33 | progressDefinition: ProgressDefinitions = {}; 34 | teamGroups: TeamGroups = {}; 35 | teams: TeamNames = []; 36 | activityFiles: string[] = []; 37 | teamProgressFile: string = ''; 38 | allowChangeTeamNameInBrowser: boolean = true; 39 | 40 | dimensionIcons: Record = { 41 | 'Build and Deployment': 'front_loader', 42 | 'Culture and Organization': 'diversity_3', 43 | Implementation: 'design_services', 44 | 'Information Gathering': 'insights', 45 | 'Test and Verification': 'checklist', 46 | default: 'check_box_outline_blank', 47 | }; 48 | 49 | public init(metaData: any): void { 50 | this.addMeta(metaData); 51 | } 52 | 53 | public addMeta(metaData: any): void { 54 | if (metaData) { 55 | // Only overwrite existing values if new values are provided 56 | this.checkForDsommUpdates = 57 | metaData.checkForDsommUpdates || this.checkForDsommUpdates || false; 58 | this.lang = metaData.lang || this.lang || 'en'; 59 | this.strings = metaData.strings || this.strings || fallbackMetaStrings; 60 | // Store default progress definition 61 | if (metaData.progressDefinition) { 62 | this.defaultProgressDefinition = { ...metaData.progressDefinition }; 63 | } 64 | // Load custom progress definition if exists, otherwise use default 65 | this.loadStoredProgressDefinition(); 66 | this.teamGroups = metaData.teamGroups || this.teamGroups || {}; 67 | this.teams = metaData.teams || this.teams || []; 68 | this.activityFiles = metaData.activityFiles || this.activityFiles || []; 69 | this.teamProgressFile = metaData.teamProgressFile || this.teamProgressFile || ''; 70 | if (metaData.allowChangeTeamNameInBrowser !== undefined) 71 | this.allowChangeTeamNameInBrowser = metaData.allowChangeTeamNameInBrowser; 72 | } 73 | } 74 | 75 | public saveProgressDefinition(definitions: ProgressDefinitions): void { 76 | this.progressDefinition = definitions; 77 | localStorage.setItem(STORAGE_PROGRESS_DEFINITIONS_KEY, JSON.stringify(definitions)); 78 | } 79 | 80 | public resetProgressDefinition(): void { 81 | this.progressDefinition = { ...this.defaultProgressDefinition }; 82 | localStorage.removeItem(STORAGE_PROGRESS_DEFINITIONS_KEY); 83 | } 84 | 85 | private loadStoredProgressDefinition(): void { 86 | const stored = localStorage.getItem(STORAGE_PROGRESS_DEFINITIONS_KEY); 87 | if (stored) { 88 | try { 89 | this.progressDefinition = JSON.parse(stored); 90 | } catch (error) { 91 | console.error('Failed to load stored progress definitions:', error); 92 | this.progressDefinition = { ...this.defaultProgressDefinition }; 93 | } 94 | } else { 95 | this.progressDefinition = { ...this.defaultProgressDefinition }; 96 | } 97 | } 98 | 99 | public updateTeamsAndGroups(teams: TeamNames, teamGroups: TeamGroups): void { 100 | this.teams = teams; 101 | this.teamGroups = teamGroups; 102 | this.saveTeamsAndGroups(); 103 | } 104 | 105 | public asStorableYamlString(): string { 106 | return this.yamlService.stringify({ teams: this.teams, teamGroups: this.teamGroups }); 107 | } 108 | 109 | public saveTeamsAndGroups() { 110 | let yamlStr: string = this.yamlService.stringify({ teams: this.teams }); 111 | localStorage.setItem(STORAGE_TEAMS_KEY, yamlStr); 112 | console.log('Saved teams to localStorage: ' + yamlStr); 113 | yamlStr = this.yamlService.stringify({ teamGroups: this.teamGroups }); 114 | localStorage.setItem(STORAGE_GROUPS_KEY, yamlStr); 115 | console.log('Saved team groups to localStorage: ' + yamlStr); 116 | this.hasLocalStorage = true; 117 | } 118 | 119 | public deleteTeamsAndGroups() { 120 | localStorage.removeItem(STORAGE_TEAMS_KEY); 121 | localStorage.removeItem(STORAGE_GROUPS_KEY); 122 | this.hasLocalStorage = false; 123 | } 124 | 125 | public loadTeamsAndGroups(): void { 126 | let storedTeams: string | null = localStorage.getItem(STORAGE_TEAMS_KEY); 127 | let storedGroups: string | null = localStorage.getItem(STORAGE_GROUPS_KEY); 128 | try { 129 | let metaTeams: { teams: TeamNames } | null = null; 130 | let metaGroups: { teamGroups: TeamGroups } | null = null; 131 | 132 | if (storedTeams) metaTeams = this.yamlService.parse(storedTeams); 133 | if (storedGroups) metaGroups = this.yamlService.parse(storedGroups); 134 | 135 | this.addMeta({ teams: metaTeams?.teams, teamGroups: metaGroups?.teamGroups }); 136 | this.hasLocalStorage = true; 137 | console.log('Loaded stored meta from localStorage'); 138 | } catch (error) { 139 | console.error('Failed to load stored meta from localStorage:', error); 140 | } 141 | } 142 | 143 | getIcon(dimension: string): string { 144 | return this.dimensionIcons[dimension] || this.dimensionIcons['default']; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | .mat-drawer, 4 | .mat-drawer-container { 5 | transition: background 300ms 6 | cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms 7 | cubic-bezier(0.4, 0, 0.2, 1); 8 | } 9 | 10 | // ---------------------------------------------- 11 | // Theme Colors and Typography 12 | // ---------------------------------------------- 13 | $light-theme: ( 14 | background: white, 15 | text: black, 16 | link: blue, 17 | ); 18 | 19 | $custom-dark-theme: ( 20 | background: #2c2c2c, 21 | text: #e0e0e0, 22 | link: #bb86fc, 23 | ); 24 | 25 | $custom-typography: mat.define-typography-level( 26 | $font-family: montserrat, 27 | $font-weight: 400, 28 | $font-size: 1rem, 29 | $line-height: 1, 30 | $letter-spacing: normal 31 | ); 32 | @include mat.core($custom-typography); 33 | 34 | // ---------------------------------------------- 35 | // Angular Material Palettes 36 | // ---------------------------------------------- 37 | $DSOMM-primary: mat.define-palette(mat.$green-palette, 400); 38 | $DSOMM-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 39 | $DSOMM-warn: mat.define-palette(mat.$red-palette); 40 | 41 | $DSOMM-dark-primary: mat.define-palette(mat.$green-palette, 600); 42 | 43 | // ---------------------------------------------- 44 | // Angular Material Themes 45 | // ---------------------------------------------- 46 | $DSOMM-light-theme: mat.define-light-theme(( 47 | color: ( 48 | primary: $DSOMM-primary, 49 | accent: $DSOMM-accent, 50 | warn: $DSOMM-warn 51 | ) 52 | )); 53 | 54 | $DSOMM-dark-theme: mat.define-dark-theme(( 55 | color: ( 56 | primary: $DSOMM-dark-primary, 57 | accent: $DSOMM-accent, 58 | warn: $DSOMM-warn 59 | ) 60 | )); 61 | 62 | // ---------------------------------------------- 63 | // Base Theme Mixin 64 | // ---------------------------------------------- 65 | @mixin apply-theme($theme) { 66 | background-color: map-get($theme, background); 67 | color: map-get($theme, text); 68 | 69 | a { 70 | color: map-get($theme, link); 71 | } 72 | a:visited { 73 | color: map-get($theme, visited-link); 74 | } 75 | } 76 | 77 | // ---------------------------------------------- 78 | // Light Mode Styles 79 | // ---------------------------------------------- 80 | body { 81 | 82 | .title-button, 83 | h1, h2, h3, h4, h5, h6 { 84 | color: map-get($light-theme, text); 85 | } 86 | } 87 | 88 | .light-theme { 89 | --text-primary: #000000; 90 | --text-secondary: #666666; 91 | --text-tertiary: #bbbbbb; 92 | --background-primary: #f5f5f5; 93 | --background-secondary: #fefefe; 94 | --background-tertiary: #f8f9fa; 95 | 96 | --primary-color: #{mat.get-color-from-palette($DSOMM-primary)}; 97 | 98 | --heatmap-filled: #4caf50; 99 | --heatmap-disabled: #dddddd; 100 | --heatmap-background: white; 101 | --heatmap-stroke: black; 102 | --heatmap-cursor-hover: #1c8b1c; 103 | --heatmap-cursor-selected:#3d3d3d; 104 | 105 | --dependency-link: #707070; 106 | --dependency-border: #222222; 107 | --dependency-mainnode-fill: #4caf50; 108 | --dependency-predecessor-fill: #deeedeff; 109 | --dependency-successor-fill: #fdfdfdff; 110 | 111 | @include mat.all-component-themes($DSOMM-light-theme); 112 | } 113 | 114 | // ---------------------------------------------- 115 | // Dark Mode Styles 116 | // ---------------------------------------------- 117 | body.dark-theme { 118 | @include apply-theme($custom-dark-theme); 119 | @include mat.all-component-themes($DSOMM-dark-theme); 120 | 121 | --text-primary: #fefefe; 122 | --text-secondary: #ababab; 123 | --text-tertiary: #999999; 124 | --background-primary: #303030; 125 | --background-secondary: #424242; 126 | --background-tertiary: #666666; 127 | 128 | --primary-color: #{mat.get-color-from-palette($DSOMM-dark-primary)}; 129 | 130 | --heatmap-filled: #007700; 131 | --heatmap-disabled: #666666; 132 | --heatmap-background: #bbbbbb; 133 | --heatmap-stroke: #000000; 134 | --heatmap-cursor-hover: #145e14; 135 | --heatmap-cursor-selected: #232323; 136 | 137 | --dependency-link: #bbbbbb; 138 | --dependency-border: #0e1b0e; 139 | --dependency-mainnode-fill: rgb(107, 190, 107); 140 | --dependency-predecessor-fill: rgb(172, 206, 172); 141 | --dependency-successor-fill: rgb(192, 192, 192); 142 | 143 | .title-button, 144 | h1, h2, h3, h4, h5, h6 { 145 | color: map-get($custom-dark-theme, text); 146 | } 147 | 148 | // General properties 149 | p, li, tr { 150 | color: #e0e0e0; 151 | } 152 | 153 | b { 154 | font-weight: 400; 155 | } 156 | 157 | // Common containers 158 | mat-card, 159 | .mat-dialog-container, 160 | .mat-expansion-panel, 161 | .mat-accordion, 162 | .overlay-wrapper { 163 | background-color: #2c2c2c; 164 | color: #e0e0e0; 165 | } 166 | 167 | // Dialog styling 168 | .mat-dialog-container { 169 | border: 1px solid #444; 170 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); 171 | } 172 | 173 | // Modal override 174 | .overlay-modal { 175 | background-color: #2c2c2c; 176 | color: #e0e0e0; 177 | border-radius: 6px; 178 | 179 | mat-card { 180 | background-color: transparent; 181 | } 182 | 183 | h1, h2, h3, h4, h5, h6 { 184 | color: #e0e0e0; 185 | } 186 | } 187 | 188 | // Circular heatmap (radar chart) 189 | .circular-heat text, 190 | .labels.segment text { 191 | fill: #ffffff; 192 | } 193 | 194 | .circular-heat line, 195 | .circular-heat path { 196 | stroke: var(--heatmap-stroke); 197 | } 198 | 199 | .mat-chip.mat-standard-chip { 200 | color: #ababab; 201 | } 202 | 203 | .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary { 204 | background-color: var(--primary-color); 205 | } 206 | } 207 | 208 | @include mat.all-component-themes($DSOMM-dark-theme); 209 | 210 | .button-container { 211 | display: flex; 212 | flex-direction: column; // Vertical alignment 213 | gap: 10px; // Space between buttons 214 | } 215 | 216 | svg .cursors path { 217 | fill: transparent; 218 | pointer-events: none; 219 | } 220 | 221 | svg .cursors #hover { 222 | stroke: var(--heatmap-cursor-hover, black); 223 | stroke-width: 6px; 224 | } 225 | 226 | svg .cursors #selected { 227 | stroke: var(--heatmap-cursor-selected, black); 228 | stroke-width: 4px; 229 | } 230 | --------------------------------------------------------------------------------