diff --git a/CHANGELOG.md b/CHANGELOG.md index 54946e0..1a6569e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to Strata. Versions follow [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [0.10.0] - 2026-10-28 ### Added - **Incident mode, with Muster** (*Incident* in the controls of an IS). Mark assets compromised, suspected or contained, from their sheet or from a playbook decision. The incident sheet then shows: - **who to call**: the contacts of the assets hit, on-call and 24/7 first, the owners of the business services they run, the IS contacts; @@ -14,7 +14,30 @@ All notable changes to Strata. Versions follow [Semantic Versioning](https://sem - **Anonymised AI prompts per asset** (*AI prompts* on an asset sheet): one prompt for the major risks of the asset, one guide that has the AI list plausible incidents and write their response playbooks in Muster's `muster/v1` YAML, with `targets`. Names, addresses, subnets, contacts and the IS name are replaced by placeholders (`ASSET-T`, `ASSET-01`, `SEG-1`, `SVC-B1`); a leak check lists anything identifying left in the text, with your own terms to never send; the answer pasted back gets your names again, in the page only. French or English. - **The connection to Muster** (`MUSTER_URL`, `MUSTER_USER`, `MUSTER_PASSWORD`). This server signs in to Muster with an analyst account and lists its responses; the incident sheet of an IS shows those linked to it (declared in Muster with this IS, or naming its assets), and the others with *Open on this IS*. Opening one marks the assets it names, then the playbook runs from the map: answering a decision or finishing a step moves the response in Muster, the tasks ticked go both ways, and what Muster's analysts do shows on the map within seconds. A decision answered anywhere marks the assets its `effects` name. Every mark and every flow cut goes to Muster's timeline, so its report keeps them, written by the Strata account with the name of the person who did it. Each incident is its own situation: opening another one on the same IS puts the marks and cuts of the previous one aside (marks made by hand before any incident stay). A decision answered in Muster marks the assets as soon as a contributor has the IS open in Strata. Muster's report gets an *Assets hit (Strata)* section. Without `MUSTER_URL`, the two example incidents stay. - **API**: `GET /api/incidents`, `PUT /api/incidents/{IS id}` (contributors and up, no proposal), `GET /api/muster`, `GET /api/muster/incidents/{id}`, `POST /api/muster/incidents/{id}/advance`, `/tasks`, `/strata`, and the `incidents` and `muster` events on `/api/events`. Kept in `incidents.json`, outside the map and its history. +- **Risk register** (*Risks*, the fourth view of an IS, next to Physical, Application and Business; and *Risks* on every asset sheet): + - **a threat landscape per asset**: 30 threats, each with its MITRE ATT&CK techniques, ENISA Threat Landscape families and EBIOS RM risk sources, apply to an asset from its type, exposure, criticality (its own or that of the business services it would hit), listening ports, system, vulnerabilities (CISA KEV, titles), role and Internet services. The sheet of a risk says why it applies. A threat can be added by hand or set aside with a justification; + - **the threat landscape of an IS**: how relevant each of the 8 risk sources is to it (0 to 3; cybercrime first by default, as in the ENISA Threat Landscape). A source rated 0 takes its threats away; + - **a risk matrix of its own for each asset**: likelihood from the threat, the exposure and reachability of the asset, its vulnerabilities (CVSS, EPSS, KEV) and its risk sources; impact from the threat and the criticality. Shown before requirements, with those applied, and once the planned ones are too; + - **requirements from baselines**: for each risk, the requirements that address it, each *applied*, *planned* or *not applicable*; each one applied removes its share of the likelihood or of the impact. Shipped: ANSSI *Guide d'hygiène informatique* (42 measures), NIST CSF 2.0 (106 subcategories), CIS Controls v8.1 (153 safeguards, numbering and paraphrased titles) and ISO/IEC 27001:2022 Annex A (93 controls, numbering and paraphrased titles); ANSSI is on by default. Import your own as JSON or CSV; + - **a decision for each risk**: treat, accept, transfer or avoid, with a justification; + - **an acceptance threshold per asset**: the matrix sets the highest acceptable level (moderate by default), which can differ by criticality (only low for critical assets, by default), and an asset sheet can set its own. A risk above the threshold of its asset, and not accepted, stays open; + - **in the 2D plan of an asset**, under its firewall rules: the requirements that address its risks, by baseline, with the risks each one reduces and its status (applied, partly applied, planned, not applied). The plans of the architecture file get it too; + - **overlapping requirements merged** (*Risks › Baselines*, in the large view): the requirements of different baselines that reduce the same threats and say much the same thing are grouped (one per baseline, from a similarity threshold you set). Merged, one requirement per group stands for the others: it takes all their threats, the others are no longer offered, and a status given to any of them still counts. The one kept is chosen by a criterion (*most demanding*, *widest coverage*, *best fit for this map*: the threats found on its assets, *preferred baseline*: in the order you give the baselines), or by hand; a group can be kept separate. Baselines are turned on and ordered there too; + - **logs for the SIEM** (*Risks › Logs for the SIEM*, and on each asset sheet): the minimum each asset must send so that its risks can be seen happening. Only the log sources it has (Windows Security and PowerShell, domain controller, EDR or Sysmon, Linux auth and auditd, firewall, VPN, web and WAF, database audit, hypervisor, backup, mail, network devices, OT, devices, DNS and proxy, availability, physical access, log source health), only the events of the threats that apply to it (Windows event IDs, Sysmon events, auditd rules…), at the verbosity its residual risk calls for: minimal, standard or detailed, with the setting to apply. Retention: 6 months, a year where a high or critical risk remains. The SIEM onboarding plan exports as CSV; + - **the register of an IS**: two heatmaps (before and with the requirements; click a cell to list its risks), the table of every risk, and a CSV export; + - **risk settings** shared by every IS (*Risks › Settings*): the matrix (3 to 5 levels each way, labels, the level of each cell, the acceptance threshold), the baselines in use, the threat catalogue (disable a threat, add your own), export and import as JSON. Saved like the rest of the map: in the history, through proposals for contributors, in exports and imports. +- **Dashboard and KPIs** (*Steering › Dashboard and KPIs*, from the overview or an IS), ready for a steering committee: + - **seven key figures**, each with its value, its change over the period said as better or worse, and its trend: Internet-facing assets (and ports), assets with an administration port open to the Internet, open critical and high vulnerabilities (and those on the CISA KEV list), their median age, vulnerabilities past their remediation deadline (critical 15 days, high 30, medium 90, low 180), owner coverage (assets with a named technical or business owner, and critical assets), scan coverage (assets a vulnerability scanner knows); + - **four charts over time**: exposure, the age of open critical and high vulnerabilities (under 30 days to over 180), owner coverage, scan coverage; over 12 weeks, 6 months or 12 months, for the whole map or one IS; + - **the IS side by side**, and **the ten oldest open critical and high vulnerabilities** with how late they are; + - **measured from the history of the map**: the server rebuilds the map at the end of each period, and dates each vulnerability from the first version where it appears (or the date its scanner gives). A map kept in a browser keeps one snapshot a day; the demo comes with a year of past, rebuilt from the dates its vulnerabilities were found; + - *Print or PDF* prints the dashboard alone; *Export (CSV)* gives every figure, every period, every IS. +- **Selecting an asset zooms on it and highlights it**: a click on an asset, in 3D or in the 2D diagram, or a search, brings the view close to it; the asset glows in the accent colour with a breathing ring under it (a pulsing halo in 2D), and the assets that have nothing to do with it fade. Closing its sheet, or a click on an empty space, zooms back out to the view from before. +- **One VLAN alone**: a click on a plate, in 3D or in the 2D diagram, shows that VLAN alone: the other plates and their assets disappear, with the flows to them, and the view zooms in on it. *Show every VLAN*, Escape, a click on an empty space or a second click on the plate brings back the VLANs shown before, and the view zooms back out to where it was. +- **API**: `GET /api/kpi` (the key figures over time), `PUT /api/risk` (approvers), `risk` in `GET /api/state`, the export, the import and proposals; `landscape` on IS documents, `risk` on asset documents. - **One-page build**: `npm run build:online` writes `dist-online/strata.html`, the whole interface without a server (map kept in the browser, three.js from jsDelivr). +### Changed +- The 3D model is no longer drawn while a lens (Application, Business, Risks) or the 2D diagram covers it: those views stay smooth on modest machines. ## [0.9.0] - 2026-10-26 ### Added @@ -195,6 +218,7 @@ All notable changes to Strata. Versions follow [Semantic Versioning](https://sem ## [0.1.0] - First release: 3D map of IS, zones, VLANs, assets, flows, exposure, vulnerabilities and incident contacts. +[0.10.0]: https://github.com/SolidGeeks/strata/releases/tag/v0.10.0 [0.9.0]: https://github.com/SolidGeeks/strata/releases/tag/v0.9.0 [0.8.0]: https://github.com/SolidGeeks/strata/releases/tag/v0.8.0 [0.7.0]: https://github.com/SolidGeeks/strata/releases/tag/v0.7.0 diff --git a/README.md b/README.md index cba8c7f..4a8064f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Strata — v0.9 +# Strata — v0.10 [![Release](https://img.shields.io/github/v/release/SolidGeeks/strata)](https://github.com/SolidGeeks/strata/releases) [![License AGPL-3.0](https://raw.githubusercontent.com/SolidGeeks/.github/main/assets/badges/license-agpl.svg)](LICENSE) [![Signed releases](https://raw.githubusercontent.com/SolidGeeks/.github/main/assets/badges/signed-releases.svg)](#2-quick-start-with-docker) [![Tests](https://github.com/SolidGeeks/strata/actions/workflows/ci.yml/badge.svg)](https://github.com/SolidGeeks/strata/actions/workflows/ci.yml) @@ -15,10 +15,19 @@ Strata is on its way to **1.0.0**: from then on, the API, the export format, the | [0.7.0](https://github.com/SolidGeeks/strata/milestone/1) ✅ | Hardening and tests: limit failed sign-ins, a security review, a browser test of the interface. Released on 2026-10-26 | | [0.8.0](https://github.com/SolidGeeks/strata/milestone/2) ✅ | Connector modules and a module store, three.js 0.186. Released on 2026-10-26 | | [0.9.0](https://github.com/SolidGeeks/strata/milestone/3) ✅ | The last breaking changes: single sign-on, pre-releases, an API reference, the old product name removed. Released on 2026-10-26 | +| 0.10.0 ✅ | Incident mode with Muster, the risk register with baselines, logs for the SIEM, the steering dashboard. Released on 2026-10-28 | | [1.0.0](https://github.com/SolidGeeks/strata/milestone/4) | Release candidates (`1.0.0-rc.N`), then 1.0.0 | Follow it on the [milestones page](https://github.com/SolidGeeks/strata/milestones). Ideas for after 1.0 are discussed, and voted on, in [Discussions](https://github.com/orgs/SolidGeeks/discussions/categories/ideas). Want to help? Start with a [good first issue](https://github.com/SolidGeeks/strata/issues?q=is%3Aopen+label%3A%22good+first+issue%22). +## New in v0.10: incident mode with Muster, the risk register, the steering dashboard + +- **Incident mode, with Muster** (*Incident* in the controls of an IS): mark assets compromised, suspected or contained; the incident sheet lists who to call, the flows to cut and the firewall where to cut each, the business services and assets that depend on them, what the attacker reaches next. A Muster incident opens on the map and its playbook decisions point at the assets. Each asset also has anonymised *AI prompts*: its major risks, and a guide for an AI to write the response playbooks of its plausible incidents in Muster's format. +- **Risk register** (the *Risks* view of an IS, and *Risks* on each asset sheet): each asset gets the threats that fit it, from its exposure, criticality, ports, system and vulnerabilities and from the threat landscape of its IS (EBIOS RM risk sources, ENISA Threat Landscape, MITRE ATT&CK). Each risk sits on a risk matrix of its own, 3×3 to 5×5 and configurable, before and after the requirements applied. For each risk, apply, plan or set aside the requirements of the ANSSI hygiene guide, NIST CSF 2.0, CIS Controls v8.1, ISO 27001 Annex A or your own baselines, or accept, transfer or avoid the risk; requirements that overlap between baselines merge into the one a criterion (most demanding, widest coverage, best fit, preferred baseline) or you choose. For each asset, the minimum logs to send to a SIEM and their verbosity. The register and the SIEM plan export as CSV. +- **Dashboard and KPIs** (*Steering › Dashboard and KPIs*): exposure to the Internet, the age of open vulnerabilities against remediation deadlines, owner coverage and scan coverage, over 12 weeks, 6 months or 12 months, for the whole map or one IS, rebuilt from the history of the map. Each figure with its trend and whether it got better; the IS side by side; the oldest open vulnerabilities. Prints to PDF for a steering committee, exports as CSV. +- **On the map**: a click on a plate shows its VLAN alone; selecting an asset zooms on it and highlights it; a click on an empty space zooms back out. The 2D plan of an asset lists the requirements against its risks under its firewall rules. +- **One-page build** (`npm run build:online`): the whole interface without a server, the map kept in the browser. + ## New in v0.9: single sign-on, API tokens, an API reference - **Single sign-on** with OpenID Connect (Entra ID, Keycloak, Authentik, Google Workspace, Okta…) or a trusted authenticating proxy, next to the local accounts; roles come from the provider's groups, and `LOCAL_LOGIN` keeps local accounts for everyone, for admins only (break-glass) or for no one. See [section 4](#4-single-sign-on-and-api-tokens). @@ -43,7 +52,6 @@ Connectors are now **modules**: a folder with a manifest (`strata-module.json`: - **Attack paths** (*Analysis*): which critical assets an attacker could reach from the Internet, every path to each one along the allowed flows, most likely first, and **where to cut** (the assets to harden and the equipment whose rules to tighten, that every path to a critical asset goes through). Hover a path to pick it out on the model, in 3D or in 2D. - **Blast radius**: pick an asset (in the panel, or *Blast radius* on its sheet) and see everything an attacker holding it could reach, the most likely way to each asset, and the business services hit. - A step counts for the highest of: the vulnerabilities of the asset taken over (as for the risk score), 50% on a remote administration port (SSH, RDP, SMB, WinRM, VNC, Telnet, RPC), 10% on any other service; a path for the product of its steps. Both lists export as CSV. -- **Incident mode, with Muster** (*Incident* in the controls of an IS): mark assets compromised, suspected or contained; the incident sheet lists who to call, the flows to cut and the firewall where to cut each, the business services and assets that depend on them, what the attacker reaches next. A Muster incident opens on the map and its playbook decisions point at the assets. Each asset also has anonymised *AI prompts*: its major risks, and a guide for an AI to write the response playbooks of its plausible incidents in Muster's format. - **Architecture file**: one document per IS (*Analysis › Architecture file*, or the IS summary), in the shape audits and security accreditation expect: cover and sign-off table, summary, scope, network diagram, zones and VLANs, applications, asset inventory, flow matrix, the rules each firewall needs, exposure, vulnerabilities and remediation order, attack paths, plans of the exposed and critical assets, incident contacts, change history and method. *Download* saves it as one HTML file, with no outside resource, that prints to A4 PDF from the browser. - **Nix and NixOS**: the repository is a flake, with a package and a hardened `services.strata` module (section 6, *Nix and NixOS*). - **Failed sign-ins are limited** per account and per client address (`429`), and passwords can be read from files (`EDIT_PASS_FILE`, `VIEW_PASS_FILE`): section 3. @@ -186,7 +194,7 @@ Or use the prebuilt image published with each release (tag `vX.Y.Z`, see the `Re Every published image is **signed**: an attestation proves it was built by this repository's `Release` workflow, from the code of the tag. To check it before deploying (you need the [GitHub CLI](https://cli.github.com/)): ```bash -gh attestation verify oci://ghcr.io/solidgeeks/strata:0.9.0 --owner SolidGeeks +gh attestation verify oci://ghcr.io/solidgeeks/strata:0.10.0 --owner SolidGeeks ``` The command must print `Verification succeeded!`. If it fails, do not use the image, and report it (see [SECURITY.md](SECURITY.md)). Images older than 0.5.4 are not signed. @@ -454,20 +462,22 @@ Every route needs the same credentials as the interface (a local account with Ba | Method and route | What it does | |---|---| -| `GET /api/state` | The whole map: `{si, assets, wan, services, rev, updatedAt}` | +| `GET /api/state` | The whole map: `{si, assets, wan, services, risk, rev, updatedAt}` | | `GET /api/export` | The same content, as a file to download | | `PUT /api/si/{id}` | Creates or replaces an IS (zones, VLANs, flows, contacts) | | `DELETE /api/si/{id}` | Deletes an IS (not its assets) | | `PUT /api/assets/{id}` | Creates or replaces an asset | | `DELETE /api/assets/{id}` | Deletes an asset | | `PUT /api/wan` | Replaces the links between IS: `{"links": [...]}` | -| `POST /api/import` | Replaces the whole map: `{si, assets, wan, services}` | +| `POST /api/import` | Replaces the whole map: `{si, assets, wan, services, risk}` | +| `GET /api/kpi` | The key figures of the dashboard at the end of each period (`?points=13&step=7`), rebuilt from the history | | `GET /api/incidents` | Incident mode of every IS: `{incidents: {: {on, marks, cuts, run, log}}}` | | `GET /api/muster` | Muster's incidents and whether Muster answers (`{url, ok, error, incidents}`), when `MUSTER_URL` is set | | `GET /api/muster/incidents/{id}` | One Muster incident: its frozen playbook, the steps taken, the link to Strata | | `POST /api/muster/incidents/{id}/advance`, `/tasks`, `/strata` | Passed on to Muster (`contributor` and up): answer a step `{edgeId}`, tick a task `{nodeId, index, done}`, report the assets and what was done `{site, marks, names, log}` | | `PUT /api/incidents/{IS id}` | Replaces the incident of an IS (`contributor` and up, no proposal): marks are `compromised`, `suspected` or `contained` | | `PUT /api/services` | Replaces the common catalogue of Internet services: `{"services": [...]}` | +| `PUT /api/risk` | Replaces the risk settings shared by every IS: `{"risk": {matrix, baselines, custom, threats}}` | | `GET /api/history` | Latest changes (`?before=REV&limit=N`); `GET /api/history/{rev}`: the before/after details | | `GET /api/state?at=REV` | The map as it was right after version REV | | `POST /api/rollback` | `{"rev": N}`: puts the map back to version N (approver) | @@ -564,7 +574,7 @@ This map describes your weak spots precisely: treat it as sensitive data. --- -## 13. Limits of v0.9 +## 13. Limits of v0.10 - Storage is a single JSON file, suited to a few thousand assets. Beyond that, a real database will be needed. - Sessions of single sign-on are kept in memory: restarting Strata signs SSO users out. Roles come from the groups at sign-in: a group change applies at the next sign-in (`SESSION_HOURS`, 8 by default), or at once with *End all single sign-on sessions*. LDAP is not read directly: use an OpenID Connect provider in front of it (Keycloak, Authentik, Entra ID). @@ -578,6 +588,8 @@ This map describes your weak spots precisely: treat it as sensitive data. - GLPI: one call per asset (plus one per technician and per unknown software, cached); allow a few minutes for several thousand assets. The new GLPI 11 API (`/api.php/v2`) is not supported yet. - NetFlow is not read yet: a module could do it ([MODULES.md](MODULES.md)). NetBox and Nmap come as modules from the store. - A connector module's process is kept away from the data files and from the server's settings, but not from the network; the Node.js permission model it relies on guards against mistakes, not against code written to escape it. Review a module before installing it outside the store. +- The risk register, the requirements linked to each threat (and the share of risk each removes), the grouping of overlapping requirements and the catalogue of SIEM logs are Strata's own judgement, not an official mapping of ANSSI, NIST, CIS or ISO: review them with your compliance and SOC teams. CIS Controls and ISO 27001 come as numbering and paraphrased titles only. +- On a server, the dashboard goes back as far as the kept history (the last `HISTORY_KEEP` changes, 5000 by default); in a map kept in a browser, as far as its daily snapshots. - Business dependencies follow the recorded flows: a dependency with no flow on the map (a licence server reached through a proxy, a shared storage mounted by hand) is not seen. Log and monitoring flows (syslog, SNMP traps, NetFlow) are left out on purpose. - Attack paths follow the recorded flows only: a flow missing from the map is a path Strata does not see, and a flow recorded but blocked in practice is a path it shows. The likelihoods are a way to rank paths, not a measured probability. Paths below 0.1% or longer than 8 steps are left out of the lists (the most likely way to each asset is always given). - The risk score uses the EPSS and KEV values recorded on each vulnerability: Strata does not fetch the FIRST and CISA feeds, so keep them up to date through the Cyberwatch connector or an import. A vulnerability is not tied to a port, so any open flow to its asset counts as a way to reach it. @@ -598,7 +610,7 @@ A pull request must start from the latest release or later: the `base` job fails ### Releasing -1. Merge a pull request that sets the new version in `package.json` (`npm version 0.9.0 --no-git-tag-version`), `docker-compose.yml`, the interface and the docs, and dates its CHANGELOG section. +1. Merge a pull request that sets the new version in `package.json` (`npm version 0.10.0 --no-git-tag-version`), `docker-compose.yml`, the interface and the docs, and dates its CHANGELOG section. 2. *Actions › Release › Run workflow*, on `main`. The workflow reads the version from `package.json` and creates the `vX.Y.Z` tag itself, so there is nothing to type. It waits for a maintainer's approval (the `release` environment), then runs the tests, publishes the signed image and the GitHub release with its notes. Pushing a `vX.Y.Z` tag, or publishing a release from the web page with a new tag on `main`, still works: the tag must then be exactly `v` and the version (`v0.9.0`, not `V0.9.0` or `v.0.9.0`). diff --git a/SECURITY.md b/SECURITY.md index f590294..f0ec469 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,8 @@ The full process (response within 3 working days, coordinated disclosure, credit | Version | Supported | |---|---| -| 0.9.x | ✅ | +| 0.10.x | ✅ | +| 0.9.x | ❌ | | 0.8.x | ❌ | | 0.7.x | ❌ | | 0.6.x | ❌ | diff --git a/docker-compose.yml b/docker-compose.yml index 42fe04d..965252f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: strata: # Prebuilt image from GitHub (docker compose pull), or built here (docker compose up -d --build) - image: ghcr.io/solidgeeks/strata:0.9.0 + image: ghcr.io/solidgeeks/strata:0.10.0 build: . container_name: strata restart: unless-stopped diff --git a/docs/api.html b/docs/api.html index 4310a6e..62fe7fc 100644 --- a/docs/api.html +++ b/docs/api.html @@ -109,13 +109,14 @@

Account

The map

-

The map is four parts: the IS documents (si, by id), the assets (by id), the links between IS (wan) and the shared Internet services (services). Their content is in Data format. Every change makes a new revision and an entry in the history.

+

The map is four parts: the IS documents (si, by id), the assets (by id), the links between IS (wan), the shared Internet services (services) and the risk settings (risk). Their content is in Data format. Every change makes a new revision and an entry in the history.

- + - + + @@ -123,7 +124,8 @@

The map

- + +
RouteRoleRequest → answer
GET /api/stateviewer→ {si, assets, wan, services, rev, updatedAt}
GET /api/stateviewer→ {si, assets, wan, services, risk, rev, updatedAt}
GET /api/state?at=REVviewer→ the same, as it was right after revision REV
GET /api/exportviewer→ an export file (Content-Disposition: attachment): {format: "strata", version: 3, exportedAt, si, assets, wan, services}
GET /api/kpi?points=N&step=DAYSviewer→ {step, since, sla, points: [{at, rev, total, sites: {id: …}}]}: the key figures of the dashboard, measured on the map as it was at the end of each period (points 2 to 60, 13 by default; step 1 to 92 days, 7 by default), rebuilt from the history; periods before the history starts are left out. Each measure: assets, exposure {assets, share, ports, admin, risky}, vulns {open, crit, high, med, low, kev, median, ages, late, lateSerious, approx}, owners {covered, share, critical, criticalCovered}, scans {covered, share}, and on the last point oldest, the ten oldest open critical and high vulnerabilities
GET /api/exportviewer→ an export file (Content-Disposition: attachment): {format: "strata", version: 3, exportedAt, si, assets, wan, services, risk}
GET /api/si/{id}, GET /api/assets/{id}viewer→ the document, or 404
PUT /api/si/{id}approverthe IS document → {ok, rev}. Creates or replaces it whole.
DELETE /api/si/{id}approver→ {ok, rev}. Its assets stay.
DELETE /api/assets/{id}approver→ {ok, rev}
PUT /api/wanapprover{links: [...]} → {ok, rev}. Replaces all the links.
PUT /api/servicesapprover{services: [...]} → {ok, rev}. Replaces the shared catalogue.
POST /api/importapprover{si, assets, wan?, services?} (an export file works as is) → {ok, rev}. Replaces the whole map; a backup is taken first.
PUT /api/riskapprover{risk: {...}} → {ok, rev}. Replaces the risk settings shared by every IS (see Data format).
POST /api/importapprover{si, assets, wan?, services?, risk?} (an export file works as is) → {ok, rev}. Replaces the whole map; a backup is taken first.

Documents are replaced whole: to change one field, read the document, change it, send it back. To add a flow or a VLAN, change the IS document (si.flows, si.vlans).

@@ -138,14 +140,14 @@

History

- +
RouteRoleRequest → answer
GET /api/history?before=REV&limit=Nviewer→ {rev, oldest, entries}, newest first, older than before if given; limit 120 by default, 500 at most. Each entry: {rev, at, user, batch, label, kind, id, op, name, site}; kind: si, assets, wan, services or all; op: created, updated or deleted. oldest: the oldest revision the history still holds.
GET /api/history?before=REV&limit=Nviewer→ {rev, oldest, entries}, newest first, older than before if given; limit 120 by default, 500 at most. Each entry: {rev, at, user, batch, label, kind, id, op, name, site}; kind: si, assets, wan, services, risk or all; op: created, updated or deleted. oldest: the oldest revision the history still holds.
GET /api/history/{rev}viewer→ {rev, entries}: the changes of that revision, each with its before and after document
POST /api/rollbackapprover{rev} → {ok, rev}. Puts the map back as it was at rev, as a new revision (nothing is erased); a backup is taken first.

Proposals

-

A proposal is a set of changes that an approver merges into the map, or not. Its changes are {si: {id: doc}, assets: {id: doc}, wan?: [...], services?: [...]}, where a document set to null deletes it. Its status goes draft → submitted → approved or rejected; a submitted proposal can be withdrawn back to draft, and editing a rejected one makes it a draft again.

+

A proposal is a set of changes that an approver merges into the map, or not. Its changes are {si: {id: doc}, assets: {id: doc}, wan?: [...], services?: [...], risk?: {...}}, where a document set to null deletes it. Its status goes draft → submitted → approved or rejected; a submitted proposal can be withdrawn back to draft, and editing a rejected one makes it a draft again.

@@ -239,7 +241,8 @@

Data format

"si": { "<IS id>": { IS document } }, "assets": { "<asset id>": { asset document } }, "wan": [ { link } ], - "services": [ { Internet service } ] + "services": [ { Internet service } ], + "risk": { risk settings } }

The export of a single IS (Export this IS (JSON)) adds "scope": "si" and holds that IS, its assets and the shared services. The interface imports it by replacing that IS only; POST /api/import always replaces the whole map, so to load one IS from a script, send its documents with PUT /api/si/{id} and PUT /api/assets/{id}.

@@ -255,6 +258,7 @@

IS document

+
RouteRoleRequest → answer
contacts[{name, role, phone, email, dispo, src?}], who to call for the IS
business[{id, name, crit, owner, notes, assets}]: business services and the assets they run on
servicesInternet services used by this IS only, as below
landscape{actors: {id: 0-3}}: how relevant each risk source is to this IS (crime-gain, amateur, activist, state-spy, state-prepo, competitor, insider, supplier); missing: the default
@@ -271,6 +275,20 @@

Asset document

apps[{name, version, ports, src?}], software and the ports it listens on contactsAs for the IS extIds in connected tools: {"glpi": "Computer/12", "cyberwatch": 101} + scannedAtWhen a vulnerability scanner last saw it (ISO date): counts for scan coverage for 30 days, as does an id in ext from a scanner (cyberwatch, tenable, qualys…) or a vulnerability with a src + riskThe treatment of its risks: {threats: {threatId: {reqs: {"baselineId/reqId": "applied"|"planned"|"na"}, decision?, note?}}, excluded?: [threatIds], added?: [threatIds], accept?: 0-3}; accept: an acceptance threshold for this asset only; decision: accept, transfer or avoid (missing: treat) + + + +

Risk settings

+
+ + + + + + +
FieldContent
matrix{likelihood: [labels], impact: [labels], cells: [[0-3]], accept: 0-3, lEdges?, iEdges?}: 3 to 5 levels each way, lowest first; cells[l][i]: low, moderate, high or critical; accept: the highest level acceptable without a decision; acceptByCrit: {1-3: 0-3}, the same for assets of that criticality (their own, or that of the business services they would hit), by default {3: 0}. Missing or out of shape: the 4×4 default
baselinesIds of the enabled baselines: anssi, nist-csf, cis-v8, iso27001, custom:…. Missing: ANSSI and every custom one
customCustom baselines: [{id, name, version?, requirements: [{id, ref?, title, text?, threats: [[threatId, share 0.05-0.9, "likelihood"|"impact"]]}]}]
merge{on, criterion, threshold, priority, choices, separate}: merging of overlapping requirements. criterion: strict, coverage, fit or baseline; threshold: how alike two requirements must be, 0.3 to 0.8 (0.45); priority: baseline ids, preferred first; choices: {group key: kept requirement key} chosen by hand; separate: group keys left unmerged. A group key is its requirement keys, sorted, joined with +
threats{custom: [{id, name, desc, base 1-4, severity 1-4, when, attack?, actors?}], disabled: [threatIds]}. when: types, exposure, critMin, ports, kev, vulnTitle, role, os (patterns), outbound, svcCat; all must hold, or one with any: true

src on a vulnerability, application or contact names the connector it came from; entries without one were typed by hand.

@@ -325,7 +343,7 @@

Deprecation

Solid Geeks: open source the world.
-

Strata 0.9.0 · AGPL-3.0 · github.com/SolidGeeks/strata · Changelog

+

Strata 0.10.0 · AGPL-3.0 · github.com/SolidGeeks/strata · Changelog

Cyberwatch, GLPI, phpIPAM and Palo Alto Networks are trademarks of their owners; Strata is not affiliated with them.

diff --git a/docs/docs.html b/docs/docs.html index cd182bb..0d7c81e 100644 --- a/docs/docs.html +++ b/docs/docs.html @@ -69,7 +69,7 @@

Install with Docker

nano .env # set EDIT_PASS docker compose up -d --build # builds the image here

On Windows: copy .env.example .env then notepad .env.

-

Or skip the build: docker compose pull && docker compose up -d fetches the ready-made image ghcr.io/solidgeeks/strata of the release. Every image is signed; check it before deploying with gh attestation verify oci://ghcr.io/solidgeeks/strata:0.9.0 --owner SolidGeeks, which must print Verification succeeded!

+

Or skip the build: docker compose pull && docker compose up -d fetches the ready-made image ghcr.io/solidgeeks/strata of the release. Every image is signed; check it before deploying with gh attestation verify oci://ghcr.io/solidgeeks/strata:0.10.0 --owner SolidGeeks, which must print Verification succeeded!

Open http://localhost:8080 and sign in with EDIT_USER / EDIT_PASS. The map starts empty: Load demo (5 IS) fills it with five sample sites, Delete everything clears it.

docker compose logs -f strata     # logs
 docker compose restart strata     # restart
@@ -395,7 +395,7 @@ 

Known limits

Solid Geeks: open source the world.
-

Strata 0.9.0 · AGPL-3.0 · github.com/SolidGeeks/strata · Changelog

+

Strata 0.10.0 · AGPL-3.0 · github.com/SolidGeeks/strata · Changelog

Cyberwatch, GLPI, phpIPAM and Palo Alto Networks are trademarks of their owners; Strata is not affiliated with them.

diff --git a/docs/index.html b/docs/index.html index 863f216..53b75e0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -123,7 +123,7 @@

From the network to the business

What to fix first

One risk score per asset, from CVSS, EPSS, the CISA list of known exploited vulnerabilities, exposure, reachability from the Internet and criticality. The result is a remediation list in the right order, exported as CSV, and a “Why?” on every asset that shows how its score was made.

-
  • EPSS and KEV from the Cyberwatch connector
  • Works offline: no feed to download
  • Attack paths from the Internet to the critical assets, and where to cut them
  • Blast radius: everything an attacker holding one asset could reach
+
  • EPSS and KEV from the Cyberwatch connector
  • Works offline: no feed to download
  • Attack paths from the Internet to the critical assets, and where to cut them
  • Blast radius: everything an attacker holding one asset could reach
  • A risk register: the threats of each asset on a risk matrix, and the requirements of ANSSI, NIST CSF 2.0, CIS Controls v8.1, ISO 27001 or your own baselines against them
  • The logs each asset must send to a SIEM, at the right verbosity
  • A dashboard for the steering committee: exposure, vulnerability age, owner and scan coverage over time
Risk priorities of the Lyon site: the fixes in order, each with its KEV, EPSS, CVSS and exposure
Risk priorities: a firewall flaw known to be exploited comes first.
@@ -212,7 +212,7 @@

Your map stays Solid Geeks: open source the world.
-

Strata 0.9.0 · AGPL-3.0 · github.com/SolidGeeks/strata

+

Strata 0.10.0 · AGPL-3.0 · github.com/SolidGeeks/strata

Cyberwatch, GLPI, phpIPAM and Palo Alto Networks are trademarks of their owners; Strata is not affiliated with them.

diff --git a/nix/package.nix b/nix/package.nix index b92be9a..a484d71 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -35,7 +35,7 @@ buildNpmPackage { nodejs = nodejs_22; # Changes with package-lock.json: Nix prints the new value when it no longer matches # (README, "Nix and NixOS") - npmDepsHash = "sha256-h4/8dfOkJs1JsiYV2pCoCsOAV060CGi57B1mkfZ6Cn4="; + npmDepsHash = "sha256-fr4NO/VIuBQB9rc5TnwisLIvPWuTDnobwmiWin87+4A="; npmBuildScript = "vendor"; diff --git a/package-lock.json b/package-lock.json index 0fd2ede..cdc9ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "strata", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "strata", - "version": "0.9.0", + "version": "0.10.0", "license": "AGPL-3.0-or-later", "dependencies": { "@fontsource/archivo": "^5.0.0", diff --git a/package.json b/package.json index de9dbf5..1352fc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strata", - "version": "0.9.0", + "version": "0.10.0", "description": "Strata — self-hosted 3D map of information systems: zones, VLANs, assets, flows, exposure, vulnerabilities and incident contacts.", "private": true, "license": "AGPL-3.0-or-later", @@ -12,7 +12,7 @@ "scripts": { "vendor": "node scripts/vendor.mjs", "start": "node server/server.js", - "test": "node --test tests/api.test.mjs tests/analysis.test.mjs tests/incident.test.mjs tests/muster.test.mjs tests/secrets.test.mjs tests/signin.test.mjs tests/hardening.test.mjs tests/readonly.test.mjs tests/modules.test.mjs tests/sso.test.mjs", + "test": "node --test tests/api.test.mjs tests/analysis.test.mjs tests/incident.test.mjs tests/risk.test.mjs tests/kpi.test.mjs tests/muster.test.mjs tests/secrets.test.mjs tests/signin.test.mjs tests/hardening.test.mjs tests/readonly.test.mjs tests/modules.test.mjs tests/sso.test.mjs", "test:e2e": "node --test tests/e2e.mjs", "setup": "npm ci && npm run vendor", "build:online": "node scripts/build-online.mjs" diff --git a/public/app.css b/public/app.css index 3c9b40f..ccf9818 100644 --- a/public/app.css +++ b/public/app.css @@ -103,7 +103,7 @@ body.readonly .edit-only{display:none!important} .seg.three{grid-template-columns:repeat(3,1fr)} .seg.four{grid-template-columns:repeat(4,1fr)} .seg button{background:none;border:0;padding:8px 0;font-size:11px;color:var(--muted);border-right:1px solid var(--line);min-height:34px} -.seg.two button,.seg.three button{font-size:12px} +.seg.two button,.seg.three button,.seg.four button{font-size:12px} .seg button:last-child{border-right:0} .seg button[aria-pressed="true"]{background:var(--fg);color:var(--panel)} .row{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:4px 0} @@ -560,3 +560,153 @@ button.chip:hover{border-color:var(--accent);color:var(--accent)} .ai-p pre{margin:0;border-radius:0} .ai-map{border-collapse:collapse;width:100%;font-size:12px} .ai-map td{border-top:1px solid var(--line);padding:3px 6px} + +/* Risk register */ +.rmx-pair{display:flex;flex-wrap:wrap;gap:18px;margin:10px 0} +.rmx-f{margin:0} +.rmx-f figcaption{margin-bottom:6px} +.rmx{border-collapse:separate;border-spacing:3px;font-size:11px} +.rmx th{font-weight:500;color:var(--muted);text-align:right;padding:0 6px 0 0;max-width:90px;line-height:1.2} +.rmx th[scope=col]{text-align:center;padding:4px 0 0;width:44px} +.rmx td{width:44px;height:30px;text-align:center;font-family:var(--f-mono);font-weight:600;border-radius:3px;color:var(--fg)} +.rmx td.lv-low{background:color-mix(in srgb,var(--low) 30%,transparent)} +.rmx td.lv-med{background:color-mix(in srgb,var(--med) 34%,transparent)} +.rmx td.lv-high{background:color-mix(in srgb,var(--high) 38%,transparent)} +.rmx td.lv-crit{background:color-mix(in srgb,var(--crit) 42%,transparent)} +.rmx td[data-cell]{cursor:pointer} +.rmx td[data-cell]:hover,.rmx td.on{outline:2px solid var(--accent);outline-offset:-2px} +.rmx-ax{display:flex;justify-content:space-between;margin:2px 0 0;font-size:10.5px;color:var(--muted)} +.rmx.edit td{width:auto;min-width:46px;height:30px;padding:0} +.rmx.edit td button{width:100%;height:100%;border:0;background:none;font:inherit;font-size:10.5px;color:inherit;cursor:pointer} +.rmx.edit input{width:76px;font:inherit;font-size:11px;padding:2px 4px;border:1px solid var(--line);border-radius:3px;background:var(--panel);color:var(--fg)} +.rmx.edit th[scope=col] input{width:100%;min-width:0;box-sizing:border-box} +#det .rmx td{width:34px;height:22px} +#det .rmx th{font-size:9.5px;max-width:70px} +#det .rmx th[scope=col]{width:34px;overflow-wrap:normal} +#det .rmx-pair{gap:10px} +#det .rmx.edit td{width:auto} +#det .rmx.edit th{max-width:none} +li.rk{display:flex;align-items:center;gap:10px} +li.rk>button{flex:1;min-width:0;text-align:left} +li.rk>button b,li.rk>button small{display:block} +li.rk>button small{margin-top:2px;font-weight:400} +.rk-lv{display:flex;align-items:center;gap:4px;flex-shrink:0} +.rk-lv .ar{color:var(--muted)} +.rk-ex,.rk-why,.rs-new{margin:10px 0;font-size:12.5px} +.rk-ex summary,.rk-why summary,.rs-new summary{cursor:pointer;color:var(--muted)} +.rk-add{margin-top:10px} +.rk-b{margin:14px 0 4px;font-size:12px;font-weight:600} +li.req{display:flex;align-items:flex-start;gap:10px;font-size:12.5px;padding:6px 0;border-bottom:1px solid var(--line)} +li.req>div{flex:1;min-width:0} +li.req small{display:block;color:var(--muted);margin-top:2px} +li.req select{flex-shrink:0;max-width:130px} +li.req.st-applied{box-shadow:inset 3px 0 0 var(--low);padding-left:8px} +li.req.st-planned{box-shadow:inset 3px 0 0 var(--med);padding-left:8px} +li.req.st-na{opacity:.6} +.rk-tools{flex-direction:row;flex-wrap:wrap} +.rk-tools .btn{width:auto} +.rk-tab{border-collapse:collapse;width:100%;max-width:1100px;font-size:12.5px;margin-top:8px} +.rk-tab th{text-align:left;font-weight:500;color:var(--muted);padding:6px 8px;border-bottom:1px solid var(--line)} +.rk-tab td{padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top} +.rk-tab td small{display:block;color:var(--muted)} +.rk-tab tbody tr{cursor:pointer} +.rk-tab tbody tr:hover,.rk-tab tbody tr:focus{background:color-mix(in srgb,var(--accent) 8%,transparent);outline:none} +.rk-tab tr.open td:first-child{box-shadow:inset 3px 0 0 var(--crit)} +.rk-src li,.rs-bl li,.rs-th li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:12.5px} +.rk-src li>div,.rs-bl li>label,.rs-th li>label{flex:1;min-width:0} +.rk-src small,.rs-bl small,.rs-th small{display:block;color:var(--muted)} +.rs-bl label,.rs-th label{align-items:flex-start} +.rs-dims{display:flex;flex-wrap:wrap;gap:12px;font-size:12.5px;margin:6px 0} +.rs-mx{overflow-x:auto} +@media (max-width:760px){.rk-tab th:nth-child(5),.rk-tab td:nth-child(5),.rk-tab th:nth-child(7),.rk-tab td:nth-child(7){display:none}} +.rk-acc{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin:6px 0 4px;font-size:12.5px} +.rk-acc select{max-width:210px} + +/* Baselines and overlapping requirements */ +.rk-tabs{max-width:460px;margin-bottom:18px} +.blcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px;max-width:1100px;margin:8px 0 18px} +.blcard{border:1px solid var(--line);border-radius:4px;padding:10px 12px;background:var(--panel);font-size:12.5px} +.blcard.on{border-color:var(--accent)} +.blcard small{display:block;color:var(--muted);margin:4px 0 0 24px} +.blord{display:flex;align-items:center;gap:6px;margin:8px 0 0 24px} +.ovctl{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px;font-size:12.5px;max-width:1100px} +.ovflt{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0;font-size:12.5px} +.ovflt input[type=search]{min-width:220px;padding:5px 8px;border:1px solid var(--line);border-radius:3px;background:var(--panel);color:var(--fg);font:inherit} +.ovgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(440px,1fr));gap:12px;max-width:1400px} +.ovg{border:1px solid var(--line);border-radius:4px;background:var(--panel);padding:10px 12px;font-size:12.5px} +.ovg.sep{opacity:.75;border-style:dashed} +.ovg header{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-bottom:6px} +.ovg header label{margin-left:auto;font-size:12px} +.ovg-th{display:flex;flex-wrap:wrap;gap:4px} +.ovg-th i{font-style:normal;font-size:10.5px;padding:1px 6px;border-radius:9px;background:var(--panel-2);color:var(--muted)} +.ovg ul{list-style:none;margin:0;padding:0} +.ovg li{padding:6px 0;border-top:1px solid var(--line)} +.ovg li label{display:flex;gap:8px;align-items:flex-start;cursor:pointer} +.ovg li input{margin-top:3px} +.ovg li.gone .ovm{color:var(--muted)} +.ovg li.keep{box-shadow:inset 3px 0 0 var(--low);padding-left:8px} +.ovm small{display:block;color:var(--muted)} +.ovb{font-family:var(--f-display);font-weight:600;font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--accent)} +.ovk{display:block;margin:3px 0 0 24px;font-size:11px;color:var(--low)} +.rk-tab tbody tr.static{cursor:default} +.lgchip{display:inline-flex;align-items:center;gap:6px;margin:0 10px 4px 0;font-size:12px} +.lgs{margin:6px 0;font-size:12.5px} +.lgs summary{cursor:pointer;display:flex;justify-content:space-between;gap:8px} +.lge small{display:block;color:var(--muted)} +@media (max-width:760px){.ovgrid{grid-template-columns:1fr}} + +/* One VLAN shown alone */ +#focusBar{position:fixed;z-index:5;top:calc(76px + env(safe-area-inset-top,0px));left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:10px;padding:6px 6px 6px 12px;font-size:12.5px;max-width:calc(100% - 32px);box-sizing:border-box} +#focusBar .btn{width:auto;white-space:nowrap} +body.lens #focusBar{display:none} +@media (max-width:760px){#focusBar{top:auto;bottom:calc(70px + env(safe-area-inset-bottom,0px))}} + +/* Dashboard and KPIs */ +:root{--k1:#2f63c8;--k2:#d97a0f;--a1:#c9d6e8;--a2:#8fa9cc;--a3:#4f73a8;--a4:#24426e} +@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--k1:#6a92e6;--k2:#cf7f2c;--a1:#2c3f5a;--a2:#3f6296;--a3:#6a92e6;--a4:#a9c4f5}} +:root[data-theme="dark"]{--k1:#6a92e6;--k2:#cf7f2c;--a1:#2c3f5a;--a2:#3f6296;--a3:#6a92e6;--a4:#a9c4f5} +#dash{position:fixed;inset:0;z-index:21;background:var(--bg);overflow:auto;padding:12px;box-sizing:border-box} +#dashBody{max-width:1280px;margin:0 auto} +.dash-bar{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;padding:8px 10px 8px 14px;margin-bottom:14px} +.dash-bar b{display:block;font-family:var(--f-display);font-size:18px} +.dash-tools{display:flex;flex-wrap:wrap;align-items:center;gap:8px} +.dash-tools .seg{min-width:280px} +.dash-tools select{padding:5px 6px;font:inherit;font-size:12.5px} +.dash-tools .btn{width:auto} +.ktiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px;margin-bottom:14px} +.ktile{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:10px 12px;display:flex;flex-direction:column;gap:3px} +.ktile>b{font-family:var(--f-display);font-size:30px;line-height:1.1;color:var(--fg)} +.ktile small{color:var(--muted);font-size:11.5px;line-height:1.35} +.ktile .kd.good{color:var(--low)}.ktile .kd.bad{color:var(--crit)} +.spark{width:100%;height:24px;margin-top:auto} +.dgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:12px;margin-bottom:12px} +.dcard{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:12px 14px;margin-bottom:12px} +.dgrid .dcard{margin-bottom:0} +.dcard h3{margin:0 0 2px;font-size:14px} +.dcard>p.muted{margin:0 0 6px;font-size:12px} +.dchart{width:100%;height:auto;display:block} +.dchart .grid{stroke:var(--line);stroke-width:1} +.dchart .ax{fill:var(--muted);font-size:10.5px;font-family:var(--f-mono)} +.dchart .dl{fill:var(--fg);font-size:11.5px;font-weight:600;font-family:var(--f-mono)} +.dleg{display:flex;flex-wrap:wrap;gap:6px 14px;margin:4px 0 0;font-size:12px;color:var(--fg)} +.dleg i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;vertical-align:-1px} +.dtab{width:100%;border-collapse:collapse;font-size:12.5px} +.dtab th{text-align:left;font-weight:500;color:var(--muted);padding:6px 8px;border-bottom:1px solid var(--line)} +.dtab td{padding:6px 8px;border-bottom:1px solid var(--line)} +.dtab .num{text-align:right;font-family:var(--f-mono);white-space:nowrap} +.dtab .bad{color:var(--crit);font-weight:600} +.dtab .kevtag{font-size:10px;font-weight:700;color:var(--crit);border:1px solid currentColor;border-radius:2px;padding:0 3px} +.dnote{color:var(--muted);font-size:11.5px;max-width:900px} +body.has-dash{overflow:hidden} +@media (max-width:760px){.dgrid{grid-template-columns:1fr}.dash-tools .seg{min-width:0;width:100%}} +@media print{ + body.has-dash>*:not(#dash){display:none!important} + body.has-dash #dash{position:static;overflow:visible;padding:0;background:#fff} + body.has-dash .dash-tools{display:none} + .dcard,.ktile{break-inside:avoid} +} + +/* The selected asset in the 2D diagram */ +#flatSvg .selhalo{animation:selhalo 1.6s ease-in-out infinite} +@keyframes selhalo{0%,100%{opacity:.1}50%{opacity:.32}} +@media (prefers-reduced-motion:reduce){#flatSvg .selhalo{animation:none}} diff --git a/public/app.js b/public/app.js index 6907dd5..d303df0 100644 --- a/public/app.js +++ b/public/app.js @@ -26,7 +26,10 @@ const F=(id,title,cvss,sev)=>Object.assign({id,title,cvss:cvss==null?null:cvss,s const TYPE_LABEL={server:'Server',dc:'Domain controller',db:'Database',hypervisor:'Hypervisor / storage',firewall:'Firewall',gateway:'VPN gateway',router:'Router / L3 core',switch:'Switch / load balancer',workstation:'Workstation',printer:'Printer',phone:'IP phone',ap:'Wi-Fi access point',camera:'IP camera',plc:'PLC',iot:'IoT device'}; // Interconnection equipment: flows can be routed through these const INTERCO=new Set(['firewall','router','switch','gateway']); -const SA=StrataAnalysis,IR=StrataIncident; +const SA=StrataAnalysis,IR=StrataIncident,RK=StrataRisk; +// Baselines shipped with Strata (baselines.js), and the risk settings shared by every IS, read at each compile +const BASELINES=()=>(typeof StrataBaselines!=='undefined'&&Array.isArray(StrataBaselines))?StrataBaselines:[]; +let RS=null; // Colours of the "Per flow" mode: --f0 to --f9 in app.css const FLOW_PAL=10; // Internet endpoints: the Internet as a whole, or a named service (ESET updates, Ubuntu mirrors…) as "svc:" @@ -327,22 +330,29 @@ function buildDemo(){ out.wan=[['paris','dcw','MPLS',4200],['lyon','dcw','MPLS',2600],['nantes','dcw','MPLS',900],['bordeaux','dcw','IPsec',650],['paris','lyon','IPsec',800], ['nantes','lyon','IPsec IKEv1',120,true,'IKEv1 tunnel with a weak pre-shared key'],['bordeaux','paris','Direct link',60,true,'Unfiltered interconnection between the two IS']] .map(([a,b,tech,vol,risk,note],i)=>({id:'w-'+i,a,b,tech,vol,risk:!!risk,note:note||''})); + // For the dashboard: when each vulnerability was first seen, the assets a scanner knows, and devices nobody owns + const h=s=>{let x=2166136261;for(const c of s)x=Math.imul(x^c.charCodeAt(0),16777619);return ((x>>>0)%100000)/100000;},now=Date.now(); + Object.entries(out.assets).forEach(([id,a])=>{ + (a.vulns||[]).forEach(v=>{const days=3+Math.floor(Math.pow(h(id+v.id),1.5)*380)+(v.kev?45:0);v.firstSeen=new Date(now-days*86400000).toISOString().slice(0,10);}); + if(['server','dc','db','hypervisor','workstation','gateway'].includes(a.type)&&h(id+'scan')<0.85)a.ext=Object.assign({},a.ext,{cyberwatch:1000+Math.floor(h(id)*9000)}); + if(['camera','phone','iot','printer','ap'].includes(a.type)&&h(id+'own')<0.6)a.contacts=[]; + }); return out; } /* ================= Storage ================= 'api' : served by the Strata server (shared data, live updates over SSE) 'local' : page opened on its own; data kept in this browser only */ -let raw={si:{},assets:{},wan:[],services:[]}; +let raw={si:{},assets:{},wan:[],services:[],risk:{}}; const LS_KEY='strata-v1'; // The live map, then what is shown: the live map, a draft applied over it, a proposal under review, // or an older version (read-only) let liveRaw=raw; function overlay(base,ch){ - const o={si:Object.assign({},base.si),assets:Object.assign({},base.assets),wan:base.wan,services:base.services}; + const o={si:Object.assign({},base.si),assets:Object.assign({},base.assets),wan:base.wan,services:base.services,risk:base.risk||{}}; if(!ch)return o; ['si','assets'].forEach(k=>Object.entries(ch[k]||{}).forEach(([id,doc])=>{if(doc==null)delete o[k][id];else o[k][id]=doc;})); - if(ch.wan)o.wan=ch.wan;if(ch.services)o.services=ch.services; + if(ch.wan)o.wan=ch.wan;if(ch.services)o.services=ch.services;if(ch.risk)o.risk=ch.risk; return o; } const Store={ @@ -363,7 +373,7 @@ const Store={ await this.pull();this.listen();return; } this.mode='local'; - try{const s=localStorage.getItem(LS_KEY);if(s){const o=JSON.parse(s);liveRaw={si:o.si||{},assets:o.assets||{},wan:o.wan||[],services:o.services||[]};}}catch(e){} + try{const s=localStorage.getItem(LS_KEY);if(s){const o=JSON.parse(s);liveRaw={si:o.si||{},assets:o.assets||{},wan:o.wan||[],services:o.services||[],risk:o.risk||{}};}}catch(e){} this.loaded=true;this.applyView(); }, applyView(delay){ @@ -375,7 +385,7 @@ const Store={ const r=await fetch('api/state',{cache:'no-store',credentials:'same-origin'}); if(!r.ok)throw new Error('HTTP '+r.status); const s=await r.json(); - liveRaw={si:s.si||{},assets:s.assets||{},wan:Array.isArray(s.wan)?s.wan:[],services:Array.isArray(s.services)?s.services:[]};this.rev=s.rev||0; + liveRaw={si:s.si||{},assets:s.assets||{},wan:Array.isArray(s.wan)?s.wan:[],services:Array.isArray(s.services)?s.services:[],risk:s.risk&&typeof s.risk==='object'?s.risk:{}};this.rev=s.rev||0; this.loaded=true;this.applyView(this.bulk?null:0); }catch(e){toast('Could not read the saved data. Check that the server is running, then reload.',true);} }, @@ -402,20 +412,20 @@ const Store={ return j; }, saveLocal(){try{localStorage.setItem(LS_KEY,JSON.stringify(liveRaw));}catch(e){toast('This browser refused to save the data (storage full or disabled).',true);}}, - // kind: si | assets | wan | services ; value null = delete + // kind: si | assets | wan | services | risk ; value null = delete async write(kind,id,value){ if(this.view==='preview'||this.view==='review'){toast(this.view==='preview'?'You are looking at an older version: it is read-only.':'A proposal under review is read-only.',true);throw new Error('readonly');} if(this.view==='draft')return this.writeDraft(kind,id,value); if(!this.liveWrite){toast(this.features.proposals?'Your role cannot change the map directly: start a proposal (Proposals › New).':'You have read-only access.',true);throw new Error('readonly');} const local=()=>{ if(kind==='si'||kind==='assets'){liveRaw[kind]=Object.assign({},liveRaw[kind]);if(value==null)delete liveRaw[kind][id];else liveRaw[kind][id]=clone(value);} - else liveRaw[kind]=clone(value||[]); + else liveRaw[kind]=clone(value||(kind==='risk'?{}:[])); }; if(this.mode==='local'){local();this.saveLocal();this.applyView(0);return;} - const url=kind==='wan'?'api/wan':kind==='services'?'api/services':`api/${kind}/${encodeURIComponent(id)}`; + const url=kind==='wan'?'api/wan':kind==='services'?'api/services':kind==='risk'?'api/risk':`api/${kind}/${encodeURIComponent(id)}`; try{ if(value==null&&(kind==='si'||kind==='assets'))await this.api('DELETE',url); - else await this.api('PUT',url,kind==='wan'?{links:value}:kind==='services'?{services:value}:value); + else await this.api('PUT',url,kind==='wan'?{links:value}:kind==='services'?{services:value}:kind==='risk'?{risk:value||{}}:value); local();this.applyView(this.bulk?null:0); }catch(e){ if(e.status===403)toast(e.message||'You cannot change this.',true); @@ -430,7 +440,7 @@ const Store={ if(kind==='si'||kind==='assets'){ // a document brought back to its live state leaves the change set if(JSON.stringify(liveRaw[kind][id]??null)===JSON.stringify(value??null))delete ch[kind][id];else ch[kind][id]=value==null?null:clone(value); - }else{if(JSON.stringify(liveRaw[kind]||[])===JSON.stringify(value||[]))delete ch[kind];else ch[kind]=clone(value||[]);} + }else{const e=kind==='risk'?{}:[];if(JSON.stringify(liveRaw[kind]||e)===JSON.stringify(value||e))delete ch[kind];else ch[kind]=clone(value||e);} this.applyView(this.bulk?null:0); clearTimeout(this._dt);this._dt=setTimeout(()=>this.saveDraft(),this.bulk?900:350); return Promise.resolve(); @@ -446,6 +456,7 @@ const Store={ delAsset(id){return this.write('assets',id,null);}, putWan(links){return this.write('wan',null,links);}, putServices(list){return this.write('services',null,list);}, + putRisk(doc){return this.write('risk',null,doc);}, async many(tasks,label){ this.bulk=true;this.batch={id:'b'+Date.now().toString(36)+Math.random().toString(36).slice(2,6),label};let done=0;const total=tasks.length; toast(`${label}: 0 / ${total}`,false,true); @@ -456,7 +467,7 @@ const Store={ }; // Replace the whole map (demo, import, delete everything) in one request async function replaceAll(data,label){ - const next={si:clone(data.si||{}),assets:clone(data.assets||{}),wan:clone(data.wan||[]),services:clone(data.services||[])}; + const next={si:clone(data.si||{}),assets:clone(data.assets||{}),wan:clone(data.wan||[]),services:clone(data.services||[]),risk:clone(data.risk||{})}; if(Store.view){toast('Leave the draft, review or older version first.',true);return;} if(!Store.liveWrite){toast('You cannot replace the map with your role.',true);return;} if(Store.mode==='local'){liveRaw=next;Store.saveLocal();Store.applyView(0);toast(`${label}: done.`);return;} @@ -569,6 +580,7 @@ function compile(){ // Attack paths: the critical assets (their own criticality, or that of a business service they // would hit) and what an attacker coming from the Internet could reach s.bizCrit=bizCrit;s.critIds=s.assets.filter(a=>a.crit===3||bizCrit[a.id]===3).map(a=>a.id);s.atkOut=SA.blastRadius(s,SA.outside(s)); + s.landscape=d.landscape&&typeof d.landscape==='object'?d.landscape:{}; s.usedSvc=new Set(s.flows.flatMap(f=>[f.src,f.dst]).filter(id=>id!=='internet'&&isNet(id))); const cnt=k=>s.assets.filter(a=>a.exp===k).length; s.stats={vlans:s.vlans.length,assets:s.assets.length,crit:s.assets.reduce((n,a)=>n+a.vulns.filter(v=>v.sev==='crit').length,0), @@ -578,6 +590,16 @@ function compile(){ return s; }); siteById={};SITES.forEach(s=>siteById[s.id]=s); + // Risk register: the threats that apply to each asset, scored on the matrix of the risk settings. First how much + // each threat weighs on the whole map (for merging overlapping requirements by best fit), then each asset. + const R0=RK.settings(raw.risk,BASELINES()),cat=RK.THREATS.concat(R0.customThreats),weights={}; + SITES.forEach(s=>s.assets.forEach(a=>cat.forEach(t=>{if(RK.matches(s,a,t,s.bizCrit[a.id],TYPE_LABEL))weights[t.id]=(weights[t.id]||0)+t.severity/4;}))); + RS=RK.settings(raw.risk,BASELINES(),{weights}); + const lv=c=>RK.LEVELS.indexOf(c.level); + SITES.forEach(s=>{s.threats={};s.register=[]; + s.assets.forEach(a=>{s.threats[a.id]=RK.assess(s,a,{settings:RS,baselines:BASELINES(),landscape:s.landscape,hops:s.expo[a.id],bizCrit:s.bizCrit[a.id],labels:TYPE_LABEL}); + s.threats[a.id].forEach(r=>{if(!r.excluded)s.register.push(Object.assign({asset:a},r));});}); + s.register.sort((x,y)=>lv(y.residual)-lv(x.residual)||y.pResidual*y.iResidual-x.pResidual*x.iResidual||x.asset.name.localeCompare(y.asset.name));}); WAN=(raw.wan||[]).filter(w=>siteById[w.a]&&siteById[w.b]&&w.a!==w.b).map(w=>Object.assign(clone(w),{vol:+w.vol||1,risk:!!w.risk})); } @@ -720,6 +742,8 @@ controls.touches={ONE:THREE.TOUCH.ROTATE,TWO:THREE.TOUCH.DOLLY_PAN}; renderer.domElement.addEventListener('mousedown',e=>{if(e.button===1)e.preventDefault();}); const W=34,D=21,SLAB=0.3,TABLE_Y=-2; +// the colour the selected asset glows in +const HL_COLOR=new THREE.Color(); let floors={},pickables=[],cloud=null,selRing=null,sun=null,levelTags=[],extent=W,nLevels=0,ANIM=[],OV={spanX:60,spanZ:40,ex:14,sites:{}}; const pGeo=new THREE.SphereGeometry(0.12,10,8); @@ -799,7 +823,7 @@ function buildBase(spanX,spanZ){ const lc=new THREE.Color(css('--matline')); [[pts,0.45],[maj,0.9]].forEach(([p,o])=>{const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3)); const l=new THREE.LineSegments(g,new THREE.LineBasicMaterial({color:lc,transparent:true,opacity:o}));l.position.y=TABLE_Y+0.005;scene.add(l);}); - selRing=new THREE.Mesh(new THREE.RingGeometry(1,1.08,64).rotateX(-Math.PI/2),new THREE.MeshBasicMaterial({color:new THREE.Color(css('--fg')),depthWrite:false})); + selRing=new THREE.Mesh(new THREE.RingGeometry(1,1.12,64).rotateX(-Math.PI/2),new THREE.MeshBasicMaterial({color:new THREE.Color(css('--accent')),depthWrite:false,transparent:true}));HL_COLOR.set(css('--accent')); selRing.visible=false;scene.add(selRing); return {steel:new THREE.MeshStandardMaterial({color:0xb9bec2,metalness:0.6,roughness:0.35}),card:new THREE.Color(css('--card')),detail:new THREE.Color(css('--detail'))}; } @@ -1095,7 +1119,10 @@ function refresh(){ ASSETS.forEach(a=>{ if(!a.mats)return; const op=V.aOp[a.id]; - a.mats.forEach(m=>{m.opacity=op;m.depthWrite=op>0.5;}); + a.mats.forEach(m=>{m.opacity=op;m.depthWrite=op>0.5; + // the selected asset glows (its own glow, if any, comes back after) + if(!m.userData.e0)m.userData.e0={c:m.emissive.getHex(),i:m.emissiveIntensity}; + if(sel===a){m.emissive.copy(HL_COLOR);m.emissiveIntensity=0.45;}else{m.emissive.setHex(m.userData.e0.c);m.emissiveIntensity=m.userData.e0.i;}}); a.label.visible=(state.labels&&op>0.5)||sel===a; if(a.portLabel)a.portLabel.visible=(state.ports&&op>0.5)||(sel===a&&state.ports); }); @@ -1119,7 +1146,7 @@ function placeSelRing(){ const det=$('#det'); const tag=k=>`${SEV[k].label}`; const xtag=k=>`${EXP[k].label}`; -function openDet(html,panel){det.classList.toggle('wide',panel==='incident'||panel==='ai');det.innerHTML=html;det.hidden=false;det.scrollTop=0;document.body.classList.add('has-det');const wasAtk=state.panel==='attack'&&panel!=='attack';state.panel=panel;renderEmpty();syncUI();placeLens();if(wasAtk&&state.view==='site')refresh();} +function openDet(html,panel){det.classList.toggle('wide',panel==='incident'||panel==='ai'||panel==='risksettings');det.innerHTML=html;det.hidden=false;det.scrollTop=0;document.body.classList.add('has-det');const wasAtk=state.panel==='attack'&&panel!=='attack';state.panel=panel;renderEmpty();syncUI();placeLens();if(wasAtk&&state.view==='site')refresh();} function closeDet(){const wasFlow=state.panel==='flow'||state.panel==='attack';det.hidden=true;document.body.classList.remove('has-det');state.panel=null;state.attackHl=null;renderEmpty();syncUI();placeLens();if(wasFlow&&state.view==='site')refresh();} function contactsHtml(list){ return `
    ${list.map(c=>`
  • ${esc(c.name||'Contact')}${esc(c.role||'')}${c.dispo?' · '+esc(c.dispo):''}
    @@ -1189,6 +1216,8 @@ function showAsset(a){

    Business services impacted · ${impactOf(a).length}

    ${impactHtml(a)} +

    Risks · ${(SITE.threats[a.id]||[]).filter(r=>!r.excluded).length}

    ${assetRiskHtml(a)} +

    Logs for the SIEM · ${logsOf(a).events}

    ${assetLogsHtml(a)}

    Inside · ${plural((a.apps||[]).length,'application')}

    ${insideHtml(a)}

    Incident contacts

    ${contacts} ${a.notes?`

    Notes

    ${esc(a.notes)}

    `:''} @@ -1574,7 +1603,7 @@ ${afTable(['Name','Role','Phone','Email','Availability'],s.contacts.map(c=>[esc( ${afTable(['Asset','Name','Role','Phone','Email','Availability'],ASSETS.flatMap(a=>a.contacts.filter(c=>c.name||c.phone||c.email).map((c,i)=>[i?'':`${esc(a.name)}`,esc(c.name),esc(c.role),`${esc(c.phone||'')}`,esc(c.email||''),esc(c.dispo||'')])))} ${H('hist')} -${hist?afTable(['Version','Date','By','Change'],hist.map(h=>[`v${esc(h.rev)}`,esc(new Date(h.at).toLocaleString()),esc(h.user||'—'),esc(h.label||`${{si:'IS',assets:'asset',wan:'inter-IS links',services:'Internet services',all:'whole map'}[h.kind]||h.kind} ${h.name?'“'+h.name+'” ':''}${h.op||''}`)])):'

    No history: this map is kept in a browser only. On a Strata server, every change is recorded with its author and date, and listed here.

    '} +${hist?afTable(['Version','Date','By','Change'],hist.map(h=>[`v${esc(h.rev)}`,esc(new Date(h.at).toLocaleString()),esc(h.user||'—'),esc(h.label||`${{si:'IS',assets:'asset',wan:'inter-IS links',services:'Internet services',risk:'risk settings',all:'whole map'}[h.kind]||h.kind} ${h.name?'“'+h.name+'” ':''}${h.op||''}`)])):'

    No history: this map is kept in a browser only. On a Strata server, every change is recorded with its author and date, and listed here.

    '} ${H('method')}

    Source. Everything in this file comes from the Strata map of the IS: entered by hand, imported, or read by the connectors (Cyberwatch, GLPI, phpIPAM, Palo Alto) and approved before publication. A flow missing from the map is missing here.

    @@ -1928,7 +1957,8 @@ async function act(name){ if(name==='new-vlan')return vlanForm(null); if(name==='edit-site')return siteForm(SITE); if(name==='new-biz')return businessForm(null); - if(name==='demo'){if(SITES.length&&!confirmArm('demo'))return;await replaceAll(buildDemo(),'Loading the demo');return;} + if(name==='demo'){if(SITES.length&&!confirmArm('demo'))return;const d=buildDemo();await replaceAll(d,'Loading the demo');if(Store.mode==='local')kpiSeedDemo(d);return;} + if(name==='dashboard')return openDash(); if(name==='wipe'){if(!confirmArm('wipe'))return;await replaceAll({si:{},assets:{},wan:[]},'Deleting');state.ovSelId=null;closeDet();return;} if(name==='export')return exportJson(); if(name==='import'){$('#io').value='';$('#io').click();return;} @@ -1945,7 +1975,7 @@ function confirmArm(key){ } document.addEventListener('click',e=>{const b=e.target.closest('[data-act]');if(b&&!b.closest('#det'))act(b.dataset.act);}); async function exportJson(){ - const data=JSON.stringify({format:'strata',version:3,exportedAt:new Date().toISOString(),si:raw.si,assets:raw.assets,wan:raw.wan,services:raw.services||[]},null,2); + const data=JSON.stringify({format:'strata',version:3,exportedAt:new Date().toISOString(),si:raw.si,assets:raw.assets,wan:raw.wan,services:raw.services||[],risk:raw.risk||{}},null,2); await saveFile(`strata-${today()}.json`,'application/json',data); } $('#io').addEventListener('change',e=>{ @@ -1960,10 +1990,11 @@ $('#io').addEventListener('change',e=>{ Object.entries(ch.si).forEach(([id,d])=>tasks.push(()=>Store.putSite(id,d))); Object.entries(ch.assets).forEach(([id,d])=>tasks.push(()=>d?Store.putAsset(id,d):Store.delAsset(id))); if(ch.services)tasks.push(()=>Store.putServices(ch.services)); + if(ch.risk)tasks.push(()=>Store.putRisk(ch.risk)); if(!tasks.length){toast('This file is identical to the map.');return;} await Store.many(tasks,`Import of IS ${Object.values(o.si)[0].name||''}`);return; } - await replaceAll({si:o.si,assets:o.assets,wan:Array.isArray(o.wan)?o.wan:[],services:Array.isArray(o.services)?o.services:[]},'Import'); + await replaceAll({si:o.si,assets:o.assets,wan:Array.isArray(o.wan)?o.wan:[],services:Array.isArray(o.services)?o.services:[],risk:o.risk&&typeof o.risk==='object'?o.risk:{}},'Import'); }; r.readAsText(f); }); @@ -2131,6 +2162,8 @@ function buildPlanSvg(a,perFlow,full,flt){ // the rules of the flows shown const FW0=fwRules(a),shown=new Set(sc.flows.map(f=>f.id)),FW={rules:FW0.rules.filter(r=>shown.has(r.flow.id)),unfiltered:FW0.unfiltered.filter(r=>shown.has(r.flow.id))},FWROWS=[...FW.rules,...FW.unfiltered],FROW=34; const TITLE=118,LEG=96,ROW=22,TAB=FLS.length?56+FLS.length*ROW:0,FWTAB=FWROWS.length?40+56+FWROWS.length*FROW+FROW:0; + // the requirements of the enabled baselines that address its risks, by baseline + const REQS=assetRequirements(a),REQROWS=REQS.reduce((n,b)=>n+1+b.list.length,0),REQTAB=REQROWS?40+56+REQROWS*FROW:0; // wider when the rules table is there, so its comments have room const W=Math.max(FWROWS.length?1280:1100,...bands.map(b=>b.w+M*2+40+CORR*2)); let y=M+TITLE; @@ -2140,7 +2173,7 @@ function buildPlanSvg(a,perFlow,full,flt){ b.h+=label;y+=b.h+gapH(bi);}); const lastB=bands.length-1; if(!(gapUse[lastB]||[]).length)y-=gapH(lastB)-40; - const tabY=y,fwY=tabY+TAB,H=fwY+FWTAB+LEG+M+30; + const tabY=y,fwY=tabY+TAB,reqY=fwY+FWTAB,H=reqY+REQTAB+LEG+M+30; const cy=id=>box[id].y+box[id].h/2,cx=id=>box[id].x+box[id].w/2; const side=(s,end)=>{const me=end?s.q:s.p,other=end?s.p:s.q; if(s.A===s.B)return 'bottom';return cy(other)>cy(me)?'bottom':'top';}; @@ -2262,6 +2295,27 @@ function buildPlanSvg(a,perFlow,full,flt){ FWROWS.forEach((r,i)=>rowAt(r,i,false)); rowAt({n:'',fw:'Every firewall above',src:'any',srcAddr:'all',dst:'any',dstAddr:'all',service:'any',action:'deny',comment:'Default rule, last on each firewall',dir:''},FWROWS.length,true); } + // Requirements for this asset: those of the enabled baselines that address its risks, by baseline + if(REQROWS){ + const ty=reqY+40,CS=W-M-150,CE=CS-120,CT=M+96,n=REQS.reduce((k,b)=>k+b.list.length,0),done=REQS.reduce((k,b)=>k+b.list.filter(q=>q.status==='applied').length,0); + out+=``; + out+=`REQUIREMENTS FOR ${X(a.name.toUpperCase())} · ${n} · ${done} APPLIED`; + [['Ref',M],['Requirement · risks it reduces',CT],['Reduces',CE],['Status',CS]].forEach(([t,x])=>out+=`${t.toUpperCase()}`); + const SC={applied:PC.low,partial:PC.med,planned:PC.med,open:PC.crit,na:PC.muted}; + let i=0; + REQS.forEach(b=>{ + const hy=ty+66+i*FROW;i++; + out+=`${X(clip(b.name,12.5,700,PF.d,W-M*2))}`; + b.list.forEach(q=>{const ry=ty+66+i*FROW; + if(i%2)out+=``; + out+=`${X(clip(q.ref,11,600,PF.m,CT-M-8))}`; + out+=`${X(clip(q.title,12,400,PF.b,CE-CT-16))}`; + out+=`${X(clip(q.threats.join(' · '),10,400,PF.m,CE-CT-16))}`; + out+=`${X(q.effect)}`; + out+=`${X(REQ_AGG[q.status])}`; + i++;}); + }); + } // Legend and scope note const ly=H-M-LEG+26; out+=``; @@ -2423,7 +2477,7 @@ function flatSvgMarkup(G,V,forExport,pal){ const op=V.aOp[a.id],dev=INTERCO.has(a.type),fill=dev?dark:card,tx=dev?devText:ink,mu=dev?'#aeb6bb':muted,sev=a.sev!=='none'?C(SEV[a.sev].css):null; const x=g.x-g.w/2,y=g.y-g.h/2,isSel=sel===a,drag=flatDrag&&flatDrag.moved&&flatDrag.kind==='asset'&&flatDrag.id===a.id; o+=``; - if(isSel)o+=``; + if(isSel)o+=``; if(DIFF&&DIFF.A[a.id])o+=`${DIFF.A[a.id]==='added'?'NEW':'CHANGED'}`; const im=forExport?null:incMark(a.id);if(im)o+=`${INC_LBL[im].toUpperCase()}`; o+=``; @@ -2536,7 +2590,7 @@ flatSvg.addEventListener('pointerdown',e=>{ if(A){const g=FL.assets[A.dataset.asset];flatDrag={kind:'asset',id:g.a.id,sx:e.clientX,sy:e.clientY,ox:wx-g.x,oy:wy-g.y,wx:g.x,wy:g.y,moved:false,edit};flatSvg.setPointerCapture(e.pointerId);return;} if(Hd&&edit){flatDrag={kind:'plate',id:Hd.dataset.plateHandle,sx:e.clientX,sy:e.clientY,startX:wx,startY:wy,dx:0,dy:0,moved:false};flatSvg.setPointerCapture(e.pointerId);return;} if(Fl){flatDrag={kind:'flow',id:Fl.dataset.flow,sx:e.clientX,sy:e.clientY,moved:false};return;} - flatDrag={kind:'pan',sx:e.clientX,sy:e.clientY,tx:flatView.tx,ty:flatView.ty,moved:false};flatSvg.setPointerCapture(e.pointerId);flatSvg.classList.add('panning'); + flatDrag={kind:'pan',sx:e.clientX,sy:e.clientY,tx:flatView.tx,ty:flatView.ty,moved:false,plate:Pl&&Pl.dataset.plate};flatSvg.setPointerCapture(e.pointerId);flatSvg.classList.add('panning'); }); flatSvg.addEventListener('pointermove',e=>{ const d=flatDrag;if(!d){flatTip(e);return;} @@ -2550,9 +2604,9 @@ flatSvg.addEventListener('pointermove',e=>{ flatSvg.addEventListener('pointerup',e=>{ const d=flatDrag;flatDrag=null;flatSvg.classList.remove('panning');if(!d)return; const [wx,wy]=toWorld(e); - if(d.kind==='pan'){if(!d.moved&&state.panel!=='form'){if(state.selId)select(null);else if(state.panel==='flow')closeDet();}return;} - if(d.kind==='asset'){if(!d.moved){if(state.panel!=='form')select(byId[d.id],false);return;}dropAsset(d.id,d.wx,d.wy);return;} - if(d.kind==='plate'){if(!d.moved){renderFlat();return;}dropPlate(d.id,wx,wy,d);return;} + if(d.kind==='pan'){if(!d.moved&&state.panel!=='form'){if(d.plate)focusVlan(d.plate);else if(state.focus){if(state.selId)select(null);unfocusVlan();}else if(state.selId)select(null);else if(state.panel==='flow')closeDet();}return;} + if(d.kind==='asset'){if(!d.moved){if(state.panel!=='form')select(byId[d.id],true);return;}dropAsset(d.id,d.wx,d.wy);return;} + if(d.kind==='plate'){if(!d.moved){if(state.panel!=='form')focusVlan(d.id);else renderFlat();return;}dropPlate(d.id,wx,wy,d);return;} if(d.kind==='svc'){if(!d.moved&&state.panel!=='form'&&SVC[d.id])showService(SVC[d.id]);return;} if(d.kind==='flow'){if(!d.moved&&state.panel!=='form'){const f=FLOWS.find(x=>x.id===d.id);if(f)showFlow(f);}return;} if(d.kind==='link'){renderFlat();const el=document.elementFromPoint(e.clientX,e.clientY),T=el&&el.closest&&(el.closest('[data-asset]')||el.closest('[data-internet]')); @@ -2579,6 +2633,7 @@ function showFlatUI(){ lensEl.hidden=!lens;document.body.classList.toggle('lens',lens); document.querySelectorAll('#lensSeg button').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.lens===state.lens))); if(lens)renderLens(); + placeFocusBar(); } async function exportFlat(){ if(!FL)return; @@ -2614,7 +2669,7 @@ function placeLens(){ addEventListener('resize',placeLens); function renderLens(){ if(state.view!=='site'||state.lens==='physical')return; - if(state.lens==='app')renderAppLens();else renderBizLens(); + if(state.lens==='app')renderAppLens();else if(state.lens==='risk')renderRiskLens();else renderBizLens(); placeLens(); } const worstOf=ids=>ids.reduce((m,id)=>byId[id]&&SEV[byId[id].sev].rank>SEV[m].rank?byId[id].sev:m,'none'); @@ -2781,13 +2836,14 @@ function computeDiff(){ if(b){const f=changedFields(b,doc,['vlans','flows','services']);if(f.length)items.push({kind:'site',op:'modified',id:sid,site:sid,label:'IS '+doc.name,fields:f});}}); if(ch.services)diffList(liveRaw.services,ch.services,x=>x.name,'service',null,{},items); if(ch.wan)items.push({kind:'wan',op:'modified',label:'Inter-IS links'}); + if(ch.risk)items.push({kind:'risk',op:'modified',label:'Risk settings'}); DIFF={A,F,V,items}; } const DIFF_COL={added:'--low',modified:'--med',removed:'--crit'}; -const KIND_LBL={asset:'Asset',flow:'Flow',vlan:'VLAN',site:'IS',service:'Internet service',wan:'WAN'}; +const KIND_LBL={asset:'Asset',flow:'Flow',vlan:'VLAN',site:'IS',service:'Internet service',wan:'WAN',risk:'Risk'}; function diffHtml(){ if(!DIFF||!DIFF.items.length)return '

    No change yet. Edit the map (2D diagram, sheets, forms): every change goes into this draft.

    '; - const order={site:0,vlan:1,asset:2,flow:3,service:4,wan:5}; + const order={site:0,vlan:1,asset:2,flow:3,service:4,wan:5,risk:6}; return `
      ${DIFF.items.slice().sort((a,b)=>order[a.kind]-order[b.kind]).map(it=>`
    • ${it.op}${KIND_LBL[it.kind]}${it.kind==='asset'&&it.op!=='removed'?``:it.kind==='flow'&&it.op!=='removed'?``:esc(it.label)}${it.fields&&it.fields.length?`${esc(it.fields.join(', '))}`:''}
    • `).join('')}
    `; } let PROPS=[]; @@ -2827,7 +2883,7 @@ function proposalForm(){ let o;try{o=JSON.parse(await f.text());}catch(e){return formErr('This file is not valid JSON.');} if(!o||typeof o.si!=='object'||typeof o.assets!=='object')return formErr('Unexpected format: a Strata export has the “si” and “assets” keys.'); changes=changesFromFile(o);const ks=Object.keys(o.si);if(ks.length===1)siteId=ks[0]; - if(!Object.keys(changes.si).length&&!Object.keys(changes.assets).length&&!changes.services)return formErr('This file is identical to the map: nothing to propose.'); + if(!Object.keys(changes.si).length&&!Object.keys(changes.assets).length&&!changes.services&&!changes.risk)return formErr('This file is identical to the map: nothing to propose.'); } try{const j=await Store.api('POST','api/proposals',{title,description:$('#fDesc').value.trim(),siteId,changes,source:from}); onSave=null;toast(from==='file'?'Proposal created from the file: check it, then submit it.':'Draft opened: your changes now go into it.');enterProp(j.proposal,'draft');} @@ -2841,6 +2897,7 @@ function changesFromFile(o){ Object.entries(o.assets||{}).forEach(([id,a])=>{if(a&&sites.includes(a.site)&&J(liveRaw.assets[id])!==J(a))ch.assets[id]=a;}); Object.entries(liveRaw.assets).forEach(([id,a])=>{if(sites.includes(a.site)&&!(id in (o.assets||{})))ch.assets[id]=null;}); if(Array.isArray(o.services)&&J(o.services)!==J(liveRaw.services||[]))ch.services=o.services; + if(o.risk&&typeof o.risk==='object'&&J(o.risk)!==J(liveRaw.risk||{}))ch.risk=o.risk; return ch; } async function openProposal(id,keepView){ @@ -2918,7 +2975,7 @@ async function showHistory(more){ const siteOnly=state.view==='site'&&!HIST.all&&SITE; const list=HIST.entries.filter(h=>!siteOnly||h.site===SITE.id); const groups=[];list.forEach(h=>{const g=groups[groups.length-1];if(g&&g.batch===h.batch&&g.rev===h.rev||g&&g.batch===h.batch&&h.batch&&!/^r\d+$/.test(h.batch))g.items.push(h);else groups.push({batch:h.batch,rev:h.rev,at:h.at,user:h.user,label:h.label,items:[h]});}); - const KL={si:'IS',assets:'asset',wan:'inter-IS links',services:'Internet services',all:'whole map'}; + const KL={si:'IS',assets:'asset',wan:'inter-IS links',services:'Internet services',risk:'risk settings',all:'whole map'}; const sum=g=>g.label||(g.items.length===1?`${KL[g.items[0].kind]} ${g.items[0].name?'“'+g.items[0].name+'” ':''}${g.items[0].op}`:`${g.items.length} changes: ${[...new Set(g.items.map(h=>h.name).filter(Boolean))].slice(0,4).join(', ')}`); const canRestore=Store.liveWrite&&!Store.view; openDet(`
    Collaboration

    History

    Version ${HIST.rev}${siteOnly?` · IS ${esc(SITE.name)}`:' · whole map'}

    @@ -2933,7 +2990,7 @@ async function showHistory(more){ async function previewVersion(rev){ let s;try{s=await Store.api('GET','api/state?at='+rev);}catch(e){toast(e.message,true);return;} const g=HIST.entries.find(h=>h.rev===rev)||{}; - Store.view='preview';Store.prop=null;Store.preview={rev,at:g.at,user:g.user,raw:{si:s.si||{},assets:s.assets||{},wan:s.wan||[],services:s.services||[]}}; + Store.view='preview';Store.prop=null;Store.preview={rev,at:g.at,user:g.user,raw:{si:s.si||{},assets:s.assets||{},wan:s.wan||[],services:s.services||[],risk:s.risk||{}}}; Store.applyView(0);renderModeBar(); pendingAfterData=()=>{if(state.view==='site'&&!siteById[state.siteId])backToOverview();closeDet();}; } @@ -3546,6 +3603,512 @@ det.addEventListener('change',e=>{ if(t.hasAttribute('data-ai-answer')){AI_ANSWER=t.value;showPrompts(a,true);} }); +/* ================= Risk register ================= */ +// The threats that apply to each asset (its exposure, criticality, ports, system, vulnerabilities and the +// threat landscape of its IS), placed on the risk matrix of the settings before and after the requirements +// of the enabled baselines. Treatments are saved on the asset (field risk), settings in the shared document risk. +const lvTag=l=>`${RK.LEVEL_LBL[l]}`; +const REQ_ST={'':'Not applied',planned:'Planned',applied:'Applied',na:'Not applicable'}; +const DECISION={treat:'Treat (apply requirements)',accept:'Accept the risk',transfer:'Transfer (insurance, contract)',avoid:'Avoid (remove the cause)'}; +const pctR=x=>Math.round(x*100)+'%'; +// A risk matrix: likelihood upwards, impact rightwards, a count in each cell +function matrixHtml(M,counts,{click,on,title}={}){ + const n=M.likelihood.length,m=M.impact.length;let h=`
    ${title?`
    ${esc(title)}
    `:''}`; + for(let l=n-1;l>=0;l--){h+=``; + for(let i=0;i${c||''}`;} + h+='';} + return h+`${M.impact.map(x=>``).join('')}
    ${esc(M.likelihood[l])}
    ${esc(x)}

    ↑ LikelihoodImpact →

    `; +} +const gridOf=(rows,k)=>{const M=RS.matrix,g=M.cells.map(r=>r.map(()=>0));rows.forEach(r=>g[r[k].l][r[k].i]++);return g;}; +const reqCount=r=>{const ap=r.requirements.filter(x=>x.status==='applied').length,pl=r.requirements.filter(x=>x.status==='planned').length;return {ap,pl,n:r.requirements.filter(x=>x.status!=='na').length};}; +function riskRow(r,withAsset){ + const c=reqCount(r); + return `
  • + ${lvTag(r.inherent.level)}${r.residual.level!==r.inherent.level||r.inherent.l!==r.residual.l||r.inherent.i!==r.residual.i?`→${lvTag(r.residual.level)}`:''}
  • `; +} +// The asset sheet: its own matrix and its risks +function assetRiskHtml(a){ + const all=SITE.threats[a.id]||[],rows=all.filter(r=>!r.excluded).map(r=>Object.assign({asset:a},r)),ex=all.filter(r=>r.excluded); + const left=RK.THREATS.concat(RS.customThreats).filter(t=>!RS.disabled.has(t.id)&&!all.some(r=>r.threat.id===t.id)); + if(!rows.length&&!ex.length)return `

    No threat of the catalogue applies to this asset with what the map says about it (type, exposure, criticality, ports, system, vulnerabilities).

    ${addThreatHtml(a,left)}`; + const over=rows.filter(r=>!r.acceptable).length,acc=RK.acceptFor(RS.matrix,a,SITE.bizCrit[a.id]); + return `${acceptHtml(a,acc)}
    ${matrixHtml(RS.matrix,gridOf(rows,'inherent'),{title:'Before requirements'})}${matrixHtml(RS.matrix,gridOf(rows,'residual'),{title:'With the requirements applied'})}
    +

    ${over?`${over} ${over===1?'risk is':'risks are'} above the acceptance threshold (${RK.LEVEL_LBL[RK.LEVELS[acc.level]]}) with no decision to accept it.`:'Every risk is at or below the acceptance threshold, or accepted.'} Open a risk to apply requirements.

    +
      ${rows.map(r=>riskRow(r,false)).join('')}
    + ${ex.length?`
    ${plural(ex.length,'threat')} set aside as not relevant
      ${ex.map(r=>`
    • `).join('')}
    `:''} + ${addThreatHtml(a,left)}`; +} +// The acceptance threshold of an asset: its own, or that of its criticality in the risk settings +function acceptHtml(a,acc){ + const from=acc.from==='asset'?'set on this asset':acc.from==='crit'?`for ${CRIT_LBL[acc.crit].toLowerCase()} criticality assets`:'for every asset',def=RK.acceptFor(RS.matrix,Object.assign({},a,{risk:null}),SITE.bizCrit[a.id]); + return `
    Acceptable up to ${lvTag(RK.LEVELS[acc.level])} ${from} +
    `; +} +// The requirements that address the risks of an asset, one line each, by baseline: what it needs, whatever the risk +const REQ_AGG={applied:'Applied',partial:'Partly applied',planned:'Planned',open:'Not applied',na:'Not applicable'}; +function assetRequirements(a){ + const by={},order=[]; + (SITE.threats[a.id]||[]).filter(r=>!r.excluded).forEach(r=>r.requirements.forEach(q=>{ + const b=by[q.baseline]||(by[q.baseline]={name:q.baselineName,reqs:{}},order.push(q.baseline),by[q.baseline]); + const x=b.reqs[q.key]||(b.reqs[q.key]={ref:q.ref,title:q.title,threats:[],statuses:[],effects:new Set()}); + x.threats.push(r.threat.name);x.statuses.push(q.status||'');x.effects.add(q.effect);if(q.merged)x.merged=q.merged;})); + const agg=st=>{const l=st.filter(x=>x!=='na');if(!l.length)return 'na';if(l.every(x=>x==='applied'))return 'applied';if(l.some(x=>x==='applied'))return 'partial';return l.every(x=>x==='planned')?'planned':l.some(x=>x==='planned')?'partial':'open';}; + return RS.baselines.filter(id=>by[id]).map(id=>({name:by[id].name,list:Object.values(by[id].reqs).map(x=>({ref:x.ref,title:x.title,threats:x.merged?[...x.threats,'stands for '+x.merged.map(m=>`${shortName(m.baselineName)} ${m.ref}`).join(', ')]:x.threats,status:agg(x.statuses),effect:[...x.effects].join(', ')})) + .sort((p,q)=>p.ref.localeCompare(q.ref,'en',{numeric:true}))})); +} +const addThreatHtml=(a,left)=>left.length?`
    `:''; +async function saveTreatment(aid,tid,change){ + const d=clone(raw.assets[aid]);if(!d)return; + const r=RK.setTreatment(d,tid,change);if(r)d.risk=r;else delete d.risk; + try{await Store.putAsset(aid,d);}catch(e){} +} + +// One risk of one asset: why it applies, how it is scored, the requirements against it and the decision +let THREAT_FOR=null; +function showThreat(aid,tid,keep){ + const a=byId[aid],r=a&&(SITE.threats[aid]||[]).find(x=>x.threat.id===tid);if(!r){if(a)showAsset(a);return;} + THREAT_FOR={aid,tid};const top=keep?det.scrollTop:0,t=r.threat,M=RS.matrix,ro=Store.canWrite?'':' disabled',F=r.factors; + const cell=c=>`${lvTag(c.level)} ${esc(M.likelihood[c.l])} × ${esc(M.impact[c.i])}`; + const byB={};r.requirements.forEach(q=>(byB[q.baseline]=byB[q.baseline]||{name:q.baselineName,list:[]}).list.push(q)); + const act=RK.ACTORS.filter(x=>t.actors.includes(x.id)),en=RK.ENISA.filter(x=>t.enisa.includes(x.id)); + const manual=Array.isArray((a.risk||{}).added)&&a.risk.added.includes(t.id); + openDet(`
    Risk · ${esc(a.name)}

    ${esc(t.name)}

    ${esc(t.desc)}

    +
    +
    +
    Applies because
    ${esc(r.why.join(' · '))}
    +
    Before requirements
    ${cell(r.inherent)}
    +
    Requirements applied
    ${cell(r.residual)}
    +
    Once planned ones too
    ${cell(r.target)}
    + ${act.length?`
    Risk sources
    ${act.map(x=>`${esc(x.name)} (relevance ${(SITE.landscape.actors||{})[x.id]??x.relevance}/3)`).join(', ')}
    `:''} + ${t.attack.length?`
    MITRE ATT&CK
    ${t.attack.map(x=>`${x}`).join(' ')}
    `:''} + ${en.length?`
    ENISA landscape
    ${en.map(x=>esc(x.name)).join(', ')}
    `:''} +
    +
    How it is scored
    +
    Likelihood
    ${t.base}/4 for the threat × ${pctR(0.4+0.6*F.context)} exposure and reachability${F.vuln!=null?` × ${pctR(0.5+0.5*F.vuln)} from its vulnerabilities`:''}${t.actors.length?` × ${pctR(F.actors)} from the relevance of its sources`:''} = ${pctR(r.p)}${r.pResidual!==r.p?`, ${pctR(r.pResidual)} with the requirements`:''}
    +
    Impact
    ${t.severity}/4 for the threat × ${pctR(F.crit)} criticality of the asset or of the business services it would hit = ${pctR(r.i)}${r.iResidual!==r.i?`, ${pctR(r.iResidual)} with the requirements`:''}
    +
    Requirements
    Each requirement applied lowers the likelihood or the impact by its share; planned ones count in the last line only.
    +

    Decision

    +
    +
    + + ${manual?`
    `:''} +

    Requirements · ${r.requirements.length}

    + ${!RS.baselines.length?'

    No baseline is enabled. Choose them in the risk settings (Risks view › Settings).

    ':!r.requirements.length?'

    No requirement of the enabled baselines addresses this threat.

    ': + Object.entries(byB).map(([id,b])=>`

    ${esc(b.name)}

      ${b.list.map(q=>`
    • ${esc(q.ref)} ${esc(q.title)}−${Math.round(q.strength*100)}% on the ${q.effect}${q.merged?` · stands for ${esc(q.merged.map(m=>`${shortName(m.baselineName)} ${m.ref}`).join(', '))}`:''}${q.text?` · details`:''}
      +
    • `).join('')}
    `).join('')} +
    `,'threat'); + if(keep)det.scrollTop=top; +} +det.addEventListener('click',e=>{ + if(!SITE)return;const q=s=>e.target.closest(s); + if(q('[data-threat]')){const b=q('[data-threat]');showThreat(b.dataset.threatAsset||state.selId,b.dataset.threat);return;} + if(state.panel!=='threat'||!THREAT_FOR)return; + if(q('[data-threat-back]')){const a=byId[THREAT_FOR.aid];if(a)select(a);return;} + if(q('[data-threat-unadd]')){const {aid,tid}=THREAT_FOR;saveTreatment(aid,tid,{add:false}).then(()=>{if(byId[aid])select(byId[aid]);});} +}); +det.addEventListener('change',e=>{ + if(!SITE)return;const t=e.target; + if(t.dataset.acceptAsset){const d=clone(raw.assets[t.dataset.acceptAsset]);if(!d)return;const r=RK.setAccept(d,t.value===''?null:+t.value);if(r)d.risk=r;else delete d.risk;Store.putAsset(t.dataset.acceptAsset,d).catch(()=>{});return;} + if(t.dataset.threatAdd!==undefined&&t.value){const aid=t.dataset.threatAdd,tid=t.value;saveTreatment(aid,tid,{add:true}).then(()=>{state.panel='threat';THREAT_FOR={aid,tid};scheduleData(0);});return;} + if(state.panel!=='threat'||!THREAT_FOR)return;const {aid,tid}=THREAT_FOR; + if(t.dataset.req)saveTreatment(aid,tid,{req:t.dataset.req,status:t.value}); + else if(t.hasAttribute('data-threat-decision'))saveTreatment(aid,tid,{decision:t.value}); + else if(t.hasAttribute('data-threat-note'))saveTreatment(aid,tid,{note:t.value}); + else if(t.hasAttribute('data-threat-exclude'))saveTreatment(aid,tid,{exclude:t.checked}); +}); + +// The Risks view: the register of the IS +let RKF=null; // filter: {k: 'inherent'|'residual', l, i} +let RKV='register'; // the tab of the Risks view: register | baselines | logs +function renderRiskLens(){ + const tabs=`
    ${[['register','Register'],['baselines','Baselines'],['logs','Logs for the SIEM']].map(([k,t])=>``).join('')}
    `; + if(RKV==='baselines')renderBaselinesView(tabs);else if(RKV==='logs')renderLogsView(tabs);else renderRegister(tabs); +} +function renderRegister(tabs){ + const rows=SITE.register,M=RS.matrix,shown=RKF?rows.filter(r=>r[RKF.k].l===RKF.l&&r[RKF.k].i===RKF.i):rows; + const by=k=>RK.LEVELS.slice().reverse().map(l=>`${rows.filter(r=>r[k].level===l).length} ${RK.LEVEL_LBL[l].toLowerCase()}`).join(' · '); + const open=rows.filter(r=>!r.acceptable).length,assets=new Set(rows.map(r=>r.asset.id)).size; + lensBody.innerHTML=`${tabs}

    Risk register

    The threats that apply to each asset, from its exposure, criticality, ports, system and vulnerabilities, and from the threat landscape of this IS. Each is placed on the risk matrix before and after the requirements of the enabled baselines (${RS.baselines.length?esc(RS.baselines.map(id=>(BASELINES().concat(RS.custom).find(b=>b.id===id)||{}).name||id).join(', ')):'none yet'}).

    +
    + ${rows.length?`

    ${plural(rows.length,'risk')} on ${plural(assets,'asset')}. Before requirements: ${by('inherent')}. With the requirements applied: ${by('residual')}. ${open} above the acceptance threshold and not accepted.

    +
    ${matrixHtml(M,gridOf(rows,'inherent'),{title:'Before requirements',click:'inherent',on:(l,i)=>RKF&&RKF.k==='inherent'&&RKF.l===l&&RKF.i===i})}${matrixHtml(M,gridOf(rows,'residual'),{title:'With the requirements applied',click:'residual',on:(l,i)=>RKF&&RKF.k==='residual'&&RKF.l===l&&RKF.i===i})}
    + ${RKF?`

    Showing the ${plural(shown.length,'risk')} ${RKF.k==='inherent'?'before requirements':'with requirements applied'} at ${esc(M.likelihood[RKF.l])} likelihood × ${esc(M.impact[RKF.i])} impact.

    `:'

    Click a cell to list its risks.

    '} + + ${shown.map(r=>{const c=reqCount(r);return ``;}).join('')} +
    AssetThreatBeforeNowPlannedRequirementsDecision
    ${esc(r.asset.name)}${esc(TYPE_LABEL[r.asset.type]||'')}${esc(r.threat.name)}${lvTag(r.inherent.level)}${lvTag(r.residual.level)}${lvTag(r.target.level)}${r.requirements.length?`${c.ap}/${c.n}${c.pl?` +${c.pl}`:''}`:'—'}${esc(DECISION[r.decision].split(' (')[0])}
    `:`

    No risk yet: no threat of the catalogue applies to the assets of this IS with what the map says about them. Record flows (exposure), criticality, systems and vulnerabilities, or add threats by hand from an asset sheet.

    `}`; +} +lensEl.addEventListener('click',e=>{ + if(state.lens!=='risk'||!SITE)return;const q=s=>e.target.closest(s); + if(q('[data-rkv]')){RKV=q('[data-rkv]').dataset.rkv;renderRiskLens();lensEl.scrollTop=0;return;} + if(q('[data-cell]')){const [k,v]=q('[data-cell]').dataset.cell.split(':'),[l,i]=v.split(',').map(Number);RKF=RKF&&RKF.k===k&&RKF.l===l&&RKF.i===i?null:{k,l,i};renderRiskLens();return;} + if(q('[data-rk-clear]')){RKF=null;renderRiskLens();return;} + if(q('[data-threat]')){const b=q('[data-threat]');state.selId=b.dataset.threatAsset;showThreat(b.dataset.threatAsset,b.dataset.threat);return;} + if(q('[data-rk-settings]')){showRiskSettings();return;} + if(q('[data-rk-landscape]')){showLandscape();return;} + if(q('[data-csv-register]'))exportRegister(); +}); +lensEl.addEventListener('keydown',e=>{if(state.lens!=='risk'||(e.key!=='Enter'&&e.key!==' '))return;const t=e.target.closest('[data-cell],[data-threat],[data-goto-logs]');if(t){e.preventDefault();t.click();}}); +function exportRegister(){ + const rows=[['Asset','IP','Type','Threat','Applies because','MITRE ATT&CK','Likelihood before','Impact before','Level before','Level now','Level once planned','Requirements applied','Requirements planned','Requirements not applied','Acceptable up to','Decision','Justification']]; + const M=RS.matrix,refs=(r,s)=>r.requirements.filter(x=>(x.status||'')===s).map(x=>`${x.baselineName} ${x.ref}`).join(', '); + SITE.register.forEach(r=>rows.push([r.asset.name,r.asset.ip||'',TYPE_LABEL[r.asset.type]||r.asset.type,r.threat.name,r.why.join('; '),r.threat.attack.join(' '),M.likelihood[r.inherent.l],M.impact[r.inherent.i],RK.LEVEL_LBL[r.inherent.level],RK.LEVEL_LBL[r.residual.level],RK.LEVEL_LBL[r.target.level],refs(r,'applied'),refs(r,'planned'),refs(r,''),RK.LEVEL_LBL[RK.LEVELS[RK.acceptFor(M,r.asset,SITE.bizCrit[r.asset.id]).level]],DECISION[r.decision].split(' (')[0],r.note])); + if(rows.length===1){toast('No risk on this IS.',true);return;} + saveFile(`risk-register_${fileStem(SITE.name)}_${today()}.csv`,'text/csv;charset=utf-8',csv(rows)); +} + +// The threat landscape of an IS: how relevant each risk source is to it (EBIOS RM, workshop 2) +function showLandscape(){ + const L=SITE.landscape.actors||{},ro=Store.canWrite?'':' disabled'; + openDet(`
    Threat landscape · IS ${esc(SITE.name)}

    Risk sources

    How relevant each risk source and its objective is to this IS. A source rated 0 takes its threats out of the register; 3 keeps them at full likelihood.

    +
      ${RK.ACTORS.map(x=>{const v=L[x.id]??x.relevance;return `
    • ${esc(x.name)}${esc(x.origin)} · ${esc(x.objective)}
    • `;}).join('')}
    +

    Defaults follow the ENISA Threat Landscape: cybercrime is the most likely source for any organisation. Raise state-sponsored sources for operators of essential services or defence suppliers.

    `,'landscape'); +} +det.addEventListener('change',e=>{ + const s=e.target.closest('[data-actor]');if(!s||state.panel!=='landscape'||!SITE)return; + const d=clone(raw.si[SITE.id]),L=Object.assign({},(d.landscape&&d.landscape.actors)||{});L[s.dataset.actor]=+s.value; + const def=RK.ACTORS.find(x=>x.id===s.dataset.actor);if(def&&def.relevance===+s.value)delete L[s.dataset.actor]; + d.landscape=Object.assign({},d.landscape,{actors:L});if(!Object.keys(L).length)delete d.landscape.actors;if(!Object.keys(d.landscape).length)delete d.landscape; + Store.putSite(SITE.id,d).catch(()=>{}); +}); + +// Risk settings: the matrix, the baselines in use, the threat catalogue. Edited on a copy, saved at once. +let RSD=null; +function showRiskSettings(keep){ + if(!keep)RSD=clone(raw.risk||{}); + const S=RK.settings(RSD,BASELINES()),M=S.matrix,ro=Store.canWrite?'':' disabled',top=keep?det.scrollTop:0; + const all=BASELINES().map(b=>RK.cleanBaseline(b,false)).concat(S.custom),cat=RK.THREATS.concat(S.customThreats); + const sizes=(k,n)=>``; + openDet(`
    Risks · every IS

    Risk settings

    The matrix, the baselines whose requirements are offered against each threat, and the threat catalogue

    +
    +

    Risk matrix

    +
    +
    +
    By criticality of the asset or of the business services it would hit:${[1,2,3].map(c=>``).join('')}
    +
    ${(()=>{let h='';for(let l=M.likelihood.length-1;l>=0;l--){h+=``;for(let i=0;i`;}h+='';} + return h+`${M.impact.map((x,i)=>``).join('')}
    `;})()}
    +

    Rows: likelihood, highest at the top. Columns: impact. Click a cell to change its level. The levels split the scores evenly; a risk above the acceptable level needs requirements or a decision. An asset sheet can set a threshold of its own.

    +

    Baselines · ${S.baselines.length} enabled

    +
      ${all.map(b=>`
    • ${b.custom?``:''}
    • `).join('')||'
    • No baseline available.
    • '}
    +
    +

    Threat catalogue · ${cat.length-S.disabled.size} in use

    +
      ${cat.map(t=>`
    • ${t.custom?``:''}
    • `).join('')}
    +
    Add a custom threat +
    +
    +
    +
    +
    +
    +
    +

    Share

    +
    + +
    `,'risksettings'); + if(keep)det.scrollTop=top; +} +// Every change is saved at once, then the panel is drawn again from the saved copy +function rsSave(){const d=clone(RSD);Store.putRisk(d).then(()=>{if(state.panel==='risksettings')showRiskSettings(true);}).catch(()=>{RSD=clone(raw.risk||{});if(state.panel==='risksettings')showRiskSettings(true);});} +const rsMatrix=()=>RK.cleanMatrix(RSD.matrix); +const rsEnabled=()=>RK.settings(RSD,BASELINES()).baselines; +let RS_IMPORT=null; +det.addEventListener('click',e=>{ + if(state.panel!=='risksettings'||!RSD)return;const q=s=>e.target.closest(s); + if(q('[data-rs-cell]')){const [l,i]=q('[data-rs-cell]').dataset.rsCell.split(',').map(Number),M=rsMatrix();M.cells=M.cells.map(r=>r.slice());M.cells[l][i]=(M.cells[l][i]+1)%4;RSD.matrix=M;rsSave();return;} + if(q('[data-rs-del-baseline]')){const id=q('[data-rs-del-baseline]').dataset.rsDelBaseline,en=rsEnabled();RSD.custom=(RSD.custom||[]).filter(b=>'custom:'+String(b.id).replace(/^custom:/,'')!==id);RSD.baselines=en.filter(x=>x!==id);rsSave();return;} + if(q('[data-rs-del-threat]')){const id=q('[data-rs-del-threat]').dataset.rsDelThreat;RSD.threats=Object.assign({},RSD.threats);RSD.threats.custom=(RSD.threats.custom||[]).filter(t=>'custom:'+String(t.id).replace(/^custom:/,'')!==id);rsSave();return;} + if(q('[data-rs-add-threat]')){ + const name=$('#rsTName').value.trim();if(!name){toast('Give the threat a name.',true);return;} + const types=[...$('#rsTTypes').selectedOptions].map(o=>o.value),w={critMin:+$('#rsTCrit').value};if(types.length)w.types=types;if($('#rsTExp').value)w.exposure=$('#rsTExp').value; + const t={id:fileStem(name).toLowerCase()+'-'+Date.now().toString(36),name,desc:$('#rsTDesc').value.trim(),base:+$('#rsTBase').value,severity:+$('#rsTSev').value,when:w,attack:$('#rsTAtt').value.split(/[\s,;]+/).map(x=>x.trim().toUpperCase()).filter(Boolean)}; + RSD.threats=Object.assign({},RSD.threats);RSD.threats.custom=(RSD.threats.custom||[]).concat([t]);rsSave();return;} + if(q('[data-rs-export]')){saveFile(`strata-risk-settings_${today()}.json`,'application/json',JSON.stringify({format:'strata-risk',version:1,risk:raw.risk||{}},null,2));return;} + if(q('[data-rs-template]')){saveFile('baseline-template.csv','text/csv;charset=utf-8',csv([['id','title','text','threats'],['R1','Multi-factor authentication on remote access','Every remote access (VPN, remote desktop, administration) asks for a second factor.','remote-access:0.5:likelihood|brute-force:0.6:likelihood'],['R2','Offline backups','A copy of the backups is kept offline and restores are tested.','ransomware:0.5:impact|backup-destruction:0.6:impact']]));return;} + if(q('[data-rs-import]')){RS_IMPORT=q('[data-rs-import]').dataset.rsImport;$('#rsFile').value='';$('#rsFile').click();} +}); +det.addEventListener('change',async e=>{ + if(state.panel!=='risksettings'||!RSD)return;const t=e.target; + if(t.dataset.rsSize){const M=rsMatrix(),n=t.dataset.rsSize==='l'?+t.value:M.likelihood.length,m=t.dataset.rsSize==='i'?+t.value:M.impact.length;RSD.matrix=Object.assign(RK.defaultMatrix(n,m),{accept:M.accept,acceptByCrit:M.acceptByCrit});rsSave();return;} + if(t.dataset.rsAcceptCrit){const M=rsMatrix(),b=Object.assign({},M.acceptByCrit);if(t.value==='')delete b[t.dataset.rsAcceptCrit];else b[t.dataset.rsAcceptCrit]=+t.value;RSD.matrix=Object.assign(M,{acceptByCrit:b});rsSave();return;} + if(t.hasAttribute('data-rs-accept')){RSD.matrix=Object.assign(rsMatrix(),{accept:+t.value});rsSave();return;} + if(t.dataset.rsLabel){const [k,x]=t.dataset.rsLabel.split(':'),M=rsMatrix();M[k]=M[k].slice();M[k][+x]=t.value.trim()||M[k][+x];RSD.matrix=M;rsSave();return;} + if(t.dataset.rsBaseline){const s=new Set(rsEnabled());t.checked?s.add(t.dataset.rsBaseline):s.delete(t.dataset.rsBaseline);RSD.baselines=[...s];rsSave();return;} + if(t.dataset.rsThreat){const s=new Set(((RSD.threats||{}).disabled)||[]);t.checked?s.delete(t.dataset.rsThreat):s.add(t.dataset.rsThreat);RSD.threats=Object.assign({},RSD.threats,{disabled:[...s]});rsSave();return;} + if(t.id==='rsFile'&&t.files[0]){ + const f=t.files[0],text=await f.text();let o; + try{ + if(RS_IMPORT==='baseline'){o=/\.csv$/i.test(f.name)||!/^\s*[[{]/.test(text)?baselineFromCsv(text,f.name):JSON.parse(text); + if(!o||!Array.isArray(o.requirements)||!o.requirements.length)throw new Error('no requirement found'); + o.id=o.id||fileStem(o.name||f.name.replace(/\.\w+$/,'')).toLowerCase();o.name=o.name||f.name.replace(/\.\w+$/,''); + const id='custom:'+String(o.id).replace(/^custom:/,'');RSD.custom=(RSD.custom||[]).filter(b=>'custom:'+String(b.id).replace(/^custom:/,'')!==id).concat([o]);RSD.baselines=[...new Set([...rsEnabled(),id])]; + toast(`Baseline “${o.name}” imported: ${plural(o.requirements.length,'requirement')}.`);} + else if(RS_IMPORT==='threats'){o=JSON.parse(text);const l=Array.isArray(o)?o:Array.isArray(o.threats)?o.threats:[];if(!l.length)throw new Error('expected a list of threats'); + RSD.threats=Object.assign({},RSD.threats);const ids=new Set(l.map(x=>String(x.id)));RSD.threats.custom=(RSD.threats.custom||[]).filter(x=>!ids.has(String(x.id))).concat(l);toast(`${plural(l.length,'threat')} imported.`);} + else{o=JSON.parse(text);const r=o&&o.risk&&typeof o.risk==='object'?o.risk:o;if(!r||typeof r!=='object'||Array.isArray(r))throw new Error('expected a Strata risk settings export');RSD=clone(r);toast('Risk settings imported.');} + }catch(err){toast('Could not import this file: '+(err.message||'invalid')+'.',true);return;} + rsSave(); + } +}); +// A baseline as CSV: id, title, text, threats (threat:share:effect separated by |), comma or semicolon separated +function baselineFromCsv(text,file){ + const rows=[];let row=[],cell='',q=false;const sep=(text.split('\n')[0].match(/;/g)||[]).length>=(text.split('\n')[0].match(/,/g)||[]).length?';':','; + for(let k=0;kh.replace(/^/,'').trim().toLowerCase()),col=k=>head.indexOf(k); + if(col('id')<0||col('title')<0)throw new Error('the first line needs the columns id and title'); + return {name:file.replace(/\.\w+$/,''),requirements:rows.filter(r=>r[col('id')]&&r[col('title')]).map(r=>({id:r[col('id')].trim(),title:r[col('title')].trim(),text:col('text')>=0?r[col('text')]||'':'', + threats:col('threats')>=0?String(r[col('threats')]||'').split('|').map(x=>x.trim().split(':')).filter(x=>x[0]).map(([id,s,ef])=>[id,+s||0.3,ef==='impact'?'impact':'likelihood']):[]}))}; +} + +// The Baselines tab: the baselines in use, their order of preference, and the requirements that overlap between +// them, merged by a criterion or by hand. Every change is saved in the risk settings at once. +let RKB={q:'',base:'',show:'all',limit:30}; +const shortName=n=>String(n).split(/ — | \(|:/)[0].replace(/^ISO\/IEC /,'ISO ').replace(/ Critical Security Controls/,'').trim(); +function renderBaselinesView(tabs){ + const M=RS.merge,ro=Store.canWrite?'':' disabled',all=BASELINES().map(b=>RK.cleanBaseline(b,false)).concat(RS.custom); + const plan=RK.mergePlan(RS,BASELINES()),order=M.priority.filter(id=>RS.baselines.includes(id)); + const q=RKB.q.trim().toLowerCase(),merged=plan.groups.filter(g=>!g.separate).reduce((n,g)=>n+g.members.length-1,0); + const list=plan.groups.filter(g=>(!RKB.base||g.members.some(m=>m.baseline===RKB.base))&&(RKB.show==='all'||(RKB.show==='manual'&&g.manual)||(RKB.show==='separate'&&g.separate)) + &&(!q||g.members.some(m=>`${m.ref} ${m.title} ${m.baselineName}`.toLowerCase().includes(q)))); + const tname=id=>(RK.THREATS.concat(RS.customThreats).find(t=>t.id===id)||{name:id}).name; + const card=(g,n)=>{const th=[...new Set(g.members.flatMap(m=>m.threats.map(t=>t.id)))]; + return `
    Group ${n}${th.slice(0,6).map(id=>`${esc(tname(id))}`).join('')}${th.length>6?`+${th.length-6}`:''} +
    +
      ${g.members.map(m=>{const keep=!g.separate&&m.key===g.keep.key,sum=m.threats.reduce((x,t)=>x+t.strength,0); + return `
    • + ${keep?`Kept · ${esc(g.why)}`:''}
    • `;}).join('')}
    `;}; + lensBody.innerHTML=`${tabs}

    Baselines

    The baselines whose requirements are offered against each risk, in order of preference, and the requirements that say the same thing in two baselines or more. Merged, one requirement of each group stands for the others: it takes all their threats, and the others are no longer offered.

    +

    Baselines · ${RS.baselines.length} in use

    +
    ${all.map(b=>{const on=RS.baselines.includes(b.id),k=order.indexOf(b.id);return `
    + ${plural(b.requirements.length,'requirement')}${b.version?' · '+esc(b.version):''}${b.custom?' · custom':''}${on?`
    #${k+1}
    `:''}
    `;}).join('')}
    +

    Overlapping requirements

    +
    + + + + ${Object.keys(M.choices).length?``:''} +
    +

    ${{strict:'Most demanding: the requirement with the highest reduction score, the sum of the shares of risk it removes from each of its threats.',coverage:'Widest coverage: the one that addresses the most threats.',fit:'Best fit: the one that reduces most the threats found on the assets of this map, weighed by their severity.',baseline:'Preferred baseline: the one from the baseline highest in the order above.'}[M.criterion]} Pick another one in a group to choose by hand.

    +

    ${plural(plan.groups.length,'group')} found among ${plural(plan.total,'requirement')}. ${M.on?`${merged} ${merged===1?'requirement is':'requirements are'} merged into another one: ${plan.total-merged} offered.`:'Merging is off: every requirement is offered.'}

    +
    + + + ${plural(list.length,'group')}
    + ${RS.baselines.length<2?'

    Enable two baselines or more to find the requirements they share.

    ':list.length?`
    ${list.slice(0,RKB.limit).map((g,i)=>card(g,plan.groups.indexOf(g)+1)).join('')}
    ${list.length>RKB.limit?`
    `:''}`:'

    No group matches.

    '}`; +} +// Changes to the risk settings made from the Baselines tab +function rkSaveMerge(fn){const d=clone(raw.risk||{});d.merge=Object.assign({},d.merge);fn(d.merge,d);Store.putRisk(d).catch(()=>{});} +lensEl.addEventListener('click',e=>{ + if(state.lens!=='risk'||RKV!=='baselines'||!SITE)return;const q=s=>e.target.closest(s); + if(q('[data-ov-more]')){RKB.limit+=30;renderRiskLens();return;} + if(q('[data-ov-reset]')){rkSaveMerge(m=>{m.choices={};});return;} + const mv=q('[data-bl-up],[data-bl-down]'); + if(mv){const id=mv.dataset.blUp||mv.dataset.blDown,up=!!mv.dataset.blUp;rkSaveMerge(m=>{const o=RS.merge.priority.filter(x=>RS.baselines.includes(x)),k=o.indexOf(id),j=up?k-1:k+1;if(k<0||j<0||j>=o.length)return;[o[k],o[j]]=[o[j],o[k]];m.priority=o;});} +}); +lensEl.addEventListener('change',e=>{ + if(state.lens!=='risk'||RKV!=='baselines'||!SITE)return;const t=e.target; + if(t.dataset.blOn){const id=t.dataset.blOn,on=t.checked;rkSaveMerge((m,d)=>{const s=new Set(RS.baselines);on?s.add(id):s.delete(id);d.baselines=[...s];});return;} + if(t.hasAttribute('data-ov-on')){const on=t.checked;rkSaveMerge(m=>{m.on=on;});return;} + if(t.hasAttribute('data-ov-crit')){const v=t.value;rkSaveMerge(m=>{m.criterion=v;});return;} + if(t.hasAttribute('data-ov-thr')){const v=+t.value;rkSaveMerge(m=>{m.threshold=v;});return;} + if(t.hasAttribute('data-ov-keep')){const g=t.closest('[data-group]').dataset.group,k=t.value;rkSaveMerge(m=>{m.choices=Object.assign({},m.choices,{[g]:k});});return;} + if(t.hasAttribute('data-ov-separate')){const g=t.closest('[data-group]').dataset.group,on=t.checked;rkSaveMerge(m=>{const s=new Set(Array.isArray(m.separate)?m.separate:[]);on?s.add(g):s.delete(g);m.separate=[...s];});return;} + if(t.hasAttribute('data-ov-base')){RKB.base=t.value;RKB.limit=30;renderRiskLens();return;} + if(t.hasAttribute('data-ov-show')){RKB.show=t.value;RKB.limit=30;renderRiskLens();} +}); +let ovT=null; +lensEl.addEventListener('input',e=>{ + if(e.target.id!=='ovQ')return;RKB.q=e.target.value;RKB.limit=30;clearTimeout(ovT); + ovT=setTimeout(()=>{const pos=e.target.selectionStart;renderRiskLens();const i=$('#ovQ');if(i){i.focus();i.setSelectionRange(pos,pos);}},200); +}); + +/* ---- Logs for the SIEM ---- */ +// What each asset must send to the SIEM so that its risks can be seen: the log sources it has, the events of the +// threats that apply to it, at the verbosity its residual risks call for (logs.js) +const LG=StrataLogs; +const vTag=l=>`${LG.VERBOSITY[l]}`; +function logsOf(a){ + const out=SITE.flows.some(f=>f.src===a.id&&isNet(f.dst)); + return LG.forAsset(a,SITE.threats[a.id]||[],{outbound:out,bizCrit:SITE.bizCrit[a.id]}); +} +function assetLogsHtml(a){ + const L=logsOf(a); + if(!L.sources.length)return '

    No risk on this asset calls for logs.

    '; + return `

    ${L.events} event ${L.events===1?'family':'families'} from ${plural(L.sources.length,'source')}, kept at least ${L.retention} months. The verbosity follows the residual risk: low asks for the minimal level, moderate for standard, high and critical for detailed.

    + ${L.sources.map(s=>`
    ${esc(s.source.name)}${vTag(s.level)}

    ${esc(s.settings)}. Sent by ${esc(s.source.transport)}.

    +
      ${s.events.map(x=>`
    • ${esc(x.event.name)} ${esc(x.event.ref)}${esc([...new Set(x.risks.map(r=>r.threat.name))].join(' · '))}
    • `).join('')}
    `).join('')} + ${L.uncovered.length?`

    No log helps see: ${esc(L.uncovered.map(r=>r.threat.name).join(', '))}.

    `:''}`; +} +function renderLogsView(tabs){ + const rows=SITE.assets.map(a=>({a,L:logsOf(a)})).filter(x=>x.L.sources.length).sort((x,y)=>y.L.events-x.L.events||x.a.name.localeCompare(y.a.name)); + const per={};rows.forEach(({a,L})=>L.sources.forEach(s=>{const p=per[s.source.id]||(per[s.source.id]={src:s.source,assets:0,level:0,events:new Set()});p.assets++;p.level=Math.max(p.level,s.level);s.events.forEach(x=>p.events.add(x.event.id));})); + lensBody.innerHTML=`${tabs}

    Logs for the SIEM

    The minimum each asset must send so that its risks can be seen happening: only the log sources it has, and only the events of the threats that apply to it, at the verbosity its residual risk calls for (low: minimal, moderate: standard, high and critical: detailed). Keep them at least 6 months, a year where a high or critical risk remains.

    +
    + ${rows.length?`

    ${plural(rows.length,'asset')} to connect · ${Object.keys(per).length} ${Object.keys(per).length===1?'kind':'kinds'} of source · ${rows.reduce((n,x)=>n+x.L.events,0)} event families in all.

    +

    By source

    + + ${Object.values(per).sort((x,y)=>y.assets-x.assets).map(p=>``).join('')}
    SourceAssetsHighest verbosityEventsSent by
    ${esc(p.src.name)}${p.assets}${vTag(p.level)}${p.events.size}${esc(p.src.transport)}
    +

    By asset

    + + ${rows.map(({a,L})=>``).join('')} +
    AssetSources and verbosityEventsKeep
    ${esc(a.name)}${esc(TYPE_LABEL[a.type]||'')}${L.sources.map(s=>`${esc(s.source.name)} ${vTag(s.level)}`).join('')}${L.events}${L.retention} months
    `:'

    No risk on this IS calls for logs yet.

    '}`; +} +lensEl.addEventListener('click',e=>{ + if(state.lens!=='risk'||RKV!=='logs'||!SITE)return;const q=s=>e.target.closest(s); + if(q('[data-csv-siem]')){exportSiem();return;} + const r=q('[data-goto-logs]');if(r){const a=byId[r.dataset.gotoLogs];if(a){state.selId=a.id;showAsset(a);const h=$('#logsCap');if(h)h.scrollIntoView({block:'start'});}} +}); +function exportSiem(){ + const rows=[['Asset','IP','Type','Source','Verbosity','Setting','Event','Reference','Risks it helps see','Sent by','Keep (months)']]; + SITE.assets.forEach(a=>{const L=logsOf(a);L.sources.forEach(s=>s.events.forEach(x=>rows.push([a.name,a.ip||'',TYPE_LABEL[a.type]||a.type,s.source.name,s.label,s.settings,x.event.name,x.event.ref,[...new Set(x.risks.map(r=>r.threat.name))].join('; '),s.source.transport,L.retention])));}); + if(rows.length===1){toast('No log to send on this IS.',true);return;} + saveFile(`siem-plan_${fileStem(SITE.name)}_${today()}.csv`,'text/csv;charset=utf-8',csv(rows)); +} + +/* ================= Dashboard and KPIs ================= */ +// Key figures over time for a steering committee: exposure, vulnerability age, owner and scan coverage +// (kpi.js). On a Strata server they are measured from the history of the map; in a browser-only map, from a +// snapshot this page keeps each day. +const KP=StrataKpi,KPI_KEY='strata-kpi'; +const DASH={scope:'all',period:'q',data:null,loading:false}; +const PERIODS={q:{label:'12 weeks',points:13,step:7},h:{label:'6 months',points:14,step:14},y:{label:'12 months',points:13,step:30}}; +const kpiLocal=()=>{try{return JSON.parse(localStorage.getItem(KPI_KEY)||'{}')||{};}catch(e){return {};}}; +const kpiSaveLocal=o=>{try{localStorage.setItem(KPI_KEY,JSON.stringify(o));}catch(e){}}; +const dayOf=t=>new Date(t).toISOString().slice(0,10); +function kpiMeasureAll(r,at,seen){const out={total:KP.measure(r,at,seen),sites:{}};Object.keys(r.si||{}).forEach(id=>out.sites[id]=KP.measure(r,at,seen,id));return out;} +// Browser-only map: note when each vulnerability is first seen, and keep one snapshot a day +function kpiTrack(){ + if(Store.mode!=='local'||Store.view||!Object.keys(liveRaw.si||{}).length)return; + const o=kpiLocal(),now=Date.now(),iso=new Date(now).toISOString();o.seen=o.seen||{};o.days=o.days||{}; + Object.entries(liveRaw.assets).forEach(([id,a])=>(a.vulns||[]).forEach(v=>{const k=`${id}|${v.id||v.title||''}`;if(!o.seen[k])o.seen[k]={at:v.firstSeen||iso};})); + const m=kpiMeasureAll(liveRaw,now,o.seen);delete m.total.oldest;Object.values(m.sites).forEach(x=>delete x.oldest); + o.days[dayOf(now)]=m; + const keep=Object.keys(o.days).sort().slice(-400);o.days=Object.fromEntries(keep.map(k=>[k,o.days[k]])); + kpiSaveLocal(o); +} +// The demo comes with its past: the map as it was each week, rebuilt from the dates its vulnerabilities were found +function kpiSeedDemo(data){ + const o={seen:{},days:{},demo:true},now=Date.now(); + for(let k=52;k>=1;k--){const at=now-k*7*KP.DAY,r={si:data.si,assets:{}}; + Object.entries(data.assets).forEach(([id,a])=>{r.assets[id]=Object.assign({},a,{vulns:(a.vulns||[]).filter(v=>!v.firstSeen||Date.parse(v.firstSeen)<=at)});}); + const m=kpiMeasureAll(r,at,{});delete m.total.oldest;Object.values(m.sites).forEach(x=>delete x.oldest);o.days[dayOf(at)]=m;} + kpiSaveLocal(o); +} +async function kpiLoad(){ + const P=PERIODS[DASH.period];DASH.loading=true; + try{ + if(Store.mode==='api'){const j=await Store.api('GET',`api/kpi?points=${P.points}&step=${P.step}`);DASH.data={points:j.points,since:j.since,source:'history'};} + else{ + kpiTrack();const o=kpiLocal(),days=Object.keys(o.days||{}).sort(),now=Date.now(),pts=[]; + KP.points(now,P.points,P.step).forEach((at,i,all)=>{ + if(i===all.length-1){const m=kpiMeasureAll(liveRaw,now,o.seen||{});pts.push({at:new Date(now).toISOString(),total:m.total,sites:m.sites});return;} + // the last snapshot of the period, if there is one + const d=days.filter(x=>x<=dayOf(at)).pop();if(d&&Date.parse(d)>at-P.step*KP.DAY)pts.push(Object.assign({at:new Date(at).toISOString()},o.days[d]));}); + DASH.data={points:pts,since:days[0]||null,source:o.demo?'demo':'local'}; + } + }catch(e){DASH.data={points:[],error:e.message||'error'};} + DASH.loading=false; +} +async function openDash(){ + $('#dash').hidden=false;document.body.classList.add('has-dash'); + if(state.view==='site'&&SITE&&DASH.scope==='all'&&!DASH.chosen)DASH.scope=SITE.id; + $('#dashBody').innerHTML='

    Measuring…

    ';await kpiLoad();renderDash(); +} +function closeDash(){$('#dash').hidden=true;document.body.classList.remove('has-dash');} + +/* ---- Charts: inline SVG, one y axis, thin marks, a on every mark for its value ---- */ +const fmtD=t=>new Date(t).toLocaleDateString(undefined,{day:'numeric',month:'short'}); +const W0=560,H0=210,PL=40,PR=74,PT=14,PB=30; +function chartFrame(pts,max,unit){ + const n=pts.length,X0=PL+14,X1=W0-PR-10,x=i=>X0+(n<2?(X1-X0)/2:i*(X1-X0)/(n-1)),y=v=>PT+(H0-PT-PB)*(1-v/max); + let g='';[0,0.25,0.5,0.75,1].forEach(k=>{const v=max*k,yy=y(v);g+=`<line x1="${PL}" x2="${W0-PR}" y1="${yy}" y2="${yy}" class="grid"/><text x="${PL-6}" y="${yy+3.5}" text-anchor="end" class="ax">${Math.round(v*10)/10}${unit||''}</text>`;}); + const ticks=n<2?[0]:[0,Math.floor((n-1)/2),n-1];ticks.forEach(i=>g+=`<text x="${x(i)}" y="${H0-8}" text-anchor="middle" class="ax">${fmtD(pts[i].at)}</text>`); + return {x,y,g}; +} +// The top of the axis: four equal round steps, just above the highest value +const niceMax=(v,pct)=>{if(pct)return 100;const r=Math.max(1,v)/4,m=Math.pow(10,Math.floor(Math.log10(r)));return 4*[1,1.2,1.5,2,2.5,3,4,5,6,8,10].find(k=>k*m>=r)*m;}; +function lineChart(pts,series,{pct,unit}={}){ + const vals=series.flatMap(s=>pts.map(p=>s.get(p))).filter(v=>v!=null),max=niceMax(Math.max(1,...vals),pct),F=chartFrame(pts,max,unit); + let o=F.g; + series.forEach(s=>{const P=pts.map((p,i)=>({x:F.x(i),v:s.get(p),at:p.at})).filter(q=>q.v!=null);if(!P.length)return; + o+=`<polyline points="${P.map(q=>`${q.x},${F.y(q.v)}`).join(' ')}" fill="none" stroke="var(${s.color})" stroke-width="2" stroke-linejoin="round"/>`; + P.forEach((q,i)=>o+=`<circle cx="${q.x}" cy="${F.y(q.v)}" r="${i===P.length-1?4:3}" fill="var(${s.color})" stroke="var(--panel)" stroke-width="2"><title>${esc(s.name)}, ${fmtD(q.at)}: ${q.v}${unit||''}`); + const L=P[P.length-1];o+=`${L.v}${unit||''}`;}); + return `${o}${series.length>1?`

    ${series.map(s=>`${esc(s.name)}`).join('')}

    `:''}`; +} +function stackChart(pts,buckets){ + const tot=pts.map(p=>buckets.reduce((n,b)=>n+b.get(p),0)),max=niceMax(Math.max(1,...tot)),F=chartFrame(pts,max); + const n=pts.length,bw=Math.max(6,Math.min(26,(W0-PL-PR)/Math.max(1,n)*0.6));let o=F.g; + pts.forEach((p,i)=>{let acc=0;buckets.forEach(b=>{const v=b.get(p);if(!v)return;const y1=F.y(acc+v),y0=F.y(acc);acc+=v; + o+=`${fmtD(p.at)} · ${esc(b.name)}: ${v}`;}); + if(i===n-1)o+=`${acc}`;}); + return `${o}

    ${buckets.map(b=>`${esc(b.name)}`).join('')}

    `; +} +function spark(vals){ + const v=vals.filter(x=>x!=null);if(v.length<2)return '';const lo=Math.min(...v),hi=Math.max(...v),r=hi-lo||1; + return ``; +} + +function renderDash(){ + const D=DASH.data||{points:[]},pts=D.points.map(p=>Object.assign({at:p.at},DASH.scope==='all'?p.total:(p.sites||{})[DASH.scope])).filter(p=>p&&p.assets!=null); + const cur=pts[pts.length-1],first=pts[0],sites=Object.entries(liveRaw.si||{}).sort((a,b)=>(a[1].order||0)-(b[1].order||0)); + const scopeName=DASH.scope==='all'?'Whole map':`IS ${(liveRaw.si[DASH.scope]||{}).name||DASH.scope}`; + const head=`
    Steering committeeSecurity dashboard · ${esc(scopeName)}
    + +
    ${Object.entries(PERIODS).map(([k,p])=>``).join('')}
    +
    `; + if(!cur){$('#dashBody').innerHTML=head+`

    ${D.error?esc('Could not measure: '+D.error):'No IS on the map yet.'}

    `;return;} + // a tile: the value now, its change over the period, said in words, and its trend + const tile=(label,get,{unit='',better='down',sub}={})=>{const v=get(cur),v0=get(first),d=v!=null&&v0!=null&&pts.length>1?Math.round((v-v0)*10)/10:null; + const good=d==null||d===0?null:(better==='down')===(d<0); + return `
    ${esc(label)}${v==null?'—':v}${v==null?'':unit}${sub?`${sub}`:''} + ${d==null?'No earlier value':`${d===0?'No change':`${d>0?'▲ +':'▼ '}${d}${unit}`} since ${fmtD(first.at)}${good==null?'':good?' · better':' · worse'}`} + ${spark(pts.map(get))}
    `;}; + const V=p=>p.vulns,E=p=>p.exposure; + const tiles=[ + tile('Internet-facing assets',p=>E(p).assets,{sub:`${cur.exposure.share??0}% of ${cur.assets} assets · ${cur.exposure.ports} ports`}), + tile('Admin ports open to the Internet',p=>E(p).admin,{sub:'Assets with SSH, RDP, SMB, WinRM, VNC or Telnet reachable from the Internet'}), + tile('Open critical and high',p=>V(p).crit+V(p).high,{sub:`${cur.vulns.crit} critical · ${cur.vulns.high} high · ${cur.vulns.kev} on the CISA KEV list`}), + tile('Median age, critical and high',p=>V(p).median,{unit:' d',sub:'Days since first seen'}), + tile('Past their deadline',p=>V(p).late,{sub:`Critical over ${KP.SLA.crit} d, high over ${KP.SLA.high} d, medium over ${KP.SLA.med} d, low over ${KP.SLA.low} d`}), + tile('Owner coverage',p=>p.owners.share,{unit:'%',better:'up',sub:`${cur.owners.covered} of ${cur.assets} assets have a named owner · critical assets: ${cur.owners.critical?Math.round(100*cur.owners.criticalCovered/cur.owners.critical):'—'}%`}), + tile('Scan coverage',p=>p.scans.share,{unit:'%',better:'up',sub:`${cur.scans.covered} of ${cur.assets} assets known to a vulnerability scanner`}), + ]; + const ageCols=['--a1','--a2','--a3','--a4']; + const ownCrit=p=>p.owners.critical?Math.round(1000*p.owners.criticalCovered/p.owners.critical)/10:null; + const charts=`
    +

    Exposure to the Internet

    Assets the Internet reaches directly, and those with an administration port open to it

    ${lineChart(pts,[{name:'Internet-facing assets',get:p=>E(p).assets,color:'--k1'},{name:'With an admin port open',get:p=>E(p).admin,color:'--k2'}])}
    +

    Age of open critical and high vulnerabilities

    How long they have been open, at the end of each period

    ${stackChart(pts,KP.AGES.map(([,,name],k)=>({name,get:p=>p.vulns.ages[k],color:ageCols[k]})))}
    +

    Owner coverage

    Share of assets with a named technical or business owner

    ${lineChart(pts,[{name:'Every asset',get:p=>p.owners.share,color:'--k1'},{name:'Critical assets',get:ownCrit,color:'--k2'}],{pct:true,unit:'%'})}
    +

    Scan coverage

    Share of assets a vulnerability scanner knows

    ${lineChart(pts,[{name:'Scan coverage',get:p=>p.scans.share,color:'--k1'}],{pct:true,unit:'%'})}
    `; + // the IS side by side, now + const now=D.points[D.points.length-1],rows=sites.map(([id,d])=>[d.name||id,now.sites&&now.sites[id]]).filter(([,m])=>m&&m.assets); + const cmp=DASH.scope!=='all'||rows.length<2?'':`

    Information systems now

    + ${rows.map(([n,m])=>``).join('')}
    ISAssetsInternet-facingAdmin ports openCritical and highMedian agePast deadlineOwnersScanned
    ${esc(n)}${m.assets}${m.exposure.assets}${m.exposure.admin}${m.vulns.crit+m.vulns.high}${m.vulns.median==null?'—':m.vulns.median+' d'}${m.vulns.late}${m.owners.share??'—'}%${m.scans.share??'—'}%
    `; + const old=(cur.oldest||[]).length?`

    Oldest open critical and high vulnerabilities

    + ${cur.oldest.map(v=>``).join('')}
    VulnerabilityAssetSeverityOpen forDeadline
    ${esc(v.id||'—')} ${esc(v.title)}${v.kev?' KEV':''}${esc(v.name)}${tag(v.sev)}${v.approx?'≥ ':''}${v.days} d${v.late?`${v.days-KP.SLA[v.sev]} d late`:'On time'}
    `:''; + const note=`

    ${D.source==='history'?`Measured on the map as it was at the end of each period, rebuilt from its history${D.since?` (kept since ${fmtD(D.since)})`:''}.`:D.source==='demo'?'The past of the demo map is rebuilt from the dates its vulnerabilities were first seen; exposure, owners and scans did not change.':'This map is kept in this browser: one snapshot is kept each day it is opened.'} A vulnerability's age counts from the first version where it appears, or the date its scanner gives${cur.vulns.approx?'; “≥” marks those already there when the history starts':''}. Deadlines: ${Object.entries(KP.SLA).map(([k,v])=>`${SEV[k].label.toLowerCase()} ${v} days`).join(', ')}. Generated ${new Date().toLocaleString()}.

    `; + $('#dashBody').innerHTML=head+`
    ${tiles.join('')}
    `+charts+cmp+old+note; +} +function exportDash(){ + const D=DASH.data;if(!D||!D.points.length)return; + const rows=[['Date','Scope','Assets','Internet-facing assets','Internet-facing ports','Admin ports open to the Internet','Risky flows','Open vulnerabilities','Critical','High','Medium','Low','On CISA KEV','Median age of critical and high (days)','Critical and high under 30 d','30 to 90 d','90 to 180 d','Over 180 d','Past deadline','Assets with an owner','Owner coverage (%)','Assets scanned','Scan coverage (%)']]; + const line=(at,scope,m)=>rows.push([at.slice(0,10),scope,m.assets,m.exposure.assets,m.exposure.ports,m.exposure.admin,m.exposure.risky,m.vulns.open,m.vulns.crit,m.vulns.high,m.vulns.med,m.vulns.low,m.vulns.kev,m.vulns.median??'',...m.vulns.ages,m.vulns.late,m.owners.covered,m.owners.share??'',m.scans.covered,m.scans.share??'']); + D.points.forEach(p=>{line(p.at,'Whole map',p.total);Object.entries(p.sites||{}).forEach(([id,m])=>line(p.at,(liveRaw.si[id]||{}).name||id,m));}); + saveFile(`kpi_${today()}.csv`,'text/csv;charset=utf-8',csv(rows)); +} +$('#dash').addEventListener('click',async e=>{ + const b=e.target.closest('[data-dash]'),pd=e.target.closest('[data-dash-period]'); + if(pd){DASH.period=pd.dataset.dashPeriod;await kpiLoad();renderDash();return;} + if(!b)return;const k=b.dataset.dash; + if(k==='close')closeDash();else if(k==='csv')exportDash();else if(k==='print')print(); +}); +$('#dash').addEventListener('change',e=>{if(e.target.hasAttribute('data-dash-scope')){DASH.scope=e.target.value;DASH.chosen=true;renderDash();}}); +addEventListener('keydown',e=>{if(e.key==='Escape'&&!$('#dash').hidden)closeDash();}); + /* ================= Navigation ================= */ let fly=null; function flyTo(target,dist){const off=camera.position.clone().sub(controls.target);off.setLength(dist||Math.min(off.length(),28)); @@ -3558,11 +4121,11 @@ function selectSite(s,focus){ } function enterSite(s){ if(!s)return; - state.view='site';state.siteId=s.id;state.ovSelId=s.id;state.selId=null;state.path=false;state.hidden=new Set();state.biz=null; + state.view='site';state.siteId=s.id;state.ovSelId=s.id;state.selId=null;state.path=false;state.hidden=new Set();state.focus=null;state.selView=null;state.biz=null; $('#pathBtn').setAttribute('aria-pressed','false'); setSite(s);closeDet();flatFitFor=null;showFlatUI();renderVlanList();buildScene();layout();refresh();renderCart();syncUI();setViewNow();renderEmpty(); } -function backToOverview(){ +function backToOverview(){state.focus=null;renderFocusBar(); if(!$('#archf').hidden)closeArchFile(); if(state.arrange)setArrange(false); const id=state.siteId;state.view='overview';state.selId=null;state.siteId=null; @@ -3573,11 +4136,18 @@ function backToOverview(){ function select(a,focus){ if(a&&state.path){state.path=false;$('#pathBtn').setAttribute('aria-pressed','false');} if(a&&state.hidden.has(a.vlan)){state.hidden.delete(a.vlan);const cb=$('#vl-'+a.vlan);if(cb)cb.checked=true;} + // the view before the first asset selected, to zoom back out to it once nothing is selected + if(a&&focus&&!state.selView)state.selView={view3d:{t:controls.target.clone(),c:camera.position.clone()},view2d:Object.assign({},flatView)}; state.selId=a?a.id:null; - if(a){showAsset(a);if(focus&&a.group&&!state.flat){const p=new THREE.Vector3();a.group.getWorldPosition(p);p.y+=a.h/2;flyTo(p);}} + if(a){showAsset(a);if(focus&&a.group&&!state.flat){const p=new THREE.Vector3();a.group.getWorldPosition(p);p.y+=a.h/2;flyTo(p,14);}} else if(!state.path&&state.panel!=='ports')closeDet(); - refresh();if(a&&focus&&state.flat)centerFlatOn(a);closeCtlMobile(!!a); + refresh();if(a&&focus&&state.flat)zoomFlatOn(a);closeCtlMobile(!!a); + if(!a&&state.selView){const v=state.selView;state.selView=null; + if(state.flat){if(v.view2d&&v.view2d.k)glideFlat(v.view2d);} + else fly={t0:performance.now(),dur:reduceMotion?1:800,fromT:controls.target.clone(),fromC:camera.position.clone(),toT:v.view3d.t.clone(),toC:v.view3d.c.clone()};} } +// In the 2D diagram: centre on the asset and zoom in, gliding like the 3D camera +function zoomFlatOn(a){const g=FL&&FL.assets[a.id];if(!g)return;const vp=flatViewport(),k=Math.min(60,Math.max(flatView.k*1.5,32));glideFlat({k,tx:vp.x+vp.w/2-g.x*k,ty:vp.y+vp.h/2-g.y*k});} function setPath(on){state.path=on;$('#pathBtn').setAttribute('aria-pressed',String(on));if(on){state.selId=null;showPath();}else if(!selAsset())closeDet();refresh();} function closeCtlMobile(cond){if(cond&&innerWidth<=760){document.body.classList.remove('ctl-open');$('#toggleCtl').setAttribute('aria-expanded','false');}} @@ -3590,7 +4160,9 @@ function onData(){ const s=siteById[state.siteId]; if(!s){toast('This IS was deleted.');backToOverview();runPending();return;} if(state.biz&&!s.bizById[state.biz])state.biz=null; + if(state.focus&&!s.vlanById[state.focus.vlan]){state.hidden=new Set([...state.focus.prev]);state.focus=null;} setSite(s);renderVlanList();buildScene();layout();refresh(); + renderFocusBar(); if(state.selId&&!byId[state.selId])state.selId=null; if(state.panel!=='form'){ if(state.panel==='asset'){const a=selAsset();if(a)showAsset(a);else closeDet();} @@ -3600,6 +4172,8 @@ function onData(){ else if(state.panel==='attack')showAttack(); else if(state.panel==='incident')showIncident(true); else if(state.panel==='ai'){const a=byId[AI_FOR];if(a)showPrompts(a,true);else closeDet();} + else if(state.panel==='threat'&&THREAT_FOR){if(byId[THREAT_FOR.aid])showThreat(THREAT_FOR.aid,THREAT_FOR.tid,true);else closeDet();} + else if(state.panel==='landscape')showLandscape(); else if(state.panel==='business'){const b=SITE.bizById[state.bizPanel];if(b)showBusiness(b);else closeDet();} } }else{ @@ -3610,13 +4184,60 @@ function onData(){ refreshOverview(); } if(state.panel==='proposal'&&Store.prop)showProposal(); - renderServiceLists();renderCollab(); + // the risk settings follow the saved document, unless a label is being typed + if(state.panel==='risksettings'&&!(det.contains(document.activeElement)&&document.activeElement.matches('input,textarea'))){RSD=clone(raw.risk||{});showRiskSettings(true);} + renderServiceLists();renderCollab();kpiTrack(); lastCount=SITES.length;renderCart();syncUI();renderEmpty();runPending(); } function runPending(){if(pendingAfterData&&!onSave){const f=pendingAfterData;pendingAfterData=null;f();}} // After a save: close the form and refresh as soon as the data is read back function afterSave(fn){onSave=null;onCancel=null;pendingAfterData=fn;scheduleData(0);} +/* ================= One VLAN alone ================= */ +// A click on a plate shows that VLAN alone: the other plates and their assets disappear, with the flows to them. +// The VLANs shown before, and the view, come back with "Show every VLAN", Escape, a click on an empty space or a +// second click on the plate. +function focusVlan(id){ + if(!vlanById[id])return; + if(state.focus&&state.focus.vlan===id){unfocusVlan();return;} + // the view before the first VLAN shown alone, to come back to it + const was=state.focus||{prev:new Set(state.hidden),view3d:{t:controls.target.clone(),c:camera.position.clone()},view2d:Object.assign({},flatView)}; + state.focus=Object.assign({},was,{vlan:id});state.hidden=new Set(VLANS.map(v=>v.id).filter(x=>x!==id)); + const a=selAsset();if(a&&a.vlan!==id)select(null); + renderVlanList();refresh();renderFocusBar(); + if(state.flat){const P=FL&&FL.plates[id];if(P){const vp=flatViewport(),k=Math.max(6,Math.min(60,Math.min(vp.w/(P.w+2),vp.h/(P.h+3)))); + glideFlat({k,tx:vp.x+(vp.w-P.w*k)/2-P.x*k,ty:vp.y+(vp.h-P.h*k)/2-P.y*k});}} + else{const fl=floors[id];if(fl){const c=new THREE.Box3().setFromObject(fl.slab).getCenter(new THREE.Vector3());flyTo(c,Math.max(18,Math.max(fl.w,fl.D)*1.3));}} +} +// The 2D diagram moves to a view in a quarter of a second, as the 3D camera flies +let glide=0; +function glideFlat(to){ + cancelAnimationFrame(glide);const from=Object.assign({},flatView),t0=performance.now(),dur=reduceMotion?1:300; + const step=t=>{const k=Math.min(1,(t-t0)/dur),e=k<.5?2*k*k:1-Math.pow(-2*k+2,2)/2; + flatView={k:from.k+(to.k-from.k)*e,tx:from.tx+(to.tx-from.tx)*e,ty:from.ty+(to.ty-from.ty)*e};applyFlatView();if(k<1)glide=requestAnimationFrame(step);}; + glide=requestAnimationFrame(step); +} +function unfocusVlan(){ + if(!state.focus)return; + const f=state.focus;state.hidden=new Set([...f.prev].filter(id=>vlanById[id]));state.focus=null; + renderVlanList();refresh();renderFocusBar(); + // back to the view from before, zooming out as it zoomed in + if(state.flat){if(f.view2d&&f.view2d.k)glideFlat(f.view2d);else fitFlat();} + else if(f.view3d)fly={t0:performance.now(),dur:reduceMotion?1:800,fromT:controls.target.clone(),fromC:camera.position.clone(),toT:f.view3d.t.clone(),toC:f.view3d.c.clone()}; +} +function renderFocusBar(){ + const b=$('#focusBar'),v=state.focus&&state.view==='site'&&vlanById[state.focus.vlan]; + if(!v){b.hidden=true;return;} + const n=ASSETS.filter(a=>a.vlan===v.id).length; + b.innerHTML=`VLAN ${esc(v.num)} · ${esc(v.name)} alone · ${plural(n,'asset')}${v.cidr?` · ${esc(v.cidr)}`:''}`; + b.hidden=false;placeFocusBar(); +} +// Under the view switcher, whose height changes with the 2D tools +function placeFocusBar(){const b=$('#focusBar');if(b.hidden||innerWidth<=760){b.style.top='';return;}b.style.top=($('#viewSw').getBoundingClientRect().bottom+8)+'px';} +addEventListener('resize',placeFocusBar); +$('#focusBar').addEventListener('click',e=>{if(e.target.closest('[data-unfocus]'))unfocusVlan();}); +addEventListener('keydown',e=>{if(e.key==='Escape'&&state.focus&&!state.panel&&!e.target.closest('input,textarea,select'))unfocusVlan();}); + /* ================= Pointer ================= */ const ray=new THREE.Raycaster(),ndc=new THREE.Vector2(),tip=$('#tip'); function isShown(o){while(o){if(!o.visible)return false;o=o.parent;}return true;} @@ -3646,9 +4267,11 @@ renderer.domElement.addEventListener('pointerup',e=>{ if(state.panel==='form')return; const u=pick(e); if(state.view==='overview'){if(u&&u.site){if(state.ovSelId===u.site.id)enterSite(u.site);else selectSite(u.site,false);}else if(state.ovSelId)selectSite(null);return;} - if(u&&u.asset)select(u.asset,false); + if(u&&u.asset)select(u.asset,true); + else if(u&&u.plate&&!state.arrange)focusVlan(u.plate); else if(u&&u.svc)showService(u.svc); else if(u&&u.flow)showFlow(u.flow); + else if(!u&&state.focus){if(state.selId)select(null);unfocusVlan();} else if(state.selId)select(null);else if(state.panel==='flow')closeDet();}); renderer.domElement.addEventListener('pointermove',e=>{ if(drag){ @@ -3664,6 +4287,7 @@ renderer.domElement.addEventListener('pointermove',e=>{ else if(u&&u.svc){const x=u.svc;html=`${esc(x.name)} · ${esc(x.hosts.join(', '))} · ${esc(x.ports.join(' '))}`;} else if(u&&u.flow){const f=u.flow;html=`${flowPath(f)} · ${esc(f.port)} · ${f.vol} MB/h · ${EXP[f.level].label}${f.risk?' · risky':''}`;} else if(u&&u.plate&&state.arrange){const v=vlanById[u.plate];html=`VLAN ${esc(v.num)} · ${esc(v.name)} · drag to move`;} + else if(u&&u.plate){const v=vlanById[u.plate];html=`VLAN ${esc(v.num)} · ${esc(v.name)} · ${state.focus&&state.focus.vlan===v.id?'click to show every VLAN again':'click to show it alone'}`;} if(state.arrange&&u&&(u.plate||u.asset)&&state.mode==='zones')cur='ew-resize';else if(html)cur='pointer'; renderer.domElement.style.cursor=cur; if(html){tip.innerHTML=html;tip.hidden=false;tip.style.left=Math.min(e.clientX+14,innerWidth-tip.offsetWidth-8)+'px';tip.style.top=(e.clientY+14)+'px';}else tip.hidden=true; @@ -3727,7 +4351,7 @@ const rerender=()=>state.view==='site'?refresh():refreshOverview(); const segPress=(sel,b)=>document.querySelectorAll(sel+' button').forEach(x=>x.setAttribute('aria-pressed',String(x===b))); $('#cart').addEventListener('click',e=>{if(e.target.closest('[data-back]'))backToOverview();}); $('#siteList').addEventListener('click',e=>{const b=e.target.closest('[data-site]');if(b)selectSite(siteById[b.dataset.site],true);}); -$('#vlans').addEventListener('change',e=>{const id=e.target.dataset.vlan;if(!id)return;e.target.checked?state.hidden.delete(id):state.hidden.add(id); +$('#vlans').addEventListener('change',e=>{const id=e.target.dataset.vlan;if(!id)return;if(state.focus){state.focus=null;renderFocusBar();}e.target.checked?state.hidden.delete(id):state.hidden.add(id); const a=selAsset();if(a&&state.hidden.has(a.vlan))select(null);else refresh();}); $('#vlans').addEventListener('click',e=>{const rz=e.target.closest('[data-reset-zone]');if(rz){resetPlaces(rz.dataset.resetZone);return;}const m=e.target.closest('[data-mv]');if(m){moveVlan(m.dataset.mv,+m.dataset.d);return;}const b=e.target.closest('[data-edit-vlan]');if(b)vlanForm(vlanById[b.dataset.editVlan]);}); $('#spaceSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const c=b.dataset.space==='compact';if(c===state.compact)return;state.compact=c;segPress('#spaceSeg',b);buildScene();layout();refresh();resetView();}); @@ -3818,8 +4442,11 @@ function tick(t){ timer.update(t);const dt=Math.min(timer.getDelta(),0.05); if(fly){const k=Math.min(1,(performance.now()-fly.t0)/fly.dur),e=k<.5?2*k*k:1-Math.pow(-2*k+2,2)/2; controls.target.lerpVectors(fly.fromT,fly.toT,e);camera.position.lerpVectors(fly.fromC,fly.toC,e);if(k>=1)fly=null;} + // the ring under the selected asset breathes + if(selRing&&selRing.visible&&!reduceMotion){const k=(Math.sin(performance.now()/260)+1)/2;selRing.material.opacity=0.55+0.45*k;const s=1.75+0.25*k;selRing.scale.set(s,1,s);} if(!reduceMotion)ANIM.forEach(o=>{if(!o.tube.visible)return;o.parts.forEach(p=>{if(!p.visible)return;p.userData.t=(p.userData.t+dt*o.speed)%1;p.position.copy(o.curve.getPoint(p.userData.t));});}); - if(!(state.flat&&state.view==='site')){controls.update();if(!fly)keepInView();renderer.render(scene,camera);}requestAnimationFrame(tick); + // nothing to draw while the 2D diagram or a lens (Application, Business, Risks) covers the model + if(!(state.view==='site'&&(state.flat||state.lens!=='physical'))){controls.update();if(!fly)keepInView();renderer.render(scene,camera);}requestAnimationFrame(tick); } function start(){ resize();buildOverview();renderCart();syncUI();renderEmpty();setViewNow();tick(); diff --git a/public/baselines.js b/public/baselines.js new file mode 100644 index 0000000..b2a9208 --- /dev/null +++ b/public/baselines.js @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Security baselines shipped with Strata, for the risk register. Each requirement lists the threats of the +// catalogue in risk.js it reduces: [threat id, share of the likelihood or impact it removes, 'likelihood'|'impact']. +// The links and shares are Strata's own judgement, not part of the baselines. Licences: +// - ANSSI, Guide d'hygiène informatique (42 mesures, 2017): Licence Ouverte / Open Licence (Etalab). French titles +// as published; English titles and summaries translated. +// - NIST Cybersecurity Framework 2.0: a U.S. Government work, public domain. Titles from NIST's own export. +// - CIS Critical Security Controls v8.1 and ISO/IEC 27001:2022 Annex A: numbering only, with titles paraphrased +// by Strata; neither text is reproduced. ig: the lowest CIS Implementation Group of the safeguard. +(function(root){ +'use strict'; +root.StrataBaselines=[{"id":"anssi","name":"ANSSI — Guide d'hygiène informatique (42 mesures)","version":"2.0 (2017)","license":"Licence Ouverte / Open Licence (Etalab, v1), as stated in the guide. Translated and summarised.","url":"https://messervices.cyber.gouv.fr/guides/guide-dhygiene-informatique","requirements":[{"id":"1","ref":"M1","title":"Train the operational teams in information system security","titleFr":"Former les équipes opérationnelles à la sécurité des systèmes d’information","text":"Train administrators, developers, project managers and other operational staff, on arrival and regularly, in security topics such as hardening, authentication, access control, partitioning and logging. Service contracts should guarantee the same training for external staff and outsourcers.","threats":[["directory-takeover",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"],["default-credentials",0.2,"likelihood"],["data-exposure",0.2,"likelihood"],["web-app",0.1,"likelihood"],["privilege-escalation",0.1,"likelihood"]]},{"id":"2","ref":"M2","title":"Raise users’ awareness of basic IT security practices","titleFr":"Sensibiliser les utilisateurs aux bonnes pratiques élémentaires de sécurité informatique","text":"Inform every user, from arrival and regularly, of the security stakes, sensitive information and everyday rules (no personal devices, no password sharing or reuse, report suspicious events, lock sessions). A signed IT charter is the strengthened level.","threats":[["phishing",0.4,"likelihood"],["bec",0.3,"likelihood"],["brute-force",0.1,"likelihood"],["physical-access",0.1,"likelihood"]]},{"id":"3","ref":"M3","title":"Control the risks of outsourcing","titleFr":"Maîtriser les risques de l’infogérance","text":"Assess the risks of outsourcing the information system or data beforehand and impose security requirements on the provider (reversibility, audits, backups in an open format, security maintenance), formalised in a security assurance plan.","threats":[["third-party-access",0.3,"likelihood"],["supply-chain",0.2,"likelihood"],["internet-dependency",0.2,"impact"]]},{"id":"4","ref":"M4","title":"Identify the most sensitive information and servers and maintain a network diagram","titleFr":"Identifier les informations et serveurs les plus sensibles et maintenir un schéma du réseau","text":"List the sensitive data and the servers and devices that hold it so they get specific protection, and keep a simplified network map showing IP zones, addressing, routing and security devices, external connections and partners.","threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.1,"likelihood"],["it-ot-pivot",0.1,"likelihood"]]},{"id":"5","ref":"M5","title":"Keep an exhaustive, up-to-date inventory of privileged accounts","titleFr":"Disposer d’un inventaire exhaustif des comptes privilégiés et le maintenir à jour","text":"Inventory accounts with administrator or elevated rights, access to managers’ or all users’ folders, or unmanaged workstations; review them periodically to remove obsolete rights, and use a clear naming scheme for system and admin accounts.","threats":[["directory-takeover",0.2,"likelihood"],["insider-misuse",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"6","ref":"M6","title":"Organise procedures for users joining, leaving and changing roles","titleFr":"Organiser les procédures d’arrivée, de départ et de changement de fonction des utilisateurs","text":"Define, with HR, arrival and departure procedures that create and remove accounts and mailboxes, adjust rights on role changes, manage badges and keys, mobile equipment and the handover of sensitive information and passwords.","threats":[["insider-misuse",0.3,"likelihood"],["remote-access",0.2,"likelihood"],["third-party-access",0.1,"likelihood"]]},{"id":"7","ref":"M7","title":"Allow only managed devices to connect to the organisation’s network","titleFr":"Autoriser la connexion au réseau de l’entité aux seuls équipements maîtrisés","text":"Only devices managed by the organisation may connect to its wired or wireless networks; personal and visitor devices go on a separate network (e.g. a dedicated Wi-Fi SSID). Device authentication such as 802.1X is the strengthened level.","threats":[["wifi-intrusion",0.3,"likelihood"],["physical-access",0.2,"likelihood"],["device-takeover",0.1,"likelihood"]]},{"id":"8","ref":"M8","title":"Identify each person accessing the system by name and distinguish user and administrator roles","titleFr":"Identifier nommément chaque personne accédant au système et distinguer les rôles utilisateur/administrateur","text":"Use nominative accounts, keep generic accounts marginal and as tightly managed, and give each administrator a separate admin account with distinct credentials, used only for administration in a dedicated environment; log account connections.","threats":[["lateral-admin",0.3,"likelihood"],["directory-takeover",0.3,"likelihood"],["insider-misuse",0.2,"likelihood"]]},{"id":"9","ref":"M9","title":"Assign the right permissions on the information system’s sensitive resources","titleFr":"Attribuer les bons droits sur les ressources sensibles du système d’information","text":"List sensitive resources (folders, databases, mailboxes, shares), define which group may access each, enforce authenticated access, avoid copies to less protected areas, and review access rights regularly.","threats":[["data-exposure",0.3,"likelihood"],["data-theft",0.3,"likelihood"],["insider-misuse",0.3,"likelihood"]]},{"id":"10","ref":"M10","title":"Define and enforce rules for choosing passwords and their length","titleFr":"Définir et vérifier des règles de choix et de dimensionnement des mots de passe","text":"Set password strength rules following ANSSI recommendations, explain them to users (in particular no reuse), and enforce them with account lockout after failed attempts, no anonymous logins and password strength checks.","threats":[["brute-force",0.4,"likelihood"],["remote-access",0.2,"likelihood"],["default-credentials",0.2,"likelihood"]]},{"id":"11","ref":"M11","title":"Protect passwords stored on systems","titleFr":"Protéger les mots de passe stockés sur les systèmes","text":"Do not store passwords in clear on paper or in files, e-mails or browsers; use an encrypted password vault protected by a strong, memorised master password.","threats":[["lateral-admin",0.2,"likelihood"],["directory-takeover",0.1,"likelihood"]]},{"id":"12","ref":"M12","title":"Change default authentication credentials on devices and services","titleFr":"Changer les éléments d’authentification par défaut sur les équipements et services","text":"Replace default passwords and credentials of every component at installation, following the password rules, report hard-coded secrets to the vendor, and renew them regularly at the strengthened level.","threats":[["default-credentials",0.6,"likelihood"],["device-takeover",0.4,"likelihood"],["network-device",0.2,"likelihood"]]},{"id":"13","ref":"M13","title":"Use strong authentication whenever possible","titleFr":"Privilégier lorsque c’est possible une authentification forte","text":"Prefer multi-factor authentication combining something you know, have or are; smart cards or one-time passwords with a hardware token are the strengthened level.","threats":[["remote-access",0.6,"likelihood"],["brute-force",0.5,"likelihood"],["bec",0.4,"likelihood"],["phishing",0.3,"likelihood"],["lateral-admin",0.2,"likelihood"],["directory-takeover",0.2,"likelihood"]]},{"id":"14","ref":"M14","title":"Apply a minimum security level across the whole IT estate","titleFr":"Mettre en place un niveau de sécurité minimal sur l’ensemble du parc informatique","text":"On every device, limit installed applications and browser add-ons, run anti-virus and a local firewall, encrypt user data partitions, disable autorun, and isolate devices that cannot meet the rules. Strengthened: back up vital data to disconnected media and test restores.","threats":[["phishing",0.3,"likelihood"],["ransomware",0.3,"likelihood"],["backup-destruction",0.2,"likelihood"],["physical-access",0.2,"impact"],["end-of-life",0.2,"impact"],["privilege-escalation",0.1,"likelihood"]]},{"id":"15","ref":"M15","title":"Protect against threats from removable media","titleFr":"Se protéger des menaces relatives à l’utilisation de supports amovibles","text":"Forbid unknown USB media, limit uncontrolled ones and scan them with anti-virus, raise user awareness; strengthened: block execution from removable media and destroy media securely at end of life.","threats":[["physical-access",0.3,"likelihood"],["data-theft",0.2,"likelihood"],["ransomware",0.1,"likelihood"]]},{"id":"16","ref":"M16","title":"Use a centralised management tool to make security policies uniform","titleFr":"Utiliser un outil de gestion centralisée afin d’homogénéiser les politiques de sécurité","text":"Manage as many workstations and servers as possible from a central tool (e.g. Active Directory) so hardening and security policies are applied uniformly and can be changed quickly in a crisis.","threats":[["privilege-escalation",0.1,"likelihood"],["lateral-admin",0.1,"likelihood"],["ransomware",0.1,"impact"]]},{"id":"17","ref":"M17","title":"Enable and configure the workstations’ local firewall","titleFr":"Activer et configurer le pare-feu local des postes de travail","text":"Turn on the host firewall to hinder lateral movement between workstations; strengthened: block administration ports by default except from identified hosts, whitelist needed inbound flows, and log blocked traffic.","threats":[["lateral-admin",0.4,"likelihood"],["ransomware",0.2,"impact"]]},{"id":"18","ref":"M18","title":"Encrypt sensitive data sent over the Internet","titleFr":"Chiffrer les données sensibles transmises par voie Internet","text":"Systematically encrypt sensitive data before e-mailing it or uploading it to online services, and pass the decryption secret through a separate trusted channel.","threats":[["data-theft",0.3,"likelihood"],["data-exposure",0.2,"impact"]]},{"id":"19","ref":"M19","title":"Segment the network and partition the resulting zones","titleFr":"Segmenter le réseau et mettre en place un cloisonnement entre ces zones","text":"Avoid flat networks: group systems with similar security needs into zones (VLANs, subnets or dedicated infrastructure) filtered by firewalls, segregating administration flows as far as possible.","threats":[["lateral-admin",0.4,"likelihood"],["it-ot-pivot",0.4,"likelihood"],["ransomware",0.3,"impact"],["directory-takeover",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"],["device-takeover",0.2,"impact"]]},{"id":"20","ref":"M20","title":"Secure Wi-Fi access networks and separate uses","titleFr":"S’assurer de la sécurité des réseaux d’accès Wi-Fi et de la séparation des usages","text":"Filter Wi-Fi flows to what is needed, use robust encryption (WPA2/AES-CCMP) and centralised, ideally certificate-based, authentication rather than one shared key, administer access points securely, and keep staff/visitor Wi-Fi apart from the organisation’s devices.","threats":[["wifi-intrusion",0.6,"likelihood"]]},{"id":"21","ref":"M21","title":"Use secure protocols wherever they exist","titleFr":"Utiliser des protocoles sécurisés dès qu’ils existent","text":"Use protocols with built-in security (TLS-based variants such as HTTPS, IMAPS, SMTPS; SSH instead of Telnet) on both the Internet and internal networks.","threats":[["data-theft",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["network-device",0.1,"likelihood"]]},{"id":"22","ref":"M22","title":"Set up a secure Internet access gateway","titleFr":"Mettre en place une passerelle d’accès sécurisé à Internet","text":"Users’ devices should not reach the Internet directly: go through a gateway with a firewall and an authenticating, logging proxy; strengthened: content scanning, URL filtering, DNS resolution via the proxy, and mobile devices browsing through it.","threats":[["phishing",0.3,"likelihood"],["ransomware",0.2,"likelihood"],["data-theft",0.2,"likelihood"],["device-takeover",0.1,"likelihood"]]},{"id":"23","ref":"M23","title":"Isolate Internet-facing services from the rest of the information system","titleFr":"Cloisonner les services visibles depuis Internet du reste du système d’information","text":"Host services exposed to the Internet in dedicated, filtered zones (DMZ) separate from the internal network, so that their compromise does not open the rest of the information system.","threats":[["exploit-public",0.3,"impact"],["web-app",0.3,"impact"],["lateral-admin",0.2,"likelihood"],["ransomware",0.2,"impact"],["data-theft",0.1,"impact"]]},{"id":"24","ref":"M24","title":"Protect professional e-mail","titleFr":"Protéger sa messagerie professionnelle","text":"Scan incoming mail for malware, enable TLS between mail servers and clients, forbid forwarding to personal mailboxes, apply procedures against transfer fraud, and train users; strengthened: relay servers, anti-spam and SPF/DKIM/DMARC.","threats":[["phishing",0.5,"likelihood"],["bec",0.5,"likelihood"],["ransomware",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"25","ref":"M25","title":"Secure dedicated network interconnections with partners","titleFr":"Sécuriser les interconnexions réseau dédiées avec les partenaires","text":"Treat partners as untrusted: use site-to-site tunnels (ideally IPsec) and a firewall restricting flows to the operational need; strengthened: a dedicated filtering device, intrusion detection and an up-to-date partner contact.","threats":[["third-party-access",0.5,"likelihood"],["supply-chain",0.2,"impact"]]},{"id":"26","ref":"M26","title":"Control and protect access to server rooms and technical premises","titleFr":"Contrôler et protéger l’accès aux salles serveurs et aux locaux techniques","text":"Restrict server rooms and technical areas with locks or badges, forbid unaccompanied external access, review and revoke access rights, and disable network sockets in public areas.","threats":[["physical-access",0.5,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"27","ref":"M27","title":"Forbid Internet access from devices or servers used to administer the information system","titleFr":"Interdire l’accès à Internet depuis les postes ou serveurs utilisés pour l’administration du système d’information","text":"Administration workstations and servers must never browse the web or read e-mail; give administrators a separate device for office use, and bring updates in from trusted sources through a controlled path.","threats":[["directory-takeover",0.4,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["lateral-admin",0.2,"likelihood"],["phishing",0.2,"impact"]]},{"id":"28","ref":"M28","title":"Use a dedicated, segregated network to administer the information system","titleFr":"Utiliser un réseau dédié et cloisonné pour l’administration du système d’information","text":"Separate the administration network from the office network, preferably physically, otherwise with IPsec tunnels, and at minimum with VLANs.","threats":[["directory-takeover",0.4,"likelihood"],["hypervisor-mass",0.4,"likelihood"],["lateral-admin",0.4,"likelihood"],["network-device",0.3,"likelihood"],["ot-manipulation",0.2,"likelihood"]]},{"id":"29","ref":"M29","title":"Limit administration rights on workstations to strict operational need","titleFr":"Limiter au strict besoin opérationnel les droits d’administration sur les postes de travail","text":"Users, including executives, should not be local administrators; only workstation administrators hold these rights, a validated application store covers needs, and any exceptional delegation is time-limited and monitored.","threats":[["phishing",0.2,"impact"],["ransomware",0.2,"impact"],["lateral-admin",0.2,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"30","ref":"M30","title":"Physically secure mobile devices","titleFr":"Prendre des mesures de sécurisation physique des terminaux nomades","text":"Make users vigilant with laptops, tablets and phones while travelling, keep devices unbranded, fit privacy screens, and optionally keep authentication or decryption secrets on a separate token.","threats":[["physical-access",0.4,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"31","ref":"M31","title":"Encrypt sensitive data, especially on hardware that can be lost","titleFr":"Chiffrer les données sensibles, en particulier sur le matériel potentiellement perdable","text":"Store only encrypted data on laptops, phones, USB keys and external drives, preferring full-disk encryption before archive or file encryption, with a robust secret.","threats":[["physical-access",0.5,"impact"],["data-theft",0.3,"impact"]]},{"id":"32","ref":"M32","title":"Secure the network connection of devices used while travelling","titleFr":"Sécuriser la connexion réseau des postes utilisés en situation de nomadisme","text":"Connect mobile workstations through an IPsec VPN to the organisation’s gateway, established automatically and not bypassable; strengthened: two-factor authentication with a certificate on a token or one-time passwords.","threats":[["remote-access",0.3,"likelihood"],["data-theft",0.2,"likelihood"]]},{"id":"33","ref":"M33","title":"Adopt security policies dedicated to mobile devices","titleFr":"Adopter des politiques de sécurité dédiées aux terminaux mobiles","text":"Do not mix personal and professional use on smartphones and tablets, and manage company devices centrally (unlock method, only validated apps); avoid built-in voice assistants.","threats":[["device-takeover",0.3,"likelihood"],["data-theft",0.2,"likelihood"],["physical-access",0.2,"impact"]]},{"id":"34","ref":"M34","title":"Define an update policy for the information system’s components","titleFr":"Définir une politique de mise à jour des composants du système d’information","text":"Track new vulnerabilities (e.g. CERT-FR) and apply security patches to all components within a month of release, with procedures covering inventory, sources, deployment tools and testing; isolate unsupported components.","threats":[["known-exploited",0.6,"likelihood"],["exploit-public",0.5,"likelihood"],["network-device",0.4,"likelihood"],["privilege-escalation",0.4,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["device-takeover",0.3,"likelihood"],["web-app",0.2,"likelihood"],["defacement",0.2,"likelihood"],["end-of-life",0.3,"impact"]]},{"id":"35","ref":"M35","title":"Anticipate the end of software and system support and limit software dependencies","titleFr":"Anticiper la fin de la maintenance des logiciels et systèmes et limiter les adhérences logicielles","text":"Keep an inventory of applications and systems, choose products supported for their whole service life, monitor end-of-support dates, keep a homogeneous estate, limit dependencies between products, and plan migrations.","threats":[["end-of-life",0.6,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"36","ref":"M36","title":"Enable and configure logs on the most important components","titleFr":"Activer et configurer les journaux des composants les plus importants","text":"Log security events on critical components (blocked flows, authentications, errors, interconnection traffic), keep them at least a year, synchronise clocks via NTP; strengthened: centralise logs so attackers cannot erase them.","threats":[["detection-blinding",0.4,"likelihood"],["ransomware",0.2,"impact"],["data-theft",0.2,"impact"],["insider-misuse",0.2,"impact"],["lateral-admin",0.2,"impact"],["directory-takeover",0.2,"impact"],["brute-force",0.2,"impact"]]},{"id":"37","ref":"M37","title":"Define and apply a backup policy for critical components","titleFr":"Définir et appliquer une politique de sauvegarde des composants critiques","text":"Formalise a backup policy listing vital data and servers, backup types including offline, frequency, procedures, storage and access restrictions, restore tests and media destruction; strengthened: a yearly restoration exercise.","threats":[["ransomware",0.6,"impact"],["hypervisor-mass",0.5,"impact"],["backup-destruction",0.5,"likelihood"],["insider-misuse",0.2,"impact"],["defacement",0.2,"impact"]]},{"id":"38","ref":"M38","title":"Carry out regular security checks and audits, then apply the corrective actions","titleFr":"Procéder à des contrôles et audits de sécurité réguliers puis appliquer les actions correctives associées","text":"Audit the information system at least yearly (technical and organisational, internal or by qualified providers), plan and track the corrective actions, and report progress to management.","threats":[["web-app",0.3,"likelihood"],["data-exposure",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["default-credentials",0.2,"likelihood"],["network-device",0.1,"likelihood"]]},{"id":"39","ref":"M39","title":"Appoint an information security point of contact and make them known to staff","titleFr":"Désigner un référent en sécurité des systèmes d’information et le faire connaître auprès du personnel","text":"Name a trained security referent, backed by management and known to all users, who sets and checks rules, drives awareness and training, and centralises incident reports.","threats":[["phishing",0.1,"impact"]]},{"id":"40","ref":"M40","title":"Define a security incident management procedure","titleFr":"Définir une procédure de gestion des incidents de sécurité","text":"Define how to react to suspected intrusions: disconnect the device from the network but keep it powered, alert management and the security referent, call on incident response providers, clean up and change passwords, and record incidents centrally.","threats":[["ransomware",0.3,"impact"],["data-theft",0.2,"impact"],["bec",0.2,"impact"],["lateral-admin",0.2,"impact"]]},{"id":"41","ref":"M41","title":"Carry out a formal risk analysis","titleFr":"Mener une analyse de risques formelle","text":"Assess information system risks formally (context, likelihood and impact, treatment plan validated by management) using a method such as ANSSI’s EBIOS, to decide and justify security measures.","threats":[]},{"id":"42","ref":"M42","title":"Prefer products and services qualified by ANSSI","titleFr":"Privilégier l’usage de produits et de services qualifiés par l’ANSSI","text":"Where possible, use security products and service providers (audit, incident response, cloud, etc.) that ANSSI has qualified, giving assurance of their robustness and trustworthiness.","threats":[["supply-chain",0.2,"likelihood"],["network-device",0.2,"likelihood"],["third-party-access",0.1,"likelihood"]]}]},{"id":"nist-csf","name":"NIST Cybersecurity Framework 2.0","version":"2.0 (2024)","license":"Public domain (U.S. Government work).","url":"https://www.nist.gov/cyberframework","requirements":[{"id":"GV.OC-01","ref":"GV.OC-01","title":"The organizational mission is understood and informs cybersecurity risk management","text":"","threats":[]},{"id":"GV.OC-02","ref":"GV.OC-02","title":"Internal and external stakeholders are understood, and their needs and expectations regarding cybersecurity risk management are understood and considered","text":"","threats":[]},{"id":"GV.OC-03","ref":"GV.OC-03","title":"Legal, regulatory, and contractual requirements regarding cybersecurity - including privacy and civil liberties obligations - are understood and managed","text":"","threats":[]},{"id":"GV.OC-04","ref":"GV.OC-04","title":"Critical objectives, capabilities, and services that external stakeholders depend on or expect from the organization are understood and communicated","text":"","threats":[]},{"id":"GV.OC-05","ref":"GV.OC-05","title":"Outcomes, capabilities, and services that the organization depends on are understood and communicated","text":"","threats":[["internet-dependency",0.2,"impact"]]},{"id":"GV.OV-01","ref":"GV.OV-01","title":"Cybersecurity risk management strategy outcomes are reviewed to inform and adjust strategy and direction","text":"","threats":[]},{"id":"GV.OV-02","ref":"GV.OV-02","title":"The cybersecurity risk management strategy is reviewed and adjusted to ensure coverage of organizational requirements and risks","text":"","threats":[]},{"id":"GV.OV-03","ref":"GV.OV-03","title":"Organizational cybersecurity risk management performance is evaluated and reviewed for adjustments needed","text":"","threats":[]},{"id":"GV.PO-01","ref":"GV.PO-01","title":"Policy for managing cybersecurity risks is established based on organizational context, cybersecurity strategy, and priorities and is communicated and enforced","text":"","threats":[["insider-misuse",0.1,"likelihood"]]},{"id":"GV.PO-02","ref":"GV.PO-02","title":"Policy for managing cybersecurity risks is reviewed, updated, communicated, and enforced to reflect changes in requirements, threats, technology, and organizational mission","text":"","threats":[]},{"id":"GV.RM-01","ref":"GV.RM-01","title":"Risk management objectives are established and agreed to by organizational stakeholders","text":"","threats":[]},{"id":"GV.RM-02","ref":"GV.RM-02","title":"Risk appetite and risk tolerance statements are established, communicated, and maintained","text":"","threats":[]},{"id":"GV.RM-03","ref":"GV.RM-03","title":"Cybersecurity risk management activities and outcomes are included in enterprise risk management processes","text":"","threats":[]},{"id":"GV.RM-04","ref":"GV.RM-04","title":"Strategic direction that describes appropriate risk response options is established and communicated","text":"","threats":[]},{"id":"GV.RM-05","ref":"GV.RM-05","title":"Lines of communication across the organization are established for cybersecurity risks, including risks from suppliers and other third parties","text":"","threats":[]},{"id":"GV.RM-06","ref":"GV.RM-06","title":"A standardized method for calculating, documenting, categorizing, and prioritizing cybersecurity risks is established and communicated","text":"","threats":[]},{"id":"GV.RM-07","ref":"GV.RM-07","title":"Strategic opportunities (i.e., positive risks) are characterized and are included in organizational cybersecurity risk discussions","text":"","threats":[]},{"id":"GV.RR-01","ref":"GV.RR-01","title":"Organizational leadership is responsible and accountable for cybersecurity risk and fosters a culture that is risk-aware, ethical, and continually improving","text":"","threats":[]},{"id":"GV.RR-02","ref":"GV.RR-02","title":"Roles, responsibilities, and authorities related to cybersecurity risk management are established, communicated, understood, and enforced","text":"","threats":[]},{"id":"GV.RR-03","ref":"GV.RR-03","title":"Adequate resources are allocated commensurate with the cybersecurity risk strategy, roles, responsibilities, and policies","text":"","threats":[]},{"id":"GV.RR-04","ref":"GV.RR-04","title":"Cybersecurity is included in human resources practices","text":"","threats":[["insider-misuse",0.2,"likelihood"]]},{"id":"GV.SC-01","ref":"GV.SC-01","title":"A cybersecurity supply chain risk management program, strategy, objectives, policies, and processes are established and agreed to by organizational stakeholders","text":"","threats":[["supply-chain",0.1,"likelihood"],["third-party-access",0.1,"likelihood"]]},{"id":"GV.SC-02","ref":"GV.SC-02","title":"Cybersecurity roles and responsibilities for suppliers, customers, and partners are established, communicated, and coordinated internally and externally","text":"","threats":[["third-party-access",0.2,"likelihood"]]},{"id":"GV.SC-03","ref":"GV.SC-03","title":"Cybersecurity supply chain risk management is integrated into cybersecurity and enterprise risk management, risk assessment, and improvement processes","text":"","threats":[["supply-chain",0.1,"likelihood"]]},{"id":"GV.SC-04","ref":"GV.SC-04","title":"Suppliers are known and prioritized by criticality","text":"","threats":[["supply-chain",0.1,"likelihood"],["third-party-access",0.1,"likelihood"],["internet-dependency",0.1,"impact"]]},{"id":"GV.SC-05","ref":"GV.SC-05","title":"Requirements to address cybersecurity risks in supply chains are established, prioritized, and integrated into contracts and other types of agreements with suppliers and other relevant third parties","text":"","threats":[["third-party-access",0.3,"likelihood"],["supply-chain",0.2,"likelihood"]]},{"id":"GV.SC-06","ref":"GV.SC-06","title":"Planning and due diligence are performed to reduce risks before entering into formal supplier or other third-party relationships","text":"","threats":[["supply-chain",0.2,"likelihood"],["third-party-access",0.2,"likelihood"]]},{"id":"GV.SC-07","ref":"GV.SC-07","title":"The risks posed by a supplier, their products and services, and other third parties are understood, recorded, prioritized, assessed, responded to, and monitored over the course of the relationship","text":"","threats":[["supply-chain",0.2,"likelihood"],["third-party-access",0.2,"likelihood"],["internet-dependency",0.1,"impact"]]},{"id":"GV.SC-08","ref":"GV.SC-08","title":"Relevant suppliers and other third parties are included in incident planning, response, and recovery activities","text":"","threats":[["supply-chain",0.2,"impact"],["third-party-access",0.2,"impact"],["internet-dependency",0.2,"impact"]]},{"id":"GV.SC-09","ref":"GV.SC-09","title":"Supply chain security practices are integrated into cybersecurity and enterprise risk management programs, and their performance is monitored throughout the technology product and service life cycle","text":"","threats":[["supply-chain",0.2,"likelihood"]]},{"id":"GV.SC-10","ref":"GV.SC-10","title":"Cybersecurity supply chain risk management plans include provisions for activities that occur after the conclusion of a partnership or service agreement","text":"","threats":[["third-party-access",0.3,"likelihood"]]},{"id":"ID.AM-01","ref":"ID.AM-01","title":"Inventories of hardware managed by the organization are maintained","text":"","threats":[["device-takeover",0.2,"likelihood"],["known-exploited",0.1,"likelihood"],["end-of-life",0.1,"likelihood"]]},{"id":"ID.AM-02","ref":"ID.AM-02","title":"Inventories of software, services, and systems managed by the organization are maintained","text":"","threats":[["end-of-life",0.2,"likelihood"],["known-exploited",0.2,"likelihood"],["exploit-public",0.1,"likelihood"]]},{"id":"ID.AM-03","ref":"ID.AM-03","title":"Representations of the organization's authorized network communication and internal and external network data flows are maintained","text":"","threats":[["it-ot-pivot",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["data-exposure",0.1,"likelihood"]]},{"id":"ID.AM-04","ref":"ID.AM-04","title":"Inventories of services provided by suppliers are maintained","text":"","threats":[["internet-dependency",0.2,"impact"],["third-party-access",0.1,"likelihood"]]},{"id":"ID.AM-05","ref":"ID.AM-05","title":"Assets are prioritized based on classification, criticality, resources, and impact on the mission","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"ID.AM-07","ref":"ID.AM-07","title":"Inventories of data and corresponding metadata for designated data types are maintained","text":"","threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"ID.AM-08","ref":"ID.AM-08","title":"Systems, hardware, software, services, and data are managed throughout their life cycles","text":"","threats":[["end-of-life",0.4,"likelihood"],["known-exploited",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"ID.IM-01","ref":"ID.IM-01","title":"Improvements are identified from evaluations","text":"","threats":[]},{"id":"ID.IM-02","ref":"ID.IM-02","title":"Improvements are identified from security tests and exercises, including those done in coordination with suppliers and relevant third parties","text":"","threats":[["exploit-public",0.1,"likelihood"],["web-app",0.1,"likelihood"]]},{"id":"ID.IM-03","ref":"ID.IM-03","title":"Improvements are identified from execution of operational processes, procedures, and activities","text":"","threats":[]},{"id":"ID.IM-04","ref":"ID.IM-04","title":"Incident response plans and other cybersecurity plans that affect operations are established, communicated, maintained, and improved","text":"","threats":[["ransomware",0.3,"impact"],["hypervisor-mass",0.2,"impact"],["directory-takeover",0.2,"impact"],["internet-dependency",0.2,"impact"],["ddos",0.2,"impact"]]},{"id":"ID.RA-01","ref":"ID.RA-01","title":"Vulnerabilities in assets are identified, validated, and recorded","text":"","threats":[["known-exploited",0.4,"likelihood"],["exploit-public",0.3,"likelihood"],["network-device",0.3,"likelihood"],["web-app",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"],["end-of-life",0.2,"likelihood"],["default-credentials",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"ID.RA-02","ref":"ID.RA-02","title":"Cyber threat intelligence is received from information sharing forums and sources","text":"","threats":[["known-exploited",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["network-device",0.2,"likelihood"]]},{"id":"ID.RA-03","ref":"ID.RA-03","title":"Internal and external threats to the organization are identified and recorded","text":"","threats":[["exploit-public",0.1,"likelihood"]]},{"id":"ID.RA-04","ref":"ID.RA-04","title":"Potential impacts and likelihoods of threats exploiting vulnerabilities are identified and recorded","text":"","threats":[]},{"id":"ID.RA-05","ref":"ID.RA-05","title":"Threats, vulnerabilities, likelihoods, and impacts are used to understand inherent risk and inform risk response prioritization","text":"","threats":[]},{"id":"ID.RA-06","ref":"ID.RA-06","title":"Risk responses are chosen, prioritized, planned, tracked, and communicated","text":"","threats":[["known-exploited",0.2,"likelihood"],["exploit-public",0.1,"likelihood"]]},{"id":"ID.RA-07","ref":"ID.RA-07","title":"Changes and exceptions are managed, assessed for risk impact, recorded, and tracked","text":"","threats":[["data-exposure",0.2,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"ID.RA-08","ref":"ID.RA-08","title":"Processes for receiving, analyzing, and responding to vulnerability disclosures are established","text":"","threats":[["web-app",0.2,"likelihood"],["exploit-public",0.1,"likelihood"]]},{"id":"ID.RA-09","ref":"ID.RA-09","title":"The authenticity and integrity of hardware and software are assessed prior to acquisition and use","text":"","threats":[["supply-chain",0.4,"likelihood"]]},{"id":"ID.RA-10","ref":"ID.RA-10","title":"Critical suppliers are assessed prior to acquisition","text":"","threats":[["supply-chain",0.3,"likelihood"],["third-party-access",0.2,"likelihood"]]},{"id":"PR.AA-01","ref":"PR.AA-01","title":"Identities and credentials for authorized users, services, and hardware are managed by the organization","text":"","threats":[["insider-misuse",0.2,"likelihood"],["default-credentials",0.2,"likelihood"],["third-party-access",0.2,"likelihood"],["remote-access",0.1,"likelihood"]]},{"id":"PR.AA-02","ref":"PR.AA-02","title":"Identities are proofed and bound to credentials based on the context of interactions","text":"","threats":[["remote-access",0.2,"likelihood"],["bec",0.1,"likelihood"]]},{"id":"PR.AA-03","ref":"PR.AA-03","title":"Users, services, and hardware are authenticated","text":"","threats":[["remote-access",0.5,"likelihood"],["brute-force",0.5,"likelihood"],["bec",0.4,"likelihood"],["default-credentials",0.3,"likelihood"],["lateral-admin",0.3,"likelihood"],["wifi-intrusion",0.3,"likelihood"],["directory-takeover",0.2,"likelihood"],["phishing",0.2,"likelihood"],["device-takeover",0.2,"likelihood"],["third-party-access",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"],["network-device",0.2,"likelihood"],["defacement",0.2,"likelihood"]]},{"id":"PR.AA-04","ref":"PR.AA-04","title":"Identity assertions are protected, conveyed, and verified","text":"","threats":[["directory-takeover",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"],["bec",0.2,"likelihood"]]},{"id":"PR.AA-05","ref":"PR.AA-05","title":"Access permissions, entitlements, and authorizations are defined in a policy, managed, enforced, and reviewed, and incorporate the principles of least privilege and separation of duties","text":"","threats":[["insider-misuse",0.4,"likelihood"],["directory-takeover",0.3,"likelihood"],["lateral-admin",0.3,"likelihood"],["data-exposure",0.3,"likelihood"],["data-theft",0.3,"impact"],["third-party-access",0.3,"impact"],["ransomware",0.2,"impact"],["hypervisor-mass",0.2,"likelihood"],["ot-manipulation",0.2,"likelihood"]]},{"id":"PR.AA-06","ref":"PR.AA-06","title":"Physical access to assets is managed, monitored, and enforced commensurate with risk","text":"","threats":[["physical-access",0.5,"likelihood"],["ot-manipulation",0.1,"likelihood"]]},{"id":"PR.AT-01","ref":"PR.AT-01","title":"Personnel are provided with awareness and training so that they possess the knowledge and skills to perform general tasks with cybersecurity risks in mind","text":"","threats":[["phishing",0.4,"likelihood"],["bec",0.3,"likelihood"],["physical-access",0.1,"likelihood"],["data-exposure",0.1,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"PR.AT-02","ref":"PR.AT-02","title":"Individuals in specialized roles are provided with awareness and training so that they possess the knowledge and skills to perform relevant tasks with cybersecurity risks in mind","text":"","threats":[["bec",0.3,"likelihood"],["phishing",0.2,"likelihood"],["data-exposure",0.2,"likelihood"],["web-app",0.2,"likelihood"],["ot-manipulation",0.1,"likelihood"]]},{"id":"PR.DS-01","ref":"PR.DS-01","title":"The confidentiality, integrity, and availability of data-at-rest are protected","text":"","threats":[["physical-access",0.5,"impact"],["data-theft",0.4,"impact"],["data-exposure",0.3,"impact"]]},{"id":"PR.DS-02","ref":"PR.DS-02","title":"The confidentiality, integrity, and availability of data-in-transit are protected","text":"","threats":[["data-theft",0.2,"likelihood"],["wifi-intrusion",0.2,"impact"],["lateral-admin",0.1,"likelihood"]]},{"id":"PR.DS-10","ref":"PR.DS-10","title":"The confidentiality, integrity, and availability of data-in-use are protected","text":"","threats":[["lateral-admin",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"PR.DS-11","ref":"PR.DS-11","title":"Backups of data are created, protected, maintained, and tested","text":"","threats":[["ransomware",0.6,"impact"],["backup-destruction",0.5,"likelihood"],["hypervisor-mass",0.5,"impact"],["defacement",0.3,"impact"],["insider-misuse",0.3,"impact"],["ot-manipulation",0.2,"impact"],["directory-takeover",0.2,"impact"]]},{"id":"PR.IR-01","ref":"PR.IR-01","title":"Networks and environments are protected from unauthorized logical access and usage","text":"","threats":[["it-ot-pivot",0.5,"likelihood"],["lateral-admin",0.4,"likelihood"],["ransomware",0.3,"impact"],["wifi-intrusion",0.3,"impact"],["device-takeover",0.3,"impact"],["data-exposure",0.3,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["third-party-access",0.2,"impact"],["remote-access",0.2,"impact"]]},{"id":"PR.IR-02","ref":"PR.IR-02","title":"The organization's technology assets are protected from environmental threats","text":"","threats":[]},{"id":"PR.IR-03","ref":"PR.IR-03","title":"Mechanisms are implemented to achieve resilience requirements in normal and adverse situations","text":"","threats":[["internet-dependency",0.4,"impact"],["ddos",0.3,"impact"],["ransomware",0.2,"impact"],["hypervisor-mass",0.2,"impact"]]},{"id":"PR.IR-04","ref":"PR.IR-04","title":"Adequate resource capacity to ensure availability is maintained","text":"","threats":[["ddos",0.3,"impact"]]},{"id":"PR.PS-01","ref":"PR.PS-01","title":"Configuration management practices are established and applied","text":"","threats":[["default-credentials",0.4,"likelihood"],["data-exposure",0.4,"likelihood"],["device-takeover",0.3,"likelihood"],["network-device",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"],["wifi-intrusion",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"],["web-app",0.1,"likelihood"]]},{"id":"PR.PS-02","ref":"PR.PS-02","title":"Software is maintained, replaced, and removed commensurate with risk","text":"","threats":[["known-exploited",0.5,"likelihood"],["end-of-life",0.5,"likelihood"],["exploit-public",0.4,"likelihood"],["privilege-escalation",0.4,"likelihood"],["web-app",0.3,"likelihood"],["network-device",0.3,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["device-takeover",0.2,"likelihood"],["ransomware",0.2,"likelihood"],["phishing",0.1,"likelihood"]]},{"id":"PR.PS-03","ref":"PR.PS-03","title":"Hardware is maintained, replaced, and removed commensurate with risk","text":"","threats":[["end-of-life",0.4,"likelihood"],["network-device",0.3,"likelihood"],["device-takeover",0.3,"likelihood"],["known-exploited",0.2,"likelihood"],["physical-access",0.1,"likelihood"]]},{"id":"PR.PS-04","ref":"PR.PS-04","title":"Log records are generated and made available for continuous monitoring","text":"","threats":[["detection-blinding",0.3,"impact"],["insider-misuse",0.3,"impact"],["ransomware",0.2,"impact"],["data-theft",0.2,"impact"],["lateral-admin",0.2,"impact"],["directory-takeover",0.2,"impact"],["bec",0.2,"impact"],["third-party-access",0.2,"impact"]]},{"id":"PR.PS-05","ref":"PR.PS-05","title":"Installation and execution of unauthorized software are prevented","text":"","threats":[["phishing",0.4,"likelihood"],["ransomware",0.4,"likelihood"],["privilege-escalation",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"PR.PS-06","ref":"PR.PS-06","title":"Secure software development practices are integrated, and their performance is monitored throughout the software development life cycle","text":"","threats":[["web-app",0.5,"likelihood"],["exploit-public",0.3,"likelihood"],["supply-chain",0.2,"likelihood"],["data-exposure",0.1,"likelihood"]]},{"id":"DE.AE-02","ref":"DE.AE-02","title":"Potentially adverse events are analyzed to better understand associated activities","text":"","threats":[["ransomware",0.2,"impact"],["directory-takeover",0.2,"impact"],["lateral-admin",0.2,"impact"],["data-theft",0.2,"impact"],["phishing",0.2,"impact"]]},{"id":"DE.AE-03","ref":"DE.AE-03","title":"Information is correlated from multiple sources","text":"","threats":[["detection-blinding",0.2,"impact"],["lateral-admin",0.2,"impact"],["directory-takeover",0.2,"impact"],["ransomware",0.2,"impact"]]},{"id":"DE.AE-04","ref":"DE.AE-04","title":"The estimated impact and scope of adverse events are understood","text":"","threats":[["ransomware",0.1,"impact"],["data-theft",0.1,"impact"]]},{"id":"DE.AE-06","ref":"DE.AE-06","title":"Information on adverse events is provided to authorized staff and tools","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"DE.AE-07","ref":"DE.AE-07","title":"Cyber threat intelligence and other contextual information are integrated into the analysis","text":"","threats":[["ransomware",0.2,"impact"],["supply-chain",0.2,"impact"],["phishing",0.1,"impact"]]},{"id":"DE.AE-08","ref":"DE.AE-08","title":"Incidents are declared when adverse events meet the defined incident criteria","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"DE.CM-01","ref":"DE.CM-01","title":"Networks and network services are monitored to find potentially adverse events","text":"","threats":[["lateral-admin",0.3,"impact"],["data-theft",0.3,"impact"],["ransomware",0.3,"impact"],["it-ot-pivot",0.3,"impact"],["wifi-intrusion",0.3,"impact"],["device-takeover",0.3,"impact"],["ddos",0.2,"impact"],["network-device",0.2,"impact"],["remote-access",0.2,"impact"]]},{"id":"DE.CM-02","ref":"DE.CM-02","title":"The physical environment is monitored to find potentially adverse events","text":"","threats":[["physical-access",0.3,"impact"]]},{"id":"DE.CM-03","ref":"DE.CM-03","title":"Personnel activity and technology usage are monitored to find potentially adverse events","text":"","threats":[["insider-misuse",0.4,"impact"],["remote-access",0.3,"impact"],["bec",0.3,"impact"],["third-party-access",0.3,"impact"],["lateral-admin",0.3,"impact"],["data-theft",0.3,"impact"],["directory-takeover",0.3,"impact"],["brute-force",0.2,"impact"]]},{"id":"DE.CM-06","ref":"DE.CM-06","title":"External service provider activities and services are monitored to find potentially adverse events","text":"","threats":[["third-party-access",0.4,"impact"],["supply-chain",0.2,"impact"],["internet-dependency",0.2,"impact"]]},{"id":"DE.CM-09","ref":"DE.CM-09","title":"Computing hardware and software, runtime environments, and their data are monitored to find potentially adverse events","text":"","threats":[["ransomware",0.4,"impact"],["privilege-escalation",0.3,"impact"],["detection-blinding",0.3,"impact"],["phishing",0.3,"impact"],["lateral-admin",0.3,"impact"],["exploit-public",0.3,"impact"],["defacement",0.3,"impact"],["directory-takeover",0.3,"impact"],["web-app",0.2,"impact"],["hypervisor-mass",0.2,"impact"],["ot-manipulation",0.2,"impact"],["supply-chain",0.2,"impact"]]},{"id":"RS.AN-03","ref":"RS.AN-03","title":"Analysis is performed to establish what has taken place during an incident and the root cause of the incident","text":"","threats":[["ransomware",0.2,"impact"],["directory-takeover",0.2,"impact"]]},{"id":"RS.AN-06","ref":"RS.AN-06","title":"Actions performed during an investigation are recorded, and the records' integrity and provenance are preserved","text":"","threats":[]},{"id":"RS.AN-07","ref":"RS.AN-07","title":"Incident data and metadata are collected, and their integrity and provenance are preserved","text":"","threats":[["insider-misuse",0.1,"impact"]]},{"id":"RS.AN-08","ref":"RS.AN-08","title":"An incident's magnitude is estimated and validated","text":"","threats":[["data-theft",0.1,"impact"]]},{"id":"RS.CO-02","ref":"RS.CO-02","title":"Internal and external stakeholders are notified of incidents","text":"","threats":[["bec",0.2,"impact"],["data-theft",0.1,"impact"]]},{"id":"RS.CO-03","ref":"RS.CO-03","title":"Information is shared with designated internal and external stakeholders","text":"","threats":[["defacement",0.1,"impact"]]},{"id":"RS.MA-01","ref":"RS.MA-01","title":"The incident response plan is executed in coordination with relevant third parties once an incident is declared","text":"","threats":[["ransomware",0.3,"impact"],["data-theft",0.2,"impact"],["directory-takeover",0.2,"impact"],["hypervisor-mass",0.2,"impact"],["bec",0.2,"impact"]]},{"id":"RS.MA-02","ref":"RS.MA-02","title":"Incident reports are triaged and validated","text":"","threats":[["ransomware",0.1,"impact"],["phishing",0.1,"impact"]]},{"id":"RS.MA-03","ref":"RS.MA-03","title":"Incidents are categorized and prioritized","text":"","threats":[]},{"id":"RS.MA-04","ref":"RS.MA-04","title":"Incidents are escalated or elevated as needed","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"RS.MA-05","ref":"RS.MA-05","title":"The criteria for initiating incident recovery are applied","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"RS.MI-01","ref":"RS.MI-01","title":"Incidents are contained","text":"","threats":[["ransomware",0.4,"impact"],["lateral-admin",0.3,"impact"],["data-theft",0.3,"impact"],["directory-takeover",0.3,"impact"],["hypervisor-mass",0.2,"impact"],["ddos",0.2,"impact"],["it-ot-pivot",0.2,"impact"],["phishing",0.2,"impact"],["bec",0.2,"impact"]]},{"id":"RS.MI-02","ref":"RS.MI-02","title":"Incidents are eradicated","text":"","threats":[["directory-takeover",0.3,"impact"],["ransomware",0.2,"impact"],["remote-access",0.1,"impact"],["phishing",0.1,"impact"]]},{"id":"RC.CO-03","ref":"RC.CO-03","title":"Recovery activities and progress in restoring operational capabilities are communicated to designated internal and external stakeholders","text":"","threats":[]},{"id":"RC.CO-04","ref":"RC.CO-04","title":"Public updates on incident recovery are shared using approved methods and messaging","text":"","threats":[["defacement",0.2,"impact"]]},{"id":"RC.RP-01","ref":"RC.RP-01","title":"The recovery portion of the incident response plan is executed once initiated from the incident response process","text":"","threats":[["ransomware",0.3,"impact"],["hypervisor-mass",0.3,"impact"],["directory-takeover",0.2,"impact"],["internet-dependency",0.1,"impact"]]},{"id":"RC.RP-02","ref":"RC.RP-02","title":"Recovery actions are selected, scoped, prioritized, and performed","text":"","threats":[["ransomware",0.2,"impact"],["hypervisor-mass",0.2,"impact"]]},{"id":"RC.RP-03","ref":"RC.RP-03","title":"The integrity of backups and other restoration assets is verified before using them for restoration","text":"","threats":[["ransomware",0.3,"impact"],["backup-destruction",0.3,"impact"]]},{"id":"RC.RP-04","ref":"RC.RP-04","title":"Critical mission functions and cybersecurity risk management are considered to establish post-incident operational norms","text":"","threats":[]},{"id":"RC.RP-05","ref":"RC.RP-05","title":"The integrity of restored assets is verified, systems and services are restored, and normal operating status is confirmed","text":"","threats":[["ransomware",0.2,"impact"],["directory-takeover",0.2,"impact"],["defacement",0.2,"impact"],["ot-manipulation",0.2,"impact"]]},{"id":"RC.RP-06","ref":"RC.RP-06","title":"The end of incident recovery is declared based on criteria, and incident-related documentation is completed","text":"","threats":[]}]},{"id":"cis-v8","name":"CIS Critical Security Controls v8.1","version":"8.1","license":"References only: safeguard titles are paraphrased; see cisecurity.org for the Controls.","url":"https://www.cisecurity.org/controls","requirements":[{"id":"1.1","ref":"1.1","title":"Complete register of hardware assets","text":"","ig":1,"threats":[["device-takeover",0.2,"likelihood"],["known-exploited",0.1,"likelihood"],["end-of-life",0.1,"likelihood"]]},{"id":"1.2","ref":"1.2","title":"Deal with devices not approved","text":"","ig":1,"threats":[["device-takeover",0.2,"likelihood"],["wifi-intrusion",0.2,"impact"],["physical-access",0.1,"impact"]]},{"id":"1.3","ref":"1.3","title":"Active scanning to discover devices","text":"","ig":2,"threats":[["device-takeover",0.2,"likelihood"],["wifi-intrusion",0.2,"impact"]]},{"id":"1.4","ref":"1.4","title":"Feed the asset register from DHCP records","text":"","ig":2,"threats":[["device-takeover",0.1,"likelihood"]]},{"id":"1.5","ref":"1.5","title":"Passive traffic listening to discover devices","text":"","ig":3,"threats":[["device-takeover",0.1,"likelihood"]]},{"id":"2.1","ref":"2.1","title":"Register of installed software","text":"","ig":1,"threats":[["end-of-life",0.2,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"2.2","ref":"2.2","title":"Only vendor-supported software in use","text":"","ig":1,"threats":[["end-of-life",0.6,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"2.3","ref":"2.3","title":"Remove or approve unlisted software","text":"","ig":1,"threats":[["end-of-life",0.1,"likelihood"],["ransomware",0.1,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"2.4","ref":"2.4","title":"Automated software inventory tooling","text":"","ig":2,"threats":[["end-of-life",0.1,"likelihood"],["known-exploited",0.1,"likelihood"]]},{"id":"2.5","ref":"2.5","title":"Application allowlisting for programs","text":"","ig":2,"threats":[["ransomware",0.5,"likelihood"],["phishing",0.4,"likelihood"],["privilege-escalation",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"],["supply-chain",0.1,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"2.6","ref":"2.6","title":"Allowlisting for code libraries","text":"","ig":2,"threats":[["ransomware",0.2,"likelihood"],["phishing",0.2,"likelihood"],["privilege-escalation",0.1,"likelihood"]]},{"id":"2.7","ref":"2.7","title":"Allowlisting or signing for scripts","text":"","ig":3,"threats":[["phishing",0.3,"likelihood"],["ransomware",0.3,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"3.1","ref":"3.1","title":"Rules for handling sensitive data","text":"","ig":1,"threats":[["data-exposure",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"3.2","ref":"3.2","title":"Register of sensitive data","text":"","ig":1,"threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"3.3","ref":"3.3","title":"Permissions restricting who reads data","text":"","ig":1,"threats":[["data-exposure",0.5,"likelihood"],["insider-misuse",0.3,"likelihood"],["data-theft",0.3,"impact"],["ransomware",0.1,"impact"]]},{"id":"3.4","ref":"3.4","title":"Delete data past its retention period","text":"","ig":1,"threats":[["data-theft",0.3,"impact"],["data-exposure",0.2,"impact"]]},{"id":"3.5","ref":"3.5","title":"Safe destruction of data","text":"","ig":1,"threats":[["data-theft",0.2,"likelihood"],["physical-access",0.2,"impact"]]},{"id":"3.6","ref":"3.6","title":"Disk encryption on laptops and workstations","text":"","ig":1,"threats":[["physical-access",0.6,"impact"],["data-theft",0.2,"impact"]]},{"id":"3.7","ref":"3.7","title":"Data sensitivity classification","text":"","ig":2,"threats":[["data-exposure",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"3.8","ref":"3.8","title":"Map of data movements","text":"","ig":2,"threats":[["data-exposure",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"3.9","ref":"3.9","title":"Encryption of USB and removable media","text":"","ig":2,"threats":[["physical-access",0.4,"impact"],["data-theft",0.2,"impact"]]},{"id":"3.10","ref":"3.10","title":"Encryption of sensitive data over networks","text":"","ig":2,"threats":[["data-theft",0.2,"likelihood"],["wifi-intrusion",0.2,"impact"],["lateral-admin",0.1,"likelihood"]]},{"id":"3.11","ref":"3.11","title":"Encryption of stored sensitive data","text":"","ig":2,"threats":[["data-theft",0.4,"impact"],["physical-access",0.3,"impact"],["data-exposure",0.3,"impact"]]},{"id":"3.12","ref":"3.12","title":"Separate storage by data sensitivity","text":"","ig":2,"threats":[["data-theft",0.3,"impact"],["data-exposure",0.2,"impact"],["lateral-admin",0.1,"likelihood"]]},{"id":"3.13","ref":"3.13","title":"Data leak prevention tooling","text":"","ig":3,"threats":[["data-theft",0.4,"impact"],["insider-misuse",0.3,"impact"],["data-exposure",0.2,"likelihood"]]},{"id":"3.14","ref":"3.14","title":"Logging of sensitive data access","text":"","ig":3,"threats":[["insider-misuse",0.3,"impact"],["data-theft",0.2,"impact"]]},{"id":"4.1","ref":"4.1","title":"Secure baseline configuration process","text":"","ig":1,"threats":[["default-credentials",0.3,"likelihood"],["data-exposure",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"],["device-takeover",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"]]},{"id":"4.2","ref":"4.2","title":"Hardening baseline for network equipment","text":"","ig":1,"threats":[["network-device",0.4,"likelihood"],["default-credentials",0.3,"likelihood"],["wifi-intrusion",0.2,"likelihood"]]},{"id":"4.3","ref":"4.3","title":"Lock idle sessions automatically","text":"","ig":1,"threats":[["physical-access",0.2,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"4.4","ref":"4.4","title":"Host firewall on servers","text":"","ig":1,"threats":[["lateral-admin",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["data-exposure",0.2,"likelihood"],["ransomware",0.2,"impact"]]},{"id":"4.5","ref":"4.5","title":"Host firewall on user devices","text":"","ig":1,"threats":[["lateral-admin",0.3,"likelihood"],["ransomware",0.2,"impact"],["wifi-intrusion",0.2,"impact"]]},{"id":"4.6","ref":"4.6","title":"Administer systems over secure channels","text":"","ig":1,"threats":[["network-device",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["hypervisor-mass",0.1,"likelihood"]]},{"id":"4.7","ref":"4.7","title":"Disable or change factory default accounts","text":"","ig":1,"threats":[["default-credentials",0.6,"likelihood"],["device-takeover",0.3,"likelihood"],["network-device",0.3,"likelihood"],["brute-force",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"]]},{"id":"4.8","ref":"4.8","title":"Turn off unneeded services and ports","text":"","ig":2,"threats":[["exploit-public",0.3,"likelihood"],["lateral-admin",0.2,"likelihood"],["device-takeover",0.2,"likelihood"],["network-device",0.2,"likelihood"],["privilege-escalation",0.1,"likelihood"]]},{"id":"4.9","ref":"4.9","title":"Point devices at trusted DNS resolvers","text":"","ig":2,"threats":[["phishing",0.1,"likelihood"],["ransomware",0.1,"likelihood"]]},{"id":"4.10","ref":"4.10","title":"Lock mobile devices after failed unlocks","text":"","ig":2,"threats":[["physical-access",0.3,"likelihood"]]},{"id":"4.11","ref":"4.11","title":"Remote wipe for mobile devices","text":"","ig":2,"threats":[["physical-access",0.4,"impact"],["data-theft",0.1,"impact"]]},{"id":"4.12","ref":"4.12","title":"Isolate work data on mobile devices","text":"","ig":3,"threats":[["device-takeover",0.2,"impact"],["data-theft",0.1,"impact"]]},{"id":"5.1","ref":"5.1","title":"Register of all user accounts","text":"","ig":1,"threats":[["insider-misuse",0.2,"likelihood"],["third-party-access",0.2,"likelihood"],["remote-access",0.1,"likelihood"]]},{"id":"5.2","ref":"5.2","title":"Distinct strong password per account","text":"","ig":1,"threats":[["brute-force",0.4,"likelihood"],["default-credentials",0.4,"likelihood"],["lateral-admin",0.3,"likelihood"],["remote-access",0.2,"likelihood"],["directory-takeover",0.1,"likelihood"]]},{"id":"5.3","ref":"5.3","title":"Disable accounts left unused","text":"","ig":1,"threats":[["insider-misuse",0.3,"likelihood"],["remote-access",0.2,"likelihood"],["brute-force",0.2,"likelihood"],["third-party-access",0.2,"likelihood"]]},{"id":"5.4","ref":"5.4","title":"Separate admin accounts from daily use","text":"","ig":1,"threats":[["directory-takeover",0.3,"likelihood"],["lateral-admin",0.3,"likelihood"],["phishing",0.3,"impact"],["ransomware",0.2,"impact"]]},{"id":"5.5","ref":"5.5","title":"Register of service accounts","text":"","ig":2,"threats":[["directory-takeover",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"5.6","ref":"5.6","title":"Manage accounts from a central directory","text":"","ig":2,"threats":[["insider-misuse",0.1,"likelihood"],["remote-access",0.1,"likelihood"],["third-party-access",0.1,"likelihood"]]},{"id":"6.1","ref":"6.1","title":"Formal process to give access","text":"","ig":1,"threats":[["insider-misuse",0.2,"likelihood"],["third-party-access",0.1,"likelihood"]]},{"id":"6.2","ref":"6.2","title":"Formal process to withdraw access","text":"","ig":1,"threats":[["insider-misuse",0.3,"likelihood"],["third-party-access",0.3,"likelihood"],["remote-access",0.2,"likelihood"]]},{"id":"6.3","ref":"6.3","title":"MFA on Internet-facing applications","text":"","ig":1,"threats":[["brute-force",0.6,"likelihood"],["bec",0.5,"likelihood"],["phishing",0.3,"likelihood"],["defacement",0.2,"likelihood"]]},{"id":"6.4","ref":"6.4","title":"MFA on VPN and remote entry","text":"","ig":1,"threats":[["remote-access",0.6,"likelihood"],["brute-force",0.4,"likelihood"],["third-party-access",0.3,"likelihood"],["ransomware",0.2,"likelihood"]]},{"id":"6.5","ref":"6.5","title":"MFA for administrator logins","text":"","ig":1,"threats":[["directory-takeover",0.4,"likelihood"],["lateral-admin",0.4,"likelihood"],["hypervisor-mass",0.4,"likelihood"],["network-device",0.3,"likelihood"],["ransomware",0.2,"likelihood"]]},{"id":"6.6","ref":"6.6","title":"List of authentication and authorisation systems","text":"","ig":2,"threats":[["remote-access",0.1,"likelihood"],["brute-force",0.1,"likelihood"]]},{"id":"6.7","ref":"6.7","title":"Central access control, e.g. single sign-on","text":"","ig":2,"threats":[["brute-force",0.2,"likelihood"],["third-party-access",0.2,"likelihood"],["remote-access",0.1,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"6.8","ref":"6.8","title":"Permissions assigned by job role","text":"","ig":3,"threats":[["insider-misuse",0.3,"likelihood"],["data-exposure",0.2,"likelihood"],["data-theft",0.2,"impact"],["lateral-admin",0.1,"likelihood"]]},{"id":"7.1","ref":"7.1","title":"Documented vulnerability handling process","text":"","ig":1,"threats":[["known-exploited",0.2,"likelihood"],["exploit-public",0.1,"likelihood"]]},{"id":"7.2","ref":"7.2","title":"Risk-based fix prioritisation process","text":"","ig":1,"threats":[["known-exploited",0.3,"likelihood"],["exploit-public",0.2,"likelihood"]]},{"id":"7.3","ref":"7.3","title":"Automatic operating system patching","text":"","ig":1,"threats":[["known-exploited",0.5,"likelihood"],["privilege-escalation",0.4,"likelihood"],["exploit-public",0.3,"likelihood"],["ransomware",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"]]},{"id":"7.4","ref":"7.4","title":"Automatic application patching","text":"","ig":1,"threats":[["known-exploited",0.4,"likelihood"],["exploit-public",0.3,"likelihood"],["phishing",0.2,"likelihood"],["web-app",0.2,"likelihood"]]},{"id":"7.5","ref":"7.5","title":"Vulnerability scanning of internal assets","text":"","ig":2,"threats":[["known-exploited",0.3,"likelihood"],["privilege-escalation",0.2,"likelihood"],["end-of-life",0.2,"likelihood"],["default-credentials",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"]]},{"id":"7.6","ref":"7.6","title":"Vulnerability scanning of Internet-facing assets","text":"","ig":2,"threats":[["exploit-public",0.4,"likelihood"],["known-exploited",0.4,"likelihood"],["web-app",0.3,"likelihood"],["network-device",0.3,"likelihood"],["data-exposure",0.3,"likelihood"],["remote-access",0.2,"likelihood"]]},{"id":"7.7","ref":"7.7","title":"Fix vulnerabilities found by scans","text":"","ig":2,"threats":[["known-exploited",0.4,"likelihood"],["exploit-public",0.3,"likelihood"],["web-app",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"],["network-device",0.2,"likelihood"]]},{"id":"8.1","ref":"8.1","title":"Documented log management process","text":"","ig":1,"threats":[["detection-blinding",0.1,"impact"],["insider-misuse",0.1,"impact"]]},{"id":"8.2","ref":"8.2","title":"Logging turned on across assets","text":"","ig":1,"threats":[["insider-misuse",0.2,"impact"],["detection-blinding",0.1,"impact"],["lateral-admin",0.1,"impact"],["data-theft",0.1,"impact"]]},{"id":"8.3","ref":"8.3","title":"Enough storage for logs","text":"","ig":1,"threats":[["detection-blinding",0.1,"impact"]]},{"id":"8.4","ref":"8.4","title":"Common time source for all systems","text":"","ig":2,"threats":[]},{"id":"8.5","ref":"8.5","title":"Detailed event logging","text":"","ig":2,"threats":[["insider-misuse",0.2,"impact"],["lateral-admin",0.2,"impact"],["data-theft",0.2,"impact"]]},{"id":"8.6","ref":"8.6","title":"Logging of DNS lookups","text":"","ig":2,"threats":[["phishing",0.2,"impact"],["data-theft",0.2,"impact"],["ransomware",0.1,"impact"]]},{"id":"8.7","ref":"8.7","title":"Logging of web requests","text":"","ig":2,"threats":[["phishing",0.2,"impact"],["data-theft",0.1,"impact"]]},{"id":"8.8","ref":"8.8","title":"Logging of commands and scripts run","text":"","ig":2,"threats":[["lateral-admin",0.3,"impact"],["privilege-escalation",0.2,"impact"],["ransomware",0.2,"impact"],["insider-misuse",0.2,"impact"]]},{"id":"8.9","ref":"8.9","title":"Central log collection","text":"","ig":2,"threats":[["detection-blinding",0.5,"impact"],["ransomware",0.2,"impact"],["lateral-admin",0.2,"impact"]]},{"id":"8.10","ref":"8.10","title":"Keep logs long enough","text":"","ig":2,"threats":[["detection-blinding",0.1,"impact"],["insider-misuse",0.1,"impact"],["data-theft",0.1,"impact"]]},{"id":"8.11","ref":"8.11","title":"Regular review of logs","text":"","ig":2,"threats":[["lateral-admin",0.2,"impact"],["insider-misuse",0.2,"impact"],["remote-access",0.2,"impact"],["brute-force",0.2,"impact"],["directory-takeover",0.2,"impact"]]},{"id":"8.12","ref":"8.12","title":"Gather logs from service providers","text":"","ig":3,"threats":[["third-party-access",0.3,"impact"],["bec",0.2,"impact"]]},{"id":"9.1","ref":"9.1","title":"Supported browsers and mail clients only","text":"","ig":1,"threats":[["phishing",0.3,"likelihood"],["end-of-life",0.3,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"9.2","ref":"9.2","title":"DNS filtering of malicious domains","text":"","ig":1,"threats":[["phishing",0.4,"likelihood"],["ransomware",0.2,"likelihood"],["data-theft",0.1,"impact"]]},{"id":"9.3","ref":"9.3","title":"Web filtering by URL or category","text":"","ig":2,"threats":[["phishing",0.3,"likelihood"],["ransomware",0.1,"likelihood"],["data-theft",0.1,"impact"]]},{"id":"9.4","ref":"9.4","title":"Limit browser and mail extensions","text":"","ig":2,"threats":[["phishing",0.2,"likelihood"],["data-theft",0.1,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"9.5","ref":"9.5","title":"DMARC on mail domains","text":"","ig":2,"threats":[["bec",0.5,"likelihood"],["phishing",0.2,"likelihood"]]},{"id":"9.6","ref":"9.6","title":"Block risky attachment file types","text":"","ig":2,"threats":[["phishing",0.4,"likelihood"],["ransomware",0.3,"likelihood"]]},{"id":"9.7","ref":"9.7","title":"Malware scanning at the mail server","text":"","ig":3,"threats":[["phishing",0.4,"likelihood"],["ransomware",0.2,"likelihood"]]},{"id":"10.1","ref":"10.1","title":"Anti-malware on every asset","text":"","ig":1,"threats":[["ransomware",0.4,"likelihood"],["phishing",0.3,"likelihood"]]},{"id":"10.2","ref":"10.2","title":"Automatic anti-malware definition updates","text":"","ig":1,"threats":[["ransomware",0.2,"likelihood"],["phishing",0.2,"likelihood"]]},{"id":"10.3","ref":"10.3","title":"No autorun from removable media","text":"","ig":1,"threats":[["physical-access",0.3,"likelihood"]]},{"id":"10.4","ref":"10.4","title":"Scan removable media when inserted","text":"","ig":2,"threats":[["physical-access",0.2,"likelihood"]]},{"id":"10.5","ref":"10.5","title":"Operating system exploit mitigations turned on","text":"","ig":2,"threats":[["privilege-escalation",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["phishing",0.2,"likelihood"],["ransomware",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"10.6","ref":"10.6","title":"Central console for anti-malware","text":"","ig":2,"threats":[["detection-blinding",0.3,"likelihood"],["ransomware",0.2,"likelihood"]]},{"id":"10.7","ref":"10.7","title":"Behaviour-based malware detection","text":"","ig":2,"threats":[["ransomware",0.4,"likelihood"],["phishing",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"],["lateral-admin",0.2,"impact"]]},{"id":"11.1","ref":"11.1","title":"Documented data recovery process","text":"","ig":1,"threats":[["ransomware",0.3,"impact"],["hypervisor-mass",0.3,"impact"],["backup-destruction",0.1,"impact"]]},{"id":"11.2","ref":"11.2","title":"Scheduled automatic backups","text":"","ig":1,"threats":[["ransomware",0.5,"impact"],["hypervisor-mass",0.4,"impact"],["insider-misuse",0.3,"impact"],["defacement",0.3,"impact"],["ot-manipulation",0.2,"impact"],["directory-takeover",0.2,"impact"]]},{"id":"11.3","ref":"11.3","title":"Protect backups like the source data","text":"","ig":1,"threats":[["backup-destruction",0.3,"likelihood"],["ransomware",0.2,"impact"],["data-theft",0.2,"likelihood"]]},{"id":"11.4","ref":"11.4","title":"Isolated, offline or immutable backup copy","text":"","ig":1,"threats":[["backup-destruction",0.6,"likelihood"],["ransomware",0.6,"impact"],["hypervisor-mass",0.5,"impact"],["insider-misuse",0.3,"impact"]]},{"id":"11.5","ref":"11.5","title":"Regular restore tests","text":"","ig":2,"threats":[["ransomware",0.3,"impact"],["hypervisor-mass",0.3,"impact"],["directory-takeover",0.2,"impact"]]},{"id":"12.1","ref":"12.1","title":"Current firmware on network equipment","text":"","ig":1,"threats":[["network-device",0.5,"likelihood"],["known-exploited",0.4,"likelihood"],["end-of-life",0.3,"likelihood"],["remote-access",0.2,"likelihood"]]},{"id":"12.2","ref":"12.2","title":"Segmented, secure network design","text":"","ig":2,"threats":[["it-ot-pivot",0.5,"likelihood"],["lateral-admin",0.4,"likelihood"],["ransomware",0.3,"impact"],["device-takeover",0.3,"impact"],["wifi-intrusion",0.3,"impact"],["exploit-public",0.2,"impact"],["hypervisor-mass",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"12.3","ref":"12.3","title":"Secure administration of network equipment","text":"","ig":2,"threats":[["network-device",0.4,"likelihood"],["default-credentials",0.2,"likelihood"]]},{"id":"12.4","ref":"12.4","title":"Up-to-date network diagrams","text":"","ig":2,"threats":[["it-ot-pivot",0.1,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"12.5","ref":"12.5","title":"Central AAA for network devices","text":"","ig":2,"threats":[["network-device",0.3,"likelihood"],["wifi-intrusion",0.2,"likelihood"],["insider-misuse",0.2,"impact"]]},{"id":"12.6","ref":"12.6","title":"Encrypted management and network protocols","text":"","ig":2,"threats":[["wifi-intrusion",0.3,"likelihood"],["network-device",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"12.7","ref":"12.7","title":"Remote users routed through VPN with central auth","text":"","ig":2,"threats":[["remote-access",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"12.8","ref":"12.8","title":"Dedicated isolated machines for admin work","text":"","ig":3,"threats":[["directory-takeover",0.4,"likelihood"],["lateral-admin",0.4,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["network-device",0.2,"likelihood"],["phishing",0.3,"impact"]]},{"id":"13.1","ref":"13.1","title":"Central correlation and alerting of events","text":"","ig":2,"threats":[["ransomware",0.3,"impact"],["lateral-admin",0.3,"impact"],["directory-takeover",0.3,"impact"],["detection-blinding",0.3,"impact"],["brute-force",0.2,"impact"],["remote-access",0.2,"impact"]]},{"id":"13.2","ref":"13.2","title":"Host-based intrusion detection","text":"","ig":2,"threats":[["privilege-escalation",0.3,"impact"],["ransomware",0.3,"impact"],["lateral-admin",0.3,"impact"],["exploit-public",0.2,"impact"],["phishing",0.2,"impact"]]},{"id":"13.3","ref":"13.3","title":"Network-based intrusion detection","text":"","ig":2,"threats":[["lateral-admin",0.3,"impact"],["it-ot-pivot",0.3,"impact"],["exploit-public",0.2,"impact"],["data-theft",0.2,"impact"],["device-takeover",0.2,"impact"]]},{"id":"13.4","ref":"13.4","title":"Filtering between network zones","text":"","ig":2,"threats":[["lateral-admin",0.5,"likelihood"],["it-ot-pivot",0.5,"likelihood"],["ransomware",0.4,"impact"],["device-takeover",0.3,"impact"],["hypervisor-mass",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"13.5","ref":"13.5","title":"Access control for remote endpoints","text":"","ig":2,"threats":[["remote-access",0.3,"likelihood"],["third-party-access",0.2,"likelihood"]]},{"id":"13.6","ref":"13.6","title":"Network flow logging","text":"","ig":2,"threats":[["data-theft",0.3,"impact"],["lateral-admin",0.2,"impact"]]},{"id":"13.7","ref":"13.7","title":"Host-based intrusion prevention","text":"","ig":3,"threats":[["ransomware",0.3,"likelihood"],["exploit-public",0.2,"likelihood"],["privilege-escalation",0.2,"likelihood"]]},{"id":"13.8","ref":"13.8","title":"Network-based intrusion prevention","text":"","ig":3,"threats":[["exploit-public",0.3,"likelihood"],["known-exploited",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"]]},{"id":"13.9","ref":"13.9","title":"Device authentication on switch ports (802.1X)","text":"","ig":3,"threats":[["physical-access",0.3,"likelihood"],["wifi-intrusion",0.3,"likelihood"],["device-takeover",0.1,"impact"]]},{"id":"13.10","ref":"13.10","title":"Application-layer filtering (WAF, proxy)","text":"","ig":3,"threats":[["web-app",0.4,"likelihood"],["exploit-public",0.3,"likelihood"],["data-theft",0.2,"likelihood"],["ddos",0.2,"impact"]]},{"id":"13.11","ref":"13.11","title":"Tuning of alert thresholds","text":"","ig":3,"threats":[["ransomware",0.1,"impact"]]},{"id":"14.1","ref":"14.1","title":"Security awareness programme","text":"","ig":1,"threats":[["phishing",0.3,"likelihood"],["bec",0.3,"likelihood"]]},{"id":"14.2","ref":"14.2","title":"Training to spot social engineering","text":"","ig":1,"threats":[["phishing",0.4,"likelihood"],["bec",0.4,"likelihood"]]},{"id":"14.3","ref":"14.3","title":"Training on passwords and MFA use","text":"","ig":1,"threats":[["brute-force",0.2,"likelihood"],["remote-access",0.2,"likelihood"],["phishing",0.1,"likelihood"]]},{"id":"14.4","ref":"14.4","title":"Training on handling sensitive data","text":"","ig":1,"threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"14.5","ref":"14.5","title":"Training on accidental data disclosure","text":"","ig":1,"threats":[["data-exposure",0.3,"likelihood"]]},{"id":"14.6","ref":"14.6","title":"Training to report suspected incidents","text":"","ig":1,"threats":[["phishing",0.3,"impact"],["bec",0.2,"impact"],["physical-access",0.1,"impact"]]},{"id":"14.7","ref":"14.7","title":"Training to report missing updates","text":"","ig":1,"threats":[["known-exploited",0.1,"likelihood"]]},{"id":"14.8","ref":"14.8","title":"Training on risks of untrusted networks","text":"","ig":1,"threats":[["wifi-intrusion",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"14.9","ref":"14.9","title":"Security training tailored to job roles","text":"","ig":2,"threats":[["phishing",0.2,"likelihood"],["bec",0.2,"likelihood"],["web-app",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"15.1","ref":"15.1","title":"Register of service providers","text":"","ig":1,"threats":[["third-party-access",0.2,"likelihood"],["internet-dependency",0.2,"impact"],["supply-chain",0.1,"likelihood"]]},{"id":"15.2","ref":"15.2","title":"Policy for managing service providers","text":"","ig":2,"threats":[["third-party-access",0.2,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"15.3","ref":"15.3","title":"Rank service providers by risk","text":"","ig":2,"threats":[["supply-chain",0.1,"likelihood"],["third-party-access",0.1,"likelihood"],["internet-dependency",0.1,"impact"]]},{"id":"15.4","ref":"15.4","title":"Security clauses in supplier contracts","text":"","ig":2,"threats":[["third-party-access",0.3,"likelihood"],["supply-chain",0.2,"likelihood"],["internet-dependency",0.2,"impact"]]},{"id":"15.5","ref":"15.5","title":"Assessment of service providers' security","text":"","ig":3,"threats":[["supply-chain",0.3,"likelihood"],["third-party-access",0.3,"likelihood"]]},{"id":"15.6","ref":"15.6","title":"Ongoing monitoring of service providers","text":"","ig":3,"threats":[["third-party-access",0.3,"impact"],["supply-chain",0.2,"impact"],["internet-dependency",0.2,"impact"]]},{"id":"15.7","ref":"15.7","title":"Secure offboarding of service providers","text":"","ig":3,"threats":[["third-party-access",0.3,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"16.1","ref":"16.1","title":"Secure software development lifecycle","text":"","ig":2,"threats":[["web-app",0.4,"likelihood"],["exploit-public",0.2,"likelihood"]]},{"id":"16.2","ref":"16.2","title":"Intake and handling of reported software flaws","text":"","ig":2,"threats":[["web-app",0.3,"likelihood"],["exploit-public",0.2,"likelihood"]]},{"id":"16.3","ref":"16.3","title":"Root-cause analysis of code flaws","text":"","ig":2,"threats":[["web-app",0.2,"likelihood"]]},{"id":"16.4","ref":"16.4","title":"Inventory of third-party code components","text":"","ig":2,"threats":[["supply-chain",0.3,"likelihood"],["web-app",0.2,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"16.5","ref":"16.5","title":"Current, trusted third-party components","text":"","ig":2,"threats":[["supply-chain",0.4,"likelihood"],["web-app",0.3,"likelihood"],["known-exploited",0.2,"likelihood"]]},{"id":"16.6","ref":"16.6","title":"Severity rating for application flaws","text":"","ig":2,"threats":[["web-app",0.1,"likelihood"]]},{"id":"16.7","ref":"16.7","title":"Hardened templates for application infrastructure","text":"","ig":2,"threats":[["data-exposure",0.3,"likelihood"],["web-app",0.2,"likelihood"],["default-credentials",0.2,"likelihood"]]},{"id":"16.8","ref":"16.8","title":"Production kept apart from test systems","text":"","ig":2,"threats":[["data-exposure",0.3,"likelihood"],["data-theft",0.2,"likelihood"],["web-app",0.1,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"16.9","ref":"16.9","title":"Secure-coding training for developers","text":"","ig":2,"threats":[["web-app",0.3,"likelihood"]]},{"id":"16.10","ref":"16.10","title":"Secure design of application architecture","text":"","ig":2,"threats":[["web-app",0.3,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"16.11","ref":"16.11","title":"Reuse vetted security modules (auth, crypto)","text":"","ig":2,"threats":[["web-app",0.3,"likelihood"]]},{"id":"16.12","ref":"16.12","title":"Automated code security analysis","text":"","ig":3,"threats":[["web-app",0.4,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"16.13","ref":"16.13","title":"Penetration tests of applications","text":"","ig":3,"threats":[["web-app",0.4,"likelihood"],["exploit-public",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"16.14","ref":"16.14","title":"Threat modelling of applications","text":"","ig":3,"threats":[["web-app",0.2,"likelihood"]]},{"id":"17.1","ref":"17.1","title":"Named lead for incident handling","text":"","ig":1,"threats":[["ransomware",0.1,"impact"]]},{"id":"17.2","ref":"17.2","title":"Up-to-date incident contact list","text":"","ig":1,"threats":[["ransomware",0.1,"impact"],["bec",0.1,"impact"]]},{"id":"17.3","ref":"17.3","title":"Way for staff to report incidents","text":"","ig":1,"threats":[["phishing",0.2,"impact"],["bec",0.2,"impact"],["insider-misuse",0.1,"impact"]]},{"id":"17.4","ref":"17.4","title":"Documented incident response process","text":"","ig":2,"threats":[["ransomware",0.3,"impact"],["data-theft",0.2,"impact"],["directory-takeover",0.2,"impact"],["hypervisor-mass",0.2,"impact"],["bec",0.2,"impact"]]},{"id":"17.5","ref":"17.5","title":"Incident response roles assigned","text":"","ig":2,"threats":[["ransomware",0.1,"impact"]]},{"id":"17.6","ref":"17.6","title":"Communication channels during incidents","text":"","ig":2,"threats":[["ransomware",0.2,"impact"],["directory-takeover",0.1,"impact"],["defacement",0.1,"impact"]]},{"id":"17.7","ref":"17.7","title":"Regular incident response exercises","text":"","ig":2,"threats":[["ransomware",0.3,"impact"],["hypervisor-mass",0.2,"impact"],["ddos",0.1,"impact"]]},{"id":"17.8","ref":"17.8","title":"Lessons-learned review after incidents","text":"","ig":2,"threats":[["ransomware",0.1,"impact"]]},{"id":"17.9","ref":"17.9","title":"Thresholds that define an incident","text":"","ig":3,"threats":[["ransomware",0.1,"impact"]]},{"id":"18.1","ref":"18.1","title":"Penetration testing programme","text":"","ig":2,"threats":[["exploit-public",0.2,"likelihood"],["lateral-admin",0.1,"likelihood"],["directory-takeover",0.1,"likelihood"]]},{"id":"18.2","ref":"18.2","title":"External penetration tests","text":"","ig":2,"threats":[["exploit-public",0.3,"likelihood"],["web-app",0.3,"likelihood"],["remote-access",0.2,"likelihood"],["data-exposure",0.2,"likelihood"],["network-device",0.2,"likelihood"]]},{"id":"18.3","ref":"18.3","title":"Fix penetration test findings","text":"","ig":2,"threats":[["exploit-public",0.3,"likelihood"],["web-app",0.2,"likelihood"],["directory-takeover",0.2,"likelihood"],["lateral-admin",0.2,"likelihood"]]},{"id":"18.4","ref":"18.4","title":"Test that security controls actually work","text":"","ig":3,"threats":[["detection-blinding",0.2,"impact"],["lateral-admin",0.1,"impact"]]},{"id":"18.5","ref":"18.5","title":"Internal penetration tests","text":"","ig":3,"threats":[["directory-takeover",0.3,"likelihood"],["lateral-admin",0.3,"likelihood"],["privilege-escalation",0.2,"likelihood"],["it-ot-pivot",0.2,"likelihood"],["default-credentials",0.2,"likelihood"]]}]},{"id":"iso27001","name":"ISO/IEC 27001:2022 Annex A","version":"2022","license":"References only: the control titles are paraphrased; the standard's text is not reproduced.","url":"https://www.iso.org/standard/27001","requirements":[{"id":"5.1","ref":"5.1","title":"Approved set of security policies","text":"","threats":[]},{"id":"5.2","ref":"5.2","title":"Assigning security roles","text":"","threats":[]},{"id":"5.3","ref":"5.3","title":"Separating conflicting duties","text":"","threats":[["insider-misuse",0.3,"likelihood"],["bec",0.2,"likelihood"]]},{"id":"5.4","ref":"5.4","title":"Management enforcing security rules","text":"","threats":[]},{"id":"5.5","ref":"5.5","title":"Relationship with relevant authorities","text":"","threats":[["ransomware",0.1,"impact"]]},{"id":"5.6","ref":"5.6","title":"Links with security communities and forums","text":"","threats":[["known-exploited",0.1,"likelihood"]]},{"id":"5.7","ref":"5.7","title":"Collecting and using threat information","text":"","threats":[["known-exploited",0.2,"likelihood"],["phishing",0.1,"likelihood"]]},{"id":"5.8","ref":"5.8","title":"Security built into projects","text":"","threats":[["web-app",0.1,"likelihood"],["data-exposure",0.1,"likelihood"]]},{"id":"5.9","ref":"5.9","title":"Inventory of information assets and owners","text":"","threats":[["end-of-life",0.2,"likelihood"],["known-exploited",0.1,"likelihood"],["data-exposure",0.1,"likelihood"]]},{"id":"5.10","ref":"5.10","title":"Rules for permitted use of assets","text":"","threats":[["insider-misuse",0.1,"likelihood"],["phishing",0.1,"likelihood"]]},{"id":"5.11","ref":"5.11","title":"Handing back assets on departure","text":"","threats":[["insider-misuse",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"5.12","ref":"5.12","title":"Classifying information by sensitivity","text":"","threats":[["data-exposure",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"5.13","ref":"5.13","title":"Marking information with its class","text":"","threats":[["data-exposure",0.1,"likelihood"]]},{"id":"5.14","ref":"5.14","title":"Securing information exchanges","text":"","threats":[["data-theft",0.2,"likelihood"],["bec",0.2,"likelihood"]]},{"id":"5.15","ref":"5.15","title":"Rules governing access","text":"","threats":[["data-exposure",0.2,"likelihood"],["insider-misuse",0.2,"likelihood"]]},{"id":"5.16","ref":"5.16","title":"Identity lifecycle handling","text":"","threats":[["insider-misuse",0.2,"likelihood"],["remote-access",0.1,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"5.17","ref":"5.17","title":"Handling passwords and other secrets","text":"","threats":[["brute-force",0.3,"likelihood"],["default-credentials",0.3,"likelihood"],["remote-access",0.2,"likelihood"]]},{"id":"5.18","ref":"5.18","title":"Granting, reviewing and revoking permissions","text":"","threats":[["insider-misuse",0.3,"likelihood"],["third-party-access",0.2,"likelihood"],["data-exposure",0.2,"likelihood"]]},{"id":"5.19","ref":"5.19","title":"Security with suppliers","text":"","threats":[["third-party-access",0.3,"likelihood"],["supply-chain",0.2,"likelihood"]]},{"id":"5.20","ref":"5.20","title":"Security clauses in supplier contracts","text":"","threats":[["third-party-access",0.2,"likelihood"],["supply-chain",0.1,"likelihood"],["internet-dependency",0.1,"impact"]]},{"id":"5.21","ref":"5.21","title":"Security of the ICT supply chain","text":"","threats":[["supply-chain",0.4,"likelihood"]]},{"id":"5.22","ref":"5.22","title":"Overseeing supplier services and their changes","text":"","threats":[["third-party-access",0.2,"likelihood"],["supply-chain",0.2,"likelihood"],["internet-dependency",0.1,"likelihood"]]},{"id":"5.23","ref":"5.23","title":"Security when using cloud services","text":"","threats":[["data-exposure",0.3,"likelihood"],["internet-dependency",0.3,"impact"]]},{"id":"5.24","ref":"5.24","title":"Preparing for incident handling","text":"","threats":[["ransomware",0.2,"impact"],["data-theft",0.1,"impact"]]},{"id":"5.25","ref":"5.25","title":"Triage and classification of security events","text":"","threats":[["ransomware",0.1,"impact"],["lateral-admin",0.1,"impact"]]},{"id":"5.26","ref":"5.26","title":"Handling security incidents","text":"","threats":[["ransomware",0.3,"impact"],["directory-takeover",0.2,"impact"],["data-theft",0.2,"impact"],["bec",0.2,"impact"]]},{"id":"5.27","ref":"5.27","title":"Learning lessons after incidents","text":"","threats":[]},{"id":"5.28","ref":"5.28","title":"Gathering and preserving evidence","text":"","threats":[["insider-misuse",0.1,"impact"]]},{"id":"5.29","ref":"5.29","title":"Keeping security during disruptions","text":"","threats":[["ransomware",0.1,"impact"],["ddos",0.1,"impact"]]},{"id":"5.30","ref":"5.30","title":"ICT preparedness for continuity","text":"","threats":[["ransomware",0.4,"impact"],["hypervisor-mass",0.4,"impact"],["internet-dependency",0.4,"impact"],["ddos",0.3,"impact"],["backup-destruction",0.2,"impact"]]},{"id":"5.31","ref":"5.31","title":"Meeting legal and contractual obligations","text":"","threats":[]},{"id":"5.32","ref":"5.32","title":"Protecting intellectual property","text":"","threats":[]},{"id":"5.33","ref":"5.33","title":"Safeguarding records","text":"","threats":[["insider-misuse",0.1,"impact"]]},{"id":"5.34","ref":"5.34","title":"Privacy and personal data","text":"","threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.2,"impact"]]},{"id":"5.35","ref":"5.35","title":"Independent assessment of security","text":"","threats":[["data-exposure",0.1,"likelihood"]]},{"id":"5.36","ref":"5.36","title":"Checking adherence to security rules","text":"","threats":[["default-credentials",0.1,"likelihood"],["data-exposure",0.1,"likelihood"]]},{"id":"5.37","ref":"5.37","title":"Written operational procedures","text":"","threats":[]},{"id":"6.1","ref":"6.1","title":"Background checks on staff","text":"","threats":[["insider-misuse",0.3,"likelihood"]]},{"id":"6.2","ref":"6.2","title":"Security duties in employment contracts","text":"","threats":[["insider-misuse",0.1,"likelihood"]]},{"id":"6.3","ref":"6.3","title":"Security awareness and training","text":"","threats":[["phishing",0.4,"likelihood"],["bec",0.3,"likelihood"],["brute-force",0.1,"likelihood"]]},{"id":"6.4","ref":"6.4","title":"Sanctions for security breaches","text":"","threats":[["insider-misuse",0.1,"likelihood"]]},{"id":"6.5","ref":"6.5","title":"Obligations after leaving or changing role","text":"","threats":[["insider-misuse",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"6.6","ref":"6.6","title":"Non-disclosure commitments","text":"","threats":[["data-theft",0.1,"likelihood"]]},{"id":"6.7","ref":"6.7","title":"Security of work done remotely","text":"","threats":[["remote-access",0.2,"likelihood"],["physical-access",0.1,"likelihood"]]},{"id":"6.8","ref":"6.8","title":"Staff reporting of security events","text":"","threats":[["phishing",0.2,"impact"],["bec",0.2,"impact"],["ransomware",0.1,"impact"]]},{"id":"7.1","ref":"7.1","title":"Defined physical security boundaries","text":"","threats":[["physical-access",0.3,"likelihood"]]},{"id":"7.2","ref":"7.2","title":"Controlling physical entry","text":"","threats":[["physical-access",0.4,"likelihood"]]},{"id":"7.3","ref":"7.3","title":"Physical protection of offices and facilities","text":"","threats":[["physical-access",0.3,"likelihood"]]},{"id":"7.4","ref":"7.4","title":"Surveillance of premises","text":"","threats":[["physical-access",0.2,"likelihood"]]},{"id":"7.5","ref":"7.5","title":"Protection from environmental hazards","text":"","threats":[]},{"id":"7.6","ref":"7.6","title":"Rules for working in protected areas","text":"","threats":[["physical-access",0.2,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"7.7","ref":"7.7","title":"Tidy desks and locked screens","text":"","threats":[["physical-access",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"7.8","ref":"7.8","title":"Placement and protection of equipment","text":"","threats":[["physical-access",0.2,"likelihood"]]},{"id":"7.9","ref":"7.9","title":"Protecting equipment outside the premises","text":"","threats":[["physical-access",0.3,"likelihood"]]},{"id":"7.10","ref":"7.10","title":"Handling of storage media","text":"","threats":[["physical-access",0.3,"likelihood"],["data-theft",0.2,"likelihood"]]},{"id":"7.11","ref":"7.11","title":"Power and other supporting services","text":"","threats":[["internet-dependency",0.1,"impact"]]},{"id":"7.12","ref":"7.12","title":"Protecting power and network cables","text":"","threats":[["physical-access",0.1,"likelihood"]]},{"id":"7.13","ref":"7.13","title":"Maintaining equipment properly","text":"","threats":[]},{"id":"7.14","ref":"7.14","title":"Safe disposal or reuse of equipment","text":"","threats":[["data-theft",0.2,"likelihood"],["physical-access",0.2,"impact"]]},{"id":"8.1","ref":"8.1","title":"Protecting user devices","text":"","threats":[["physical-access",0.3,"impact"],["device-takeover",0.2,"likelihood"],["phishing",0.1,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"8.2","ref":"8.2","title":"Restricting and managing privileged access","text":"","threats":[["directory-takeover",0.4,"likelihood"],["lateral-admin",0.3,"likelihood"],["insider-misuse",0.3,"likelihood"],["hypervisor-mass",0.3,"likelihood"]]},{"id":"8.3","ref":"8.3","title":"Limiting access to information","text":"","threats":[["data-exposure",0.3,"likelihood"],["data-theft",0.2,"likelihood"],["insider-misuse",0.2,"likelihood"]]},{"id":"8.4","ref":"8.4","title":"Controlling access to source code","text":"","threats":[["supply-chain",0.1,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"8.5","ref":"8.5","title":"Robust authentication mechanisms","text":"","threats":[["remote-access",0.5,"likelihood"],["brute-force",0.5,"likelihood"],["bec",0.3,"likelihood"],["phishing",0.2,"likelihood"],["default-credentials",0.2,"likelihood"]]},{"id":"8.6","ref":"8.6","title":"Managing resource capacity","text":"","threats":[["ddos",0.2,"impact"]]},{"id":"8.7","ref":"8.7","title":"Defending against malicious software","text":"","threats":[["ransomware",0.3,"likelihood"],["phishing",0.3,"likelihood"],["device-takeover",0.1,"likelihood"]]},{"id":"8.8","ref":"8.8","title":"Handling technical vulnerabilities","text":"","threats":[["known-exploited",0.6,"likelihood"],["exploit-public",0.5,"likelihood"],["network-device",0.4,"likelihood"],["privilege-escalation",0.4,"likelihood"],["hypervisor-mass",0.3,"likelihood"],["web-app",0.3,"likelihood"],["device-takeover",0.3,"likelihood"],["end-of-life",0.2,"likelihood"],["defacement",0.2,"likelihood"]]},{"id":"8.9","ref":"8.9","title":"Secure configuration baselines","text":"","threats":[["default-credentials",0.4,"likelihood"],["data-exposure",0.4,"likelihood"],["device-takeover",0.2,"likelihood"],["network-device",0.2,"likelihood"],["exploit-public",0.2,"likelihood"]]},{"id":"8.10","ref":"8.10","title":"Erasing information no longer needed","text":"","threats":[["data-theft",0.2,"impact"]]},{"id":"8.11","ref":"8.11","title":"Masking or obfuscating data","text":"","threats":[["data-theft",0.2,"impact"],["data-exposure",0.2,"impact"]]},{"id":"8.12","ref":"8.12","title":"Preventing data leaks","text":"","threats":[["data-theft",0.3,"likelihood"],["insider-misuse",0.2,"likelihood"]]},{"id":"8.13","ref":"8.13","title":"Backing up information","text":"","threats":[["ransomware",0.6,"impact"],["hypervisor-mass",0.5,"impact"],["backup-destruction",0.3,"likelihood"],["defacement",0.3,"impact"],["insider-misuse",0.2,"impact"],["ot-manipulation",0.1,"impact"]]},{"id":"8.14","ref":"8.14","title":"Redundant processing capacity","text":"","threats":[["internet-dependency",0.4,"impact"],["ddos",0.2,"impact"],["hypervisor-mass",0.1,"impact"]]},{"id":"8.15","ref":"8.15","title":"Recording and protecting event logs","text":"","threats":[["detection-blinding",0.3,"likelihood"],["lateral-admin",0.2,"impact"],["insider-misuse",0.2,"impact"],["data-theft",0.2,"impact"]]},{"id":"8.16","ref":"8.16","title":"Watching for anomalous activity","text":"","threats":[["ransomware",0.3,"impact"],["data-theft",0.3,"impact"],["lateral-admin",0.3,"impact"],["directory-takeover",0.3,"impact"],["brute-force",0.2,"impact"],["detection-blinding",0.2,"impact"],["ot-manipulation",0.2,"impact"],["it-ot-pivot",0.2,"impact"]]},{"id":"8.17","ref":"8.17","title":"Common time source for systems","text":"","threats":[["lateral-admin",0.1,"impact"]]},{"id":"8.18","ref":"8.18","title":"Restricting powerful system utilities","text":"","threats":[["privilege-escalation",0.1,"likelihood"],["lateral-admin",0.1,"likelihood"]]},{"id":"8.19","ref":"8.19","title":"Controlling software installation","text":"","threats":[["phishing",0.2,"likelihood"],["ransomware",0.1,"likelihood"],["supply-chain",0.1,"likelihood"]]},{"id":"8.20","ref":"8.20","title":"Protecting networks and devices on them","text":"","threats":[["lateral-admin",0.2,"likelihood"],["network-device",0.2,"likelihood"],["wifi-intrusion",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"8.21","ref":"8.21","title":"Securing network services","text":"","threats":[["ddos",0.2,"impact"],["internet-dependency",0.2,"likelihood"],["network-device",0.1,"likelihood"]]},{"id":"8.22","ref":"8.22","title":"Splitting networks into zones","text":"","threats":[["it-ot-pivot",0.5,"likelihood"],["lateral-admin",0.4,"likelihood"],["ransomware",0.3,"impact"],["wifi-intrusion",0.3,"impact"],["ot-manipulation",0.2,"likelihood"],["hypervisor-mass",0.2,"likelihood"],["device-takeover",0.2,"impact"],["exploit-public",0.2,"impact"]]},{"id":"8.23","ref":"8.23","title":"Filtering web browsing","text":"","threats":[["phishing",0.3,"likelihood"],["ransomware",0.1,"likelihood"]]},{"id":"8.24","ref":"8.24","title":"Encryption and key handling","text":"","threats":[["data-theft",0.3,"impact"],["physical-access",0.3,"impact"],["data-exposure",0.2,"impact"]]},{"id":"8.25","ref":"8.25","title":"Security across the development process","text":"","threats":[["web-app",0.3,"likelihood"]]},{"id":"8.26","ref":"8.26","title":"Defining application security needs","text":"","threats":[["web-app",0.3,"likelihood"]]},{"id":"8.27","ref":"8.27","title":"Security-by-design architecture","text":"","threats":[["lateral-admin",0.2,"likelihood"],["exploit-public",0.2,"impact"],["web-app",0.1,"likelihood"],["hypervisor-mass",0.1,"likelihood"]]},{"id":"8.28","ref":"8.28","title":"Writing code securely","text":"","threats":[["web-app",0.5,"likelihood"]]},{"id":"8.29","ref":"8.29","title":"Testing security before release","text":"","threats":[["web-app",0.3,"likelihood"],["exploit-public",0.1,"likelihood"]]},{"id":"8.30","ref":"8.30","title":"Overseeing externally developed software","text":"","threats":[["supply-chain",0.2,"likelihood"],["web-app",0.1,"likelihood"]]},{"id":"8.31","ref":"8.31","title":"Keeping dev, test and production apart","text":"","threats":[["data-exposure",0.2,"likelihood"],["insider-misuse",0.1,"likelihood"]]},{"id":"8.32","ref":"8.32","title":"Controlled changes to systems","text":"","threats":[["insider-misuse",0.1,"likelihood"],["data-exposure",0.1,"likelihood"],["defacement",0.1,"likelihood"]]},{"id":"8.33","ref":"8.33","title":"Protecting data used for testing","text":"","threats":[["data-exposure",0.2,"likelihood"],["data-theft",0.1,"likelihood"]]},{"id":"8.34","ref":"8.34","title":"Limiting audit impact on live systems","text":"","threats":[]}]}]; +})(typeof globalThis!=='undefined'?globalThis:this); diff --git a/public/index.html b/public/index.html index ce6b915..0d0a62d 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@ +
    +

    Steering

    +
    +